
🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在算法学习的过程中,动态规划始终是一个绕不开的重要主题.它不仅是解决复杂问题的高效工具,也是面试和竞赛中出现频率极高的核心考点.对于初学者而言,动态规划之所以难,往往不在于代码实现本身,而在于如何理解"状态""转移"和"最优子结构"这些抽象概念.而斐波那契数列,正是理解动态规划思想最经典、最基础的入门模型.表面上看,斐波那契数列只是一个简单的递推问题:后一个数等于前两个数之和.但如果从算法设计的角度深入分析,就会发现其中蕴含着动态规划的核心思想------将原问题拆解为规模更小的子问题,并通过保存中间结果来避免重复计算,从而显著提升效率.也正因为如此,斐波那契数列常被视为学习动态规划的第一把钥匙.本文将围绕斐波那契数列模型展开,从暴力递归入手,逐步分析其时间复杂度问题,再引出记忆化搜索和自底向上的动态规划写法,帮助读者循序渐进地理解动态规划的本质与实现方式,为后续学习更复杂的动态规划问题打下坚实基础.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!
目录
1.动态规划算法思想背景
动态规划(Dynamic Programming,简称 DP)是一种通过分阶段求解问题 、保存中间结果来提高计算效率的算法设计思想.它的核心理念在于:对于一个复杂问题,如果其求解过程可以拆分为若干个相互关联的子问题,那么就可以先求解子问题,再由子问题的结果逐步推出原问题的答案,从而避免大量重复计算.
动态规划思想最早由美国数学家理查德·贝尔曼(Richard Bellman)在 20 世纪 50 年代提出,最初主要应用于运筹学、最优化理论以及决策过程研究中.随着计算机科学的发展,这一思想逐渐被广泛应用到算法设计领域,成为解决最优化问题、计数问题、路径问题和序列问题的重要方法之一.
从本质上讲,动态规划是对暴力递归的一种优化.许多问题在递归求解时,往往会重复计算相同的子问题,导致时间复杂度急剧上升.而动态规划正是通过记录已经求出的状态结果,在后续计算中直接复用这些结果,进而将原本低效的指数级算法优化为多项式级别的高效算法.
动态规划能够成立,通常依赖两个关键条件:
- 问题具有最优子结构,即原问题的最优解可以由子问题的最优解构成;
- 问题具有重复子问题,即在求解过程中,相同的子问题会被多次访问.只有同时具备这些特征,才能通过状态定义和状态转移方程,将问题系统化地拆解并高效求解.
在实际学习中,动态规划不仅是一种算法技巧,更是一种分析问题和构建解法的思维方式.它要求我们从整体问题中抽象出"状态",找出状态之间的递推关系,并根据计算顺序设计合理的求解过程.因此,掌握动态规划,不仅有助于解决具体算法题,也能够提升对复杂问题结构的理解能力.
正因为动态规划兼具理论价值与实践意义,它已成为算法学习中的核心内容之一.无论是经典的斐波那契数列、背包问题,还是路径规划、最长公共子序列、区间合并等问题,背后都体现了动态规划"以空间换时间""由小推大"的基本思想.
2.斐波那契数列模型背景介绍
斐波那契数列是数学与计算机科学中一个非常经典的数列模型,通常定义为:前两个数固定为0和1,从第三项开始,每一项都等于前两项之和.即:
F ( 0 ) = 0 , F ( 1 ) = 1 F(0)=0,\quad F(1)=1 F(0)=0,F(1)=1
F ( n ) = F ( n − 1 ) + F ( n − 2 ) ( n ≥ 2 ) F(n)=F(n-1)+F(n-2)\quad (n\ge 2) F(n)=F(n−1)+F(n−2)(n≥2)
这一数列最早来源于中世纪数学家斐波那契提出的兔子繁殖问题.该问题通过描述兔群数量随时间增长的规律,引出了这样一种递推关系:当前阶段的结果,依赖于前面两个阶段的状态.这种"当前问题由更小规模子问题推导而来"的特点,使斐波那契数列成为研究递归思想和动态规划算法的典型案例.
在算法学习中,斐波那契数列之所以重要,并不仅仅因为它形式简单,而是因为它完整体现了动态规划问题的基本特征.首先,它具有明显的重复子问题,例如在递归求解 F ( n ) F(n) F(n)时, F ( n − 1 ) F(n-1) F(n−1)和 F ( n − 2 ) F(n-2) F(n−2)的计算过程中会反复出现相同的子问题;其次,它满足最优子结构或递推结构,即原问题的解可以由子问题的解直接构成.正因如此,斐波那契数列常被用作讲解暴力递归、记忆化搜索、状态转移方程以及空间优化等内容的入门模型.
从教学角度来看,斐波那契数列模型是连接"数学递推思想"和"程序设计实现"之间的重要桥梁.通过这个模型,学习者不仅可以直观理解递归到动态规划的优化过程,还能够逐步掌握如何抽象状态、设计转移方程以及分析算法复杂度.因此,斐波那契数列不仅是动态规划的起点,也为后续学习背包问题、路径问题、区间动态规划等更复杂的模型奠定了基础.
3.第N个泰波那契数(OJ题)

算法流程解法(动态规划)
-
状态表示 :
这道题可以根据题目的要求直接定义出状态表示:
dp[i]表示:第i个泰波那契数的值. -
状态转移方程 :
题目已经非常贴心的告诉我们了:
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3] -
初始化 :
从我们的递推公式可以看出,
dp[i]在i = 0以及i = 1的时候是没有办法进行推导的,因为dp[-2]或dp[-1]不是一个有效的数据.因此我们需要在填表之前,将
0, 1, 2位置的值初始化.题目中已经告诉我们dp[0] = 0,dp[1] = dp[2] = 1. -
填表顺序 :
毫无疑问是从左往右.
-
返回值 :
应该返回
dp[n]的值.

核心代码
cpp
//使用一维数组
class Solution {
public:
//函数功能:求解第n个泰波那契数
//参数n:目标泰波那契数的下标
//返回值:第n个泰波那契数的值
int tribonacci(int n) {
//边界条件1:n=0返回0,n=1返回1(题目给定的初始值)
if (n == 0 || n == 1)
return n;
//定义dp数组:dp[i] 表示第i个泰波那契数
//数组大小为n+1,保证能存储到下标为n的元素
vector<int> dp(n + 1);
//初始化dp数组:题目给定的前3个泰波那契数初始值
dp[0] = 0; //第0个泰波那契数
dp[1] = 1; //第1个泰波那契数
dp[2] = 1; //第2个泰波那契数
//核心循环:从左往右填充dp数组(从第3个开始递推)
//泰波那契数递推公式:T(n) = T(n-1) + T(n-2) + T(n-3)
for (int i = 3; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
//返回最终结果:第n个泰波那契数
return dp[n];
}
};
//滚动数组优化
class Solution {
public:
//函数功能:计算第n个泰波那契数(滚动数组优化版,空间复杂度O(1))
//泰波那契数列规则:T(0)=0, T(1)=1, T(2)=1, T(n)=T(n-1)+T(n-2)+T(n-3)
int tribonacci(int n) {
//边界条件1:n=0时,直接返回0
if(n == 0) return 0;
//边界条件2:n=1或n=2时,直接返回1
if(n == 1 || n == 2) return 1;
//滚动变量定义:
//a 保存 T(i-3) 的值
//b 保存 T(i-2) 的值
//c 保存 T(i-1) 的值
//d 保存当前计算的 T(i) 的值
int a = 0, b = 1, c = 1, d = 0;
//从第3项开始,循环递推计算到第n项
for(int i = 3; i <= n; i++)
{
//核心递推公式:当前项 = 前三项之和
d = a + b + c;
//滚动更新变量:为下一次循环做准备
a = b; //原T(i-2) 变成 新的T(i-3)
b = c; //原T(i-1) 变成 新的T(i-2)
c = d; //新计算的T(i) 变成 新的T(i-1)
}
//循环结束后,d即为第n个泰波那契数
return d;
}
};
完整测试代码
cpp
#include <iostream>
using namespace std;
class Solution {
public:
int tribonacci(int n) {
if(n == 0) return 0;
if(n == 1 || n == 2) return 1;
int a = 0, b = 1, c = 1, d = 0;
for(int i = 3; i <= n; i++)
{
d = a + b + c;
a = b;
b = c;
c = d;
}
return d;
}
};
int main() {
Solution sol; //创建解法对象
//定义测试用例(泰波那契数列标准值)
int testCases[] = {0, 1, 2, 3, 4, 5, 6, 10};
//标准结果:T0=0, T1=1, T2=1, T3=2, T4=4, T5=7, T6=13, T10=149
cout << "泰波那契数列测试结果:" << endl;
//遍历测试所有用例
for(int n : testCases) {
int res = sol.tribonacci(n);
cout << "n = " << n << " -> 结果:" << res << endl;
}
return 0;
}

4.三步问题(OJ题)

算法思路:解法(动态规划)
-
状态表示
这道题可以根据经验 + 题目要求直接定义出状态表示:
dp[i]表示:到达i位置时,一共有多少种方法. -
状态转移方程
以
i位置状态的最近的一步,来分情况讨论:如果
dp[i]表示小孩上第i阶楼梯的所有方式,那么它应该等于所有上一步的方式之和:i. 上一步上一级台阶,
dp[i] += dp[i - 1];ii. 上一步上两级台阶,
dp[i] += dp[i - 2];iii. 上一步上三级台阶,
dp[i] += dp[i - 3];
综上所述,dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3].
需要注意的是,这道题目说明,由于结果可能很大,需要对结果取模.
在计算的时候,三个值全部加起来再取模,即 (dp[i - 1] + dp[i - 2] + dp[i - 3]) % MOD 是不可取的,同学们可以试验一下,n 取题目范围内最大值时,网站会报错 signed integer overflow.
对于这类需要取模的问题,我们每计算一次(两个数相加/乘等),都需要取一次模.否则,万一发生了溢出,我们的答案就错了.
-
初始化
从我们的递推公式可以看出,
dp[i]在i = 0,i = 1以及i = 2的时候是没有办法进行推导的,因为dp[-3]、dp[-2]或dp[-1]不是一个有效的数据.因此我们需要在填表之前,将
1, 2, 3位置的值初始化.根据题意,
dp[1] = 1,dp[2] = 2,dp[3] = 4. -
填表顺序
毫无疑问是从左往右.
-
返回值
应该返回
dp[n]的值.

核心代码
cpp
class Solution {
public:
//取模常量:防止数值溢出,题目要求结果对 1e9+7 取模
const int MOD = 1e9 + 7;
//函数功能:计算上 n 级台阶的总方法数
//规则:每次可以走 1 步、2 步 或 3 步
int waysToStep(int n) {
//动态规划标准解题四步走
//1. 创建 dp 表
//2. 初始化
//3. 填表
//4. 返回结果
//边界条件:n 为 1/2/3 时,直接返回固定结果(提前处理,避免数组操作)
if(n == 1 || n == 2) return n;
if(n == 3) return 4;
//1.创建 dp 表
//dp[i] 表示:上到第 i 级台阶,总共有多少种方法
//数组大小 n+1,保证下标能取到 n
vector<int> dp(n + 1);
//2.初始化 dp 数组(基础值,无法通过递推公式得到)
dp[1] = 1; //上1级台阶:只有1种方法(走1步)
dp[2] = 2; //上2级台阶:2种方法(1+1 / 2)
dp[3] = 4; //上3级台阶:4种方法(1+1+1 / 1+2 / 2+1 / 3)
//3.填表:从第4级开始,从左往右递推计算
//状态转移方程:dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
//每一步相加后都取模,避免整数溢出(核心优化)
for(int i = 4; i <= n; i++)
dp[i] = ((dp[i - 1] + dp[i - 2]) % MOD + dp[i - 3]) % MOD;
//4.返回结果:第n级台阶的总方法数
return dp[n];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
const int MOD = 1e9 + 7;
int waysToStep(int n) {
//1. 创建 dp 表
//2. 初始化
//3. 填表
//4. 返回
//处理边界情况
if(n == 1 || n == 2) return n;
if(n == 3) return 4;
vector<int> dp(n + 1);
dp[1] = 1, dp[2] = 2, dp[3] = 4;
for(int i = 4; i <= n; i++)
dp[i] = ((dp[i - 1] + dp[i - 2]) % MOD + dp[i - 3]) % MOD;
return dp[n];
}
};
int main() {
Solution sol;
//测试用例:台阶数 1~6
int testCases[] = {1, 2, 3, 4, 5, 6};
cout << "三步上楼梯 测试结果:" << endl;
cout << "每次可走1/2/3级台阶,求上n级台阶的方法数" << endl;
cout << "----------------------------------------" << endl;
//遍历测试所有用例
for(int n : testCases) {
int res = sol.waysToStep(n);
cout << "台阶数 n = " << n << " → 总方法数:" << res << endl;
}
return 0;
}

5.使用最小花费爬楼梯(OJ题)

算法思路:解法(动态规划)解法⼀:
-
状态表示 :
这道题可以根据经验 + 题目要求直接定义出状态表示:
第一种:以
i位置为结尾,
dp[i]表示:到达i位置时的最小花费.(注意:到达i位置的时候,i位置的钱不需要算上) -
状态转移方程 :
根据最近的一步,分情况讨论:
- 先到达
i - 1的位置,然后支付cost[i - 1],接下来走一步走到i位置:
dp[i - 1] + cost[i - 1]; - 先到达
i - 2的位置,然后支付cost[i - 2],接下来走一步走到i位置:
dp[i - 2] + cost[i - 2].
-
初始化 :
从我们的递推公式可以看出,我们需要先初始化
i = 0,以及i = 1位置的值.容易得到dp[0] = dp[1] = 0,因为不需要任何花费,就可以直接站在第 0 层和第 1 层上. -
填表顺序 :
根据状态转移方程可得,遍历的顺序是从左往右.
-
返回值 :
根据状态表示以及题目要求,需要返回
dp[n]位置的值.

核心代码
cpp
class Solution
{
public:
//参数cost:每一级台阶对应的花费数组,每次可以爬1级或2级台阶
//目标:爬到楼梯顶部(最后一级台阶的上方)的最小花费
int minCostClimbingStairs(vector<int>& cost)
{
//获取台阶的总数量
int n = cost.size();
//1.创建dp表
//dp[i] 表示:爬到第 i 级台阶位置的最小花费
//数组大小为 n+1:因为要爬到顶部(第n级位置),需要覆盖0~n所有位置
vector<int> dp(n + 1, 0);
//2.初始化dp数组
//初始状态:站在第0级、第1级台阶,不需要任何花费,直接站立
dp[0] = dp[1] = 0;
//3.填表:从第2级台阶开始,从左往右递推计算
for (int i = 2; i < n + 1; i++)
//状态转移方程:
//到达第i级台阶有两种方式:
//①从i-1级爬1步上来:总花费 = 到i-1级的最小花费 + i-1级台阶的花费
//②从i-2级爬2步上来:总花费 = 到i-2级的最小花费 + i-2级台阶的花费
//取两种方式的最小值,作为到达i级台阶的最小花费
dp[i] = min(cost[i - 1] + dp[i - 1], cost[i - 2] + dp[i - 2]);
//4.返回结果
//dp[n] 就是爬到楼梯顶部的最小花费
return dp[n];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
int minCostClimbingStairs(vector<int>& cost)
{
int n = cost.size();
//初始化一个 dp 表
vector<int> dp(n + 1, 0);
//初始化
dp[0] = dp[1] = 0;
//填表
for (int i = 2; i < n + 1; i++)
//根据状态转移方程得
dp[i] = min(cost[i - 1] + dp[i - 1], cost[i - 2] + dp[i - 2]);
//返回结果
return dp[n];
}
};
int main() {
Solution sol;
//测试用例1:力扣官方示例1
vector<int> cost1 = {10, 15, 20};
cout << "测试用例1:cost = [10,15,20]" << endl;
cout << "最小花费:" << sol.minCostClimbingStairs(cost1) << endl << endl;
//测试用例2:力扣官方示例2
vector<int> cost2 = {1, 100, 1, 1, 1, 100, 1, 1, 100, 1};
cout << "测试用例2:cost = [1,100,1,1,1,100,1,1,100,1]" << endl;
cout << "最小花费:" << sol.minCostClimbingStairs(cost2) << endl;
return 0;
}

算法思路:解法(动态规划)解法二:
-
状态表示:
这道题可以根据经验 + 题目要求直接定义出状态表示:
第二种:以
i位置为起点.
dp[i]表示:从i位置出发,到达楼顶,此时的最小花费. -
状态转移方程:
根据最近的一步,分情况讨论:
- 支付
cost[i],往后走一步,接下来从i + 1的位置出发到终点:dp[i + 1] + cost[i]; - 支付
cost[i],往后走两步,接下来从i + 2的位置出发到终点:dp[i + 2] + cost[i];
我们要的是最小花费,因此 dp[i] = min(dp[i + 1], dp[i + 2]) + cost[i].
-
初始化:
为了保证填表的时候不越界,我们需要初始化最后两个位置的值,结合状态表示易得:
dp[n - 1] = cost[n - 1],dp[n - 2] = cost[n - 2] -
填表顺序:
根据状态转移方程可得,遍历的顺序是从右往左.
-
返回值:
根据状态表示以及题目要求,需要返回
dp[n]位置的值.
核心代码
cpp
class Solution
{
public:
// 函数功能:计算爬楼梯的最小花费(反向动态规划解法)
// 参数cost:每级台阶的花费数组,每次可爬1级或2级台阶
// 核心思路:从楼顶倒推,计算每个位置到楼顶的最小花费
int minCostClimbingStairs(vector<int>& cost)
{
//动态规划解题四步模板
//1.创建 dp 表
//2.初始化
//3.确定填表顺序
//4.确定返回值
//获取台阶的总数量
int n = cost.size();
//1.创建 dp 表
//dp[i] 表示:从第 i 级台阶出发,到达楼顶的最小花费
vector<int> dp(n);
//2.初始化 dp 数组(最后两个台阶)
//最后一级台阶(n-1):直接跳1步到楼顶,花费=当前台阶费用
dp[n - 1] = cost[n - 1];
//倒数第二级台阶(n-2):直接跳2步到楼顶,花费=当前台阶费用
dp[n - 2] = cost[n - 2];
//3.填表顺序:从右往左(从 n-3 遍历到 0)
//状态转移方程:dp[i] = min(跳1步, 跳2步) + 当前台阶花费
for(int i = n - 3; i >= 0; i--)
dp[i] = min(dp[i + 1], dp[i + 2]) + cost[i];
//4.返回值
//可以选择从第0级 或 第1级台阶开始,取两者的最小值
return min(dp[0], dp[1]);
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
int minCostClimbingStairs(vector<int>& cost)
{
//1.创建 dp 表
//2.初始化
//3.填表顺序
//4.返回值
int n = cost.size();
//dp[i] 表示:从第i级台阶出发,到达楼顶的最小花费
vector<int> dp(n);
//初始化最后两级台阶:直接跳到底楼顶,花费为当前台阶费用
dp[n - 1] = cost[n - 1], dp[n - 2] = cost[n - 2];
//从右往左填表,倒推计算每一级台阶的最小花费
for(int i = n - 3; i >= 0; i--)
dp[i] = min(dp[i + 1], dp[i + 2]) + cost[i];
// 可以从0级或1级台阶起步,取最小花费
return min(dp[0], dp[1]);
}
};
int main() {
Solution sol;
// 测试用例1:力扣官方示例1
vector<int> cost1 = {10, 15, 20};
cout << "测试用例1:cost = [10,15,20]" << endl;
cout << "最小花费:" << sol.minCostClimbingStairs(cost1) << endl << endl;
// 测试用例2:力扣官方示例2
vector<int> cost2 = {1, 100, 1, 1, 1, 100, 1, 1, 100, 1};
cout << "测试用例2:cost = [1,100,1,1,1,100,1,1,100,1]" << endl;
cout << "最小花费:" << sol.minCostClimbingStairs(cost2) << endl;
return 0;
}

6.解码方法(OJ题)

算法思路:解法(动态规划):
-
状态表示:
根据以往的经验,对于大多数线性
dp,我们经验上都是以某个位置结束或者开始做文章,这里我们继续尝试用i位置为结尾结合题目要求来定义状态表示.
dp[i]表示:字符串中[0, i]区间上,一共有多少种编码方法. -
状态转移方程:
定义好状态表示,我们就可以分析
i位置的dp值,如何由前面或者后面的信息推导出来.关于
i位置的编码状况,我们可以分为下面两种情况:i. 让
i位置上的数单独解码成一个字母;ii. 让
i位置上的数与i - 1位置上的数结合,解码成一个字母.
下面我们就上面的两种解码情况,继续分析:
-
让
i位置上的数单独解码成一个字母,就存在解码成功和解码失败两种情况:i. 解码成功:当
i位置上的数在[1, 9]之间的时候,说明i位置上的数是可以单独解码的,那么此时[0, i]区间上的解码方法应该等于[0, i - 1]区间上的解码方法.因为[0, i - 1]区间上的所有解码结果,后面填上一个i位置解码后的字母就可以了.此时dp[i] = dp[i - 1];ii. 解码失败:当
i位置上的数是0的时候,说明i位置上的数是不能单独解码的,那么此时[0, i]区间上不存在解码方法.因为i位置如果单独参与解码,但是解码失败了,那么前面做的努力就全部白费了.此时dp[i] = 0. -
让
i位置上的数与i - 1位置上的数结合在一起,解码成一个字母,也存在解码成功和解码失败两种情况:i. 解码成功:当结合的数在
[10, 26]之间的时候,说明[i - 1, i]两个位置是可以解码成功的,那么此时[0, i]区间上的解码方法应该等于[0, i - 2]区间上的解码方法,原因同上.此时dp[i] = dp[i - 2];ii. 解码失败:当结合的数在
[0, 9]和[27, 99]之间的时候,说明两个位置结合后解码失败(这里一定要注意00 01 02 03 04 ......这几种情况),那么此时[0, i]区间上的解码方法就不存在了,原因依旧同上.此时dp[i] = 0.
综上所述:dp[i] 最终的结果应该是上面四种情况下,解码成功的两种的累加和(因为我们关心的是解码方法,既然解码失败,就不用加入到最终结果中去),因此可以得到状态转移方程(dp[i] 默认初始化为 0):
i. 当 s[i] 上的数在 [1, 9] 区间上时:dp[i] += dp[i - 1];
ii. 当 s[i - 1] 与 s[i] 上的数结合后,在 [10, 26] 之间的时候:dp[i] += dp[i - 2];
如果上述两个判断都不成立,说明没有解码方法,dp[i] 就是默认值 0.
- 初始化:
方法一(直接初始化):
由于可能要用到i - 1以及i - 2位置上的dp值,因此要先初始化前两个位置.
初始化 dp[0]:
i. 当 s[0] == '0' 时,没有编码方法,结果 dp[0] = 0;
ii. 当 s[0] != '0' 时,能编码成功,dp[0] = 1
初始化 dp[1]:
i. 当 s[1] 在 [1, 9] 之间时,能单独编码,此时 dp[1] += dp[0](原因同上,dp[1] 默认为 0 )
ii. 当 s[0] 与 s[1] 结合后的数在 [10, 26] 之间时,说明在前两个字符中,又有一种编码方式,此时 dp[1] += 1
方法二(添加辅助位置初始化):
可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:
i. 辅助结点里面的值要保证后续填表是正确的;
ii. 下标的映射关系
-
填表顺序:
毫无疑问是从左往右
-
返回值:
应该返回
dp[n - 1]的值,表示在[0, n - 1]区间上的编码方法.

核心代码
cpp
//使⽤直接初始化的⽅式解决问题
class Solution
{
public:
//规则:1-26 分别对应 A-Z,0不能单独解码,0开头的数字无法解码
int numDecodings(string s)
{
int n = s.size(); //获取字符串长度
vector<int> dp(n); //1.创建dp表
//dp[i] 表示:字符串 s[0...i] 区间的解码方法总数
//2.初始化dp数组:处理第一个字符
// 第一个字符非0,有1种解码方法;为0,无法解码(0种)
dp[0] = s[0] != '0';
//边界情况:字符串只有1个字符,直接返回结果
if(n == 1) return dp[0];
//初始化第二个字符 dp[1]
//情况1:第二个字符单独解码(要求 1<=s[1]<=9),方法数 += 第一个字符的解码数
if(s[1] <= '9' && s[1] >= '1')
dp[1] += dp[0];
//情况2:前两个字符组合解码(要求数值在 10~26 之间),方法数 +1
int t = (s[0] - '0') * 10 + s[1] - '0'; //字符转数字,计算组合值
if(t >= 10 && t <= 26)
dp[1] += 1;
//3. 填表:从第三个字符开始,从左往右递推
for(int i = 2; i < n; i++)
{
//情况1:当前字符单独解码(1-9有效),方法数 += 前一位的解码数
if(s[i] <= '9' && s[i] >= '1')
dp[i] += dp[i - 1];
//情况2:当前字符与前一个字符组合解码(10-26有效)
int t = (s[i - 1] - '0') * 10 + s[i] - '0';
if(t >= 10 && t <= 26)
dp[i] += dp[i - 2]; //方法数 += 前两位的解码数
}
//4. 返回结果:整个字符串的解码方法总数
return dp[n - 1];
}
};
//使⽤添加辅助结点的⽅式初始化
class Solution
{
public:
//优化点:使用 n+1 大小的dp数组 + 虚拟下标,简化初始化逻辑,避免边界判断
int numDecodings(string s)
{
int n = s.size(); //获取字符串的长度
//1.创建dp表:dp[i] 表示 字符串前 i 个字符的解码方法总数
vector<int> dp(n + 1);
//2.初始化dp数组
dp[0] = 1; //虚拟节点:前0个字符(空字符串)有1种解码方法,保证后续计算合法
dp[1] = s[0] != '0'; //前1个字符:非0则1种方法,为0则0种
//3.填表:从左往右遍历,i 代表前i个字符
for (int i = 2; i <= n; i++)
{
//情况1:第 i 个字符(s[i-1])单独编码
//字符不为0时有效,解码方法数 += 前i-1个字符的方法数
if (s[i - 1] != '0')
dp[i] += dp[i - 1];
//情况2:第 i-1 和 i 个字符(s[i-2]、s[i-1])联合编码
int t = (s[i - 2] - '0') * 10 + s[i - 1] - '0';
//组合数字在 10~26 之间时有效,解码方法数 += 前i-2个字符的方法数
if (t >= 10 && t <= 26)
dp[i] += dp[i - 2];
}
//4.返回结果:整个字符串(前n个字符)的解码方法总数
return dp[n];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution
{
public:
int numDecodings(string s)
{
//优化版动态规划:使用虚拟节点简化初始化
int n = s.size();
//dp[i] 表示:字符串前 i 个字符的解码方法总数
vector<int> dp(n + 1);
dp[0] = 1; //虚拟节点:空字符串的解码方法为1,保证后续计算合法
dp[1] = s[0] != '0'; //第一个字符非0则1种方法,为0则0种
//填表:从左往右递推计算
for (int i = 2; i <= n; i++)
{
//情况1:当前字符单独编码(非0有效)
if (s[i - 1] != '0')
dp[i] += dp[i - 1];
//情况2:当前字符与前一字符联合编码(10~26有效)
int t = (s[i - 2] - '0') * 10 + s[i - 1] - '0';
if (t >= 10 && t <= 26)
dp[i] += dp[i - 2];
}
//返回整个字符串的解码方法总数
return dp[n];
}
};
int main() {
Solution sol;
//测试用例1:力扣官方示例1
string s1 = "12";
cout << "测试用例1:s = \"" << s1 << "\"" << endl;
cout << "解码方法数:" << sol.numDecodings(s1) << endl << endl;
//测试用例2:力扣官方示例2
string s2 = "226";
cout << "测试用例2:s = \"" << s2 << "\"" << endl;
cout << "解码方法数:" << sol.numDecodings(s2) << endl << endl;
//测试用例3:包含无效0的情况
string s3 = "06";
cout << "测试用例3:s = \"" << s3 << "\"" << endl;
cout << "解码方法数:" << sol.numDecodings(s3) << endl << endl;
//测试用例4:边界情况-单个字符0
string s4 = "0";
cout << "测试用例4:s = \"" << s4 << "\"" << endl;
cout << "解码方法数:" << sol.numDecodings(s4) << endl << endl;
//测试用例5:边界情况-单个有效字符
string s5 = "1";
cout << "测试用例5:s = \"" << s5 << "\"" << endl;
cout << "解码方法数:" << sol.numDecodings(s5) << endl;
return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容:【动态规划算法】(从入门到精通:路径问题)
每日心灵鸡汤:"我们吞咽了太多意义,其实生命只需要呼吸"
人们总是喜欢为各种事情赋予特定的意义,以显示其存在价值.于是,为着这三言两语的意义,我们久经风霜又翻山越岭,遍体鳞伤还要咬着牙说一句"坚持不懈,久炼成钢".像是往鸟儿的足上绑了块石头,谁飞得更高,谁就是最值得嘉奖的鸟儿.我们都忘了,其实一开始,它们只需要飞翔,而不需要比较飞翔之高低.
一如我们的生命,只需要呼吸就够了,不用驮着石头蹉跎.
