动态规划算法------斐波那契数列模型
线性DP通用五步法:
-
状态表示:dpi含义(以i结尾 / 以i起点)
-
状态转移方程:根据最近一步推导
-
初始化:处理下标越界,初始化前面基础项
-
填表顺序:从左往右 / 从右往左
-
返回值:确定返回哪个dp下标
1、泰波那契数列(LeetCode 1137 easy)
题目描述

算法思路(动态规划)
-
状态表示:dpi:第 i 个泰波那契数的值。
-
状态转移方程:dpi=dpi-1+dpi-2+dpi-3
-
初始化:dp0=0, dp1=1, dp2=1
i=0/1/2不能用递推式,会访问dp-1 dp-2非法下标,手动赋值。
-
填表顺序:从左往右。
-
返回值:返回 dpn
C++代码
版本1:一维数组
cpp
class Solution {
public:
int tribonacci(int n) {
if (n == 0 || n == 1) return n;
vector<int> dp(n + 1); // dp[i]表示第i个泰波那契数
dp[0] = 0, dp[1] = 1, dp[2] = 1; //初始化
//从左往右填表
for (int i = 3; i <= n; i++)
{
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
}
//返回结果
return dp[n];
}
};
版本2:滚动数组优化(空间O(1))
只保留最近3个值,不需要完整数组
cpp
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;
}
};
知识点
三阶递推,类似斐波那契,一次依赖前面3项。
滚动数组:不需要保存全部dp数组,只保存转移需要的最近几个变量,空间复杂度从O(n)降到O(1)。
2、三步问题(面试题 08.01 easy)
题目描述

算法思路
-
状态表示:dpi:到达i位置,一共有多少种上楼方式。
-
状态转移方程
到达i的最近一步,可以走1、2、3阶过来:
dpi = dpi-1+dpi-2+dpi-3
每一步计算就要取模:dpi = (( (dpi - 1 + dpi - 2) % MOD ) + dpi - 3) % MOD,防止int溢出。
- 初始化
dp1=1, dp2=2, dp3=4
i=1,2,3无法用递推,会访问负下标,手动初始化。
-
填表顺序:从左往右。
-
返回值:返回dpn

C++代码
cpp
class Solution {
public:
const int MOD = 1e9 + 7;
int waysToStep(int n) {
//处理边界情况
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];
}
};
知识点
-
大数取模:每一次加法后就要取模,不要最后一次性取模,会整数溢出报错signed integer overflow。
-
递推和泰波那契公式一样,但是初始值不同,初始值由题意决定。
3、使用最小花费爬楼梯(LeetCode 746 easy)
两种dp思路:
思路1:dpi到达i位置的最小花费(从左往右)
思路2:dpi从i位置出发到达楼顶最小花费(从右往左)
题目描述

提示:
2 <= cost.length <= 10000 <= cost[i] <= 999
解法一:dpi表示到达i位置的最小花费,从左往右填表
- 状态表示
dpi:到达i位置时的最小花费。到达i位置不用算i位置的cost。
- 状态转移方程
到达i,要么i‑1爬一步上来,要么i‑2爬两步上来。
dpi = min(dpi-1+costi-1, dpi-2+costi-2)
- 初始化
dp0=0, dp1=0
站在0、1台阶,不需要花钱。
-
填表顺序:从左往右,i从2到n。
-
返回值:dpn,n等于cost.size()。

cpp
class Solution
{
public:
int minCostClimbingStairs(vector<int>& cost)
{
int n = cost.size();
vector<int> dp(n + 1, 0);
dp[0] = dp[1] = 0;
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];
}
};
解法二:dpi表示从i位置出发到达楼顶的最小花费,从右往左填表
- 状态表示
dpi:从i位置出发,到达楼顶的最小花费。
- 状态转移方程
站i,必须先付costi;之后可以走一步到i+1,或者两步到i+2。
dpi=min(dpi+1, dpi+2)+costi
- 初始化
数组最后两个元素,直接就是本身cost:
dpn‑1 = costn‑1; dpn‑2 = costn‑2;
-
填表顺序:从右往左,i从n‑3遍历到0。
-
返回值:min(dp0, dp1),可以从0或者1出发。

cpp
class Solution
{
public:
int minCostClimbingStairs(vector<int>& cost)
{
int n = cost.size();
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];
}
return min(dp[0], dp[1]);
}
};
知识点
-
dp的状态定义不唯一:可以定义「到达i」,也可以定义「从i出发」,填表方向会改变。
-
审题关键点:楼顶不在cost数组内部。
4、解码方法 (LeetCode 91 medium)
题目描述

示例 1:
输入:s = "12"
输出:2
解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输入:s = "226"
输出:3
解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
示例 3:
输入:s = "06"
输出:0
解释:"06" 无法映射到 "F" ,因为存在前导零("6" 和 "06" 并不等价)。
提示:
1 <= s.length <= 100s只包含数字,并且可能包含前导零。
算法思路
- 状态表示
dpi:字符串区间0,i一共有多少种解码方法。
- 状态转移方程
两种解码方式:
① 当前字符si单独解码:数字必须 1 <= si <= 9,则 dpi += dpi‑1
② 和前一个字符组合解码:数字必须 10 <= 组合数 <= 26,则 dpi += dpi‑2
两种条件都不满足:dpi=0,代表该位置无法解码。
- 初始化两种写法
方式1:直接初始化dp0、dp1,手动处理前两个字符边界。
方式2:增加辅助虚拟结点dp0=1,下标映射偏移,简化边界初始化。
-
填表顺序:从左往右。
-
返回值:dpn‑1(n是字符串长度)
C++版本1:直接初始化前两位
cpp
class Solution {
public:
int numDecodings(string s)
{
int n = s.size();
vector<int> dp(n);
//初始化第0位
dp[0] = s[0] != '0';
if(n == 1) return dp[0];
//初始化第1位
if(s[1] <= '9' && s[1] >= '1') dp[1] += dp[0];
int t = (s[0] - '0') * 10 + s[1] - '0';
if(t >= 10 && t <= 26) dp[1] += 1;
//填表
for(int i = 2; i < n; i++)
{
//单独编码
if(s[i] <= '9' && s[i] >= '1')
dp[i] += dp[i - 1];
//和前面字符组合编码
int t = (s[i - 1] - '0') * 10 + s[i] - '0';
if(t >= 10 && t <= 26)
dp[i] += dp[i - 2];
}
return dp[n - 1];
}
};
C++版本2:增加辅助结点简化初始化(推荐)
dp数组长度n+1;dp0=1作为辅助;dp1对应字符串s0。
cpp
class Solution {
public:
int numDecodings(string s)
{
int n = s.size();
vector<int> dp(n + 1);
dp[0] = 1; //辅助结点,保证填表逻辑正确
dp[1] = s[0] != '0';
for (int i = 2; i <= n; i++)
{
//处理单独编码 s[i‑1]是当前字符
if (s[i - 1] != '0')
dp[i] += dp[i - 1];
//和前一个字符组合编码
int t = (s[i - 2] - '0') * 10 + s[i - 1] - '0';
if (t >= 10 && t <= 26)
dp[i] += dp[i - 2];
}
return dp[n];
}
};
如果错误写 dp0 = 0
跑一遍 s="12":
dp0=0
dp1 = 1(字符'1')
i=2
情况1:'2'单独解码:dp2 += dp1 → dp2=1
情况2:拼成12合法:dp2 += dp0 → dp2 +=0
最终 dp2=1,正确答案应该是2,直接算错。
因为合并两位的那一条路径直接消失了。
逻辑理解
dp0 代表空串,空串不是输入的字符串,是动态规划的边界条件。
空字符串的解码方案数定义为 1(数学上的乘法单位元,类似阶乘里 0! =1)。
如果写成0,所有"最后两位合并"的分支全部失效。
知识点
-
字符串dp,下标映射很容易出错;增加虚拟辅助结点,可以减少边界if。
-
0的坑:字符'0'不能单独解码;组合只能是10~26,0x全部无效。
-
转移是累加满足条件的分支,条件不成立该分支不加。
整体总结【线性DP通用套路】
- 状态定义二选一
dpi:以i位置结尾,从左往右填表(绝大多数)
dpi:从i位置出发,从右往左填表(最小花费爬楼梯解法二)
-
转移方程:看最近一步,思考最后一步怎么来到i;把所有来源相加/min/max。
-
初始化:防止下标越界,前1‑2项手动赋值。
技巧:增加虚拟辅助下标,简化初始化,尤其字符串类dp。
-
填表顺序由转移方程决定,从已知推未知。
-
注意溢出:大数题目每一步都要取模。
-
空间优化:如果dpi只依赖前面固定k项,可以用滚动变量代替数组O(1)。
动态规划------路径问题
题型分类:路径计数、路径最值、反向DP(地下城游戏)
统一模式:dpij 表示到达(i,j)位置的某种状态;填表顺序:从上往下,从左往右;地下城游戏例外:从下往上、从右往左
5. 不同路径(LeetCode 62 medium)
题目描述

示例 4:
输入:m = 3, n = 3
输出:6
提示:
1 <= m, n <= 100- 题目数据保证答案小于等于
2 * 109
算法思路
- 状态表示
dpij:从起点出发,走到位置(i,j)的路径总数量。
- 状态转移方程
到达(i,j)只有两个来源:上方(i‑1,j)向下走、左边(i,j‑1)向右走。
dpij=dpi-1j+dpij-1
- 初始化(辅助数组技巧:多一行多一列)
创建 dpm+1n+1,全部初始化为0;
dp11=1 (起点位置路径数为1)
- 填表顺序
从上往下遍历每一行,每行从左往右遍历。
- 返回值:dpmn

C++完整代码
cpp
class Solution
{
public:
int uniquePaths(int m, int n)
{
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
dp[1][1] = 1; // 初始化起点
// 从上往下
for (int i = 1; i <= m; i++)
// 从左往右
for (int j = 1; j <= n; j++)
{
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
return dp[m][n];
}
};
知识点:二维DP的辅助行/列技巧,规避第一行第一列边界判断。
6. 不同路径 II(LeetCode 63 mediun)
题目描述

算法思路
- 状态表示
dpij:从起点走到(i,j)的路径数量。
- 状态转移方程
如果当前格子是障碍物:dpij=0(无法到达)
否则:dpij=dpi-1j+dpij-1
- 初始化
dpm+1n+1,全部0;dp10=1;
注意:输入数组下标是obstacleGridi‑1j‑1,因为dp数组多了一行一列。
-
填表顺序:从上往下,从左往右。
-
返回值:dpmn
C++完整代码
cpp
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& ob) {
int m = ob.size(), n = ob[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
dp[1][0] = 1;
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
if(ob[i - 1][j - 1] == 0)
{
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
// 障碍物自动保持dp[i][j]=0
}
return dp[m][n];
}
};
重点:遇到障碍物直接置0,代表没有路径可以抵达该位置。
7. 珠宝的最高价值(LCR 166)
题目描述

算法思路
- 状态表示
dpij:走到(i,j)位置时,能拿到礼物的最大价值。
- 状态转移方程
可以从上(i‑1,j)或者左(i,j‑1)过来,取二者最大值,加上当前格子价值:
dpij=max(dpi-1j,dpij-1)+framei-1j-1
- 初始化
dpm+1n+1全部初始化为0。
-
填表顺序:从上往下,从左往右。
-
返回值:dpmn
C++完整代码
cpp
class Solution {
public:
int jewelleryValue(vector<vector<int>>& frame) {
int m = frame.size(), n = frame[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + frame[i - 1][j - 1];
}
}
return dp[m][n];
}
};
对比:路径计数用加法;路径最值用max/min。
8.下降路径最小和(LeetCode 931 medium)
题目描述

算法思路
- 状态表示
dpij:到达(i,j)位置,所有下降路径的最小和。
- 状态转移方程
上一行有3个来源:(i‑1,j‑1)左上、(i‑1,j)正上方、(i‑1,j+1)右上。
dpij=min(dpi-1j-1,min(dpi-1j,dpi-1j+1))+matrixi-1j-1
- 初始化
dpn+1n+2全部初始化为INT_MAX(无穷大);
dp0j=0,第一行初始为0。
-
填表顺序:从上往下逐行。
-
返回值:dp最后一行所有元素的最小值(终点可以是最后一行任意位置)

C++完整代码
cpp
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& matrix)
{
int n = matrix.size();
vector<vector<int>> dp(n + 1, vector<int>(n + 2, INT_MAX));
// 第0行全部置0
for(int j = 0; j < n + 2; j++)
dp[0][j] = 0;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= n; j++)
{
dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i-1][j+1])) + matrix[i-1][j-1];
}
}
int ret = INT_MAX;
for(int j = 1; j <= n; j++)
ret = min(ret, dp[n][j]);
return ret;
}
};
重点:终点不是固定右下角,可以是最后一行任意位置,所以要取最后一行最小值;边界用无穷大避免越界。
9.最小路径和(LeetCode 64 medium)
题目描述

提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 2000 <= grid[i][j] <= 200
算法思路
- 状态表示
dpij:到达(i,j)位置的最小路径和。
- 状态转移方程
dpij=min(dpi-1j,dpij-1)+gridi-1j-1
- 初始化
dpm+1n+1全部初始化为INT_MAX;
dp01=dp10=0。
-
填表顺序:从上往下,从左往右。
-
返回值:dpmn

C++完整代码
cpp
class Solution {
public:
int minPathSum(vector<vector<int>>& grid)
{
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
dp[0][1] = dp[1][0] = 0;
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1];
}
return dp[m][n];
}
};
对比礼物最大价值:只是把max换成min;初始化要无穷大。
10.地下城游戏(LeetCode 174 Hard)
题目描述

算法思路
⚠️ 本题不能正向DP!
如果正向定义:dpij到达(i,j)最小初始血量,后面的格子会影响前面的状态,正向无法计算。
✅ 改为反向DP:从终点倒推回起点。
- 状态表示
dpij:从(i,j)位置出发走到终点,所需要的最小初始生命值。
- 状态转移方程
下一步可以向右(i,j+1),向下(i+1,j)。
dpij=min(dpi+1j,dpij+1)-dungeonij
骑士血量不能小于1:如果算出来小于等于0,强制设置为1。
dpij=max(1, dpij)
- 初始化
dpm+1n+1全部初始化为INT_MAX;
终点旁边:dpmn‑1=dpm‑1n=1。
-
填表顺序:从下往上,一行一行;每一行从右往左
-
返回值:dp00(起点位置的最小初始血量)
C++完整代码
cpp
class Solution {
public:
int calculateMinimumHP(vector<vector<int>>& dungeon)
{
int m = dungeon.size(), n = dungeon[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
dp[m][n-1] = dp[m-1][n] = 1;
// 从下往上
for(int i = m - 1; i >= 0; i--)
// 从右往左
for(int j = n - 1; j >= 0; j--)
{
dp[i][j] = min(dp[i+1][j], dp[i][j+1]) - dungeon[i][j];
dp[i][j] = max(1, dp[i][j]);
}
return dp[0][0];
}
};
核心考点:当后续状态会影响当前结果,就要反向DP。
专题总结表格
|------------|---------------------|--------------------------------------------|----------|--------------|---------------|
| 题目 | dp含义 | 转移 | 填表顺序 | 返回值 | 关键点 |
| 62不同路径 | 到达(i,j)路径数 | dpij=dpi‑1j+dpij‑1 | 从上往下,左→右 | dpmn | 计数,加法 |
| 63不同路径II | 到达(i,j)路径数 | 障碍物则dp=0,否则加法 | 从上往下,左→右 | dpmn | 障碍物置0 |
| 剑指47礼物最大价值 | 到达(i,j)最大价值 | max(上,左)+grid | 从上往下,左→右 | dpmn | 求最大值 |
| 931下降路径最小和 | 到达(i,j)最小路径和 | min(左上,上,右上)+matrix | 从上往下 | 最后一行min | 终点可以是最后一行任意位置 |
| 64最小路径和 | 到达(i,j)最小路径和 | min(上,左)+grid | 从上往下,左→右 | dpmn | 求最小值 |
| 174地下城游戏 | 从(i,j)出发到终点所需最小初始血量 | min(下,右)-dungeon,max(1, ...) | 从下往上,右→左 | dp00 | 反向DP,血量必须≥1 |
二维网格DP通用模板
-
定义dpij:到达(i,j)的XX状态(大部分题正向);部分题目需要反向定义。
-
分析来源:可以从哪些格子走到当前(i,j),写出转移方程。
-
初始化:多一行一列辅助数组,规避边界判断;最值问题初始化为无穷大。
-
填表顺序:
正向:从上往下,每行从左往右
地下城游戏:从下往上,每行从右往左
- 返回对应dp的值。