目录
3. 746. 爬楼梯的变异:三步问题(waysToStep)
动态规划(DP)是算法中的重要思想,核心是“用已求结果推导当前结果”,常能将暴力解法的高复杂度优化为线性。本文通过四道经典题目,拆解DP的基础思路与避坑要点。
1. 1137. 第 N 个泰波那契数
题目核心
求泰波那契数列的第n项,数列定义为: T(0)=0,T(1)=1,T(2)=1,T(n)=T(n-1)+T(n-2)+T(n-3) (n≥3)。
解题思路
- 状态定义:无需额外DP数组,用三个变量 a、b、c 分别代表 T(n-3)、T(n-2)、T(n-1) ,通过迭代更新变量值,避免空间浪费。
- 递推逻辑:从n=3开始,每次计算 T(n)=a+b+c ,再将 a→b 、 b→c 、 c→T(n) ,逐步推进到第n项。
- 边界处理:直接判断n=0、1、2的情况,返回固定值,避免进入循环。
代码实现
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;
for(int i=3;i<=n;i++)
{
int d = a + b + c;
a = b;
b = c;
c = d;
}
return c;
}
};
易错点
- 无需用数组存储所有项:新手易习惯性定义 dp[n] 数组,但本题只需前三项结果,用变量迭代可将空间复杂度从O(n)降至O(1)。
2. 746. 使用最小花费爬楼梯
题目核心
楼梯有n阶,每次可爬1或2阶,每阶对应“爬上去的成本” cost[i] ,求到达楼顶(第n阶,无成本)的最小成本。
解题思路
- 状态定义: dp[i] 表示到达第i阶的最小成本。
- 递推逻辑:到达第i阶只有两种路径:
1. 从第i-1阶爬1阶,成本为 dp[i-1] + cost[i-1] ( cost[i-1] 是第i-1阶的成本);
2. 从第i-2阶爬2阶,成本为 dp[i-2] + cost[i-2] 。
取两者最小值作为 dp[i] ,即 dp[i] = min(dp[i-1[icost[i-1], dp[i-2[icost[i-2]) 。
- 边界处理: dp[0] = 0 (起点在第0阶,无需成本)、 dp[1] = 0 (可直接从第0阶或第1阶起步,均无额外成本)。
代码实现
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
vector<int> dp(n+1);
for (int i = 2; i <= n; i++)
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
return dp[n];
}
};
易错点
- 混淆“阶数”与“cost数组下标”:楼顶是第n阶,对应 dp[n] ,而 cost 数组仅到 cost[n-1] (第n-1阶的成本),需避免下标越界。
- 初始状态错误:部分人会将 dp[1] 设为 cost[0] ,但题目允许从第1阶直接起步,无需先爬第0阶,因此 dp[1] 应为0。
3. 746. 爬楼梯的变异:三步问题(waysToStep)
题目核心
求爬到n阶台阶的方法数,每次可爬1、2或3阶,结果需对 1e9+7 取模(防止溢出)。
解题思路
- 状态定义: dp[i] 表示爬到第i阶的总方法数。
- 递推逻辑:到达第i阶的路径来自前3阶(爬1阶到i、爬2阶到i、爬3阶到i),因此 dp[i] = dp[i-1] + dp[i-2] + dp[i-3] 。
- 边界处理: dp[1]=1 、 dp[2]=2 、 dp[3]=4 (直接枚举所有可能路径)。
- 溢出处理:n较大时(如n=1e6),结果会超出 int 范围,需用 long long 存储中间值,并每次计算后取模。
代码实现
class Solution {
public:
int waysToStep(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
if (n == 3) return 4;
long long a = 1, b = 2, c = 4;
long long d;
for (int i = 4; i <= n; ++i) {
d = (a + b + c) % 1000000007;
a = b;
b = c;
c = d;
}
return c;
}
};
易错点
- 未处理数值溢出: int 最大约2e9,当n≥30时,方法数会超过 int 范围,必须用 long long 存储中间变量,且每次计算后取模(而非最后取模,避免中间值溢出)。
- 边界值记忆错误:易将 dp[3] 误记为3(忽略“1+1+1”“1+2”“2+1”“3”四种路径),需准确枚举初始值。
4. 91. 解码方法
题目核心
将字符串 s (仅含数字)解码为字母(A=1,B=2,…,Z=26),求总解码方法数。
解题思路
- 状态定义: dp[i] 表示字符串前i个字符( s[0..i-1] )的解码方法数。
- 递推逻辑:分两种情况判断当前字符的解码方式:
1. 单个字符解码:若 s[i-1] != '0' (0无法单独解码),则 dp[i] += dp[i-1] (前i-1个字符的方法数可直接延续);
2. 两个字符解码:若前两个字符组成的数字 t 满足 10≤t≤26 (如“12”可解为L,“27”不可解),则 dp[i] += dp[i-2] (前i-2个字符的方法数延续)。
- 边界处理:
- dp[0] = 1 (空字符串有1种解码方式,作为递推基础);
- dp[1] = (s[0] != '0') ? 1 : 0 (单个字符若为0则无法解码)。
代码实现
class Solution {
public:
int numDecodings(string s) {
int n=s.size();
vector<int>dp(n+1);
dp[0]=1; // 空字符串的解码方法数
dp[1]=s[0]!='0'; // 单个字符的解码情况
if(n==1)return dp[1];
for(int i=2;i<=n;i++)
{
// 单个字符解码
if(s[i-1]!='0')dp[i[i=dp[i-1];
// 两个字符解码
int t=(s[i-2]-'0')*10+(s[i-1]-'0');
if(t>=10&&t<=26)dp[i[i=dp[i-2];
}
return dp[n];
}
};
易错点
- 忽略“0”的特殊处理:
- 单个“0”无法解码,若 s[i-1] == '0' ,则不能用单个字符解码;
- 两个字符中,若前一个是“0”(如“06”),组成的数字是6,不满足 10≤t≤26 ,无法解码。
- 边界 dp[0] 的意义:新手易忽略 dp[0]=1 ,但当i=2且两个字符可解码时(如“12”), dp[2] = dp[1] + dp[0] , dp[0] 的1是“空字符串+两个字符”的解码方式基础。
- 字符串下标与 dp 下标错位: dp[i] 对应前i个字符( s[0] 到 s[i-1] ),计算两个字符时需取 s[i-2] 和 s[i-1] ,避免下标越界。
总结:动态规划的通用解题步骤
1. 定义状态:明确 dp[i] 代表的具体含义(如“到第i阶的方法数”“前i个字符的解码数”);
2. 推导递推公式:找到“当前状态与前几个状态的关系”(如泰波那契是前3项和,解码是单/双字符两种情况);
3. 处理边界条件:确定初始值(如 dp[0] 、 dp[1] ),避免递推起始错误;
4. 优化空间(可选):若递推仅依赖前k个状态(如泰波那契依赖前3个),可用变量代替数组,将空间复杂度从O(n)降至O(1);
5. 规避特殊情况:如数值溢出(取模、用 long long )、无效输入(如“0”“27”)。
掌握这四道题的思路,可轻松应对大部分基础DP问题,后续复杂DP(如二维DP、状态压缩)也能以此为基础扩展。