动态规划专题:斐波那契数列模型 + 网格路径DP(LeetCode例题全解析)

动态规划算法------斐波那契数列模型

线性DP通用五步法:

  1. 状态表示:dpi含义(以i结尾 / 以i起点)

  2. 状态转移方程:根据最近一步推导

  3. 初始化:处理下标越界,初始化前面基础项

  4. 填表顺序:从左往右 / 从右往左

  5. 返回值:确定返回哪个dp下标


1、泰波那契数列(LeetCode 1137 easy)

题目描述

算法思路(动态规划)

  1. 状态表示:dpi:第 i 个泰波那契数的值。

  2. 状态转移方程:dpi=dpi-1+dpi-2+dpi-3

  3. 初始化:dp0=0, dp1=1, dp2=1

i=0/1/2不能用递推式,会访问dp-1 dp-2非法下标,手动赋值。

  1. 填表顺序:从左往右。

  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)

题目描述

算法思路

  1. 状态表示:dpi:到达i位置,一共有多少种上楼方式。

  2. 状态转移方程

到达i的最近一步,可以走1、2、3阶过来:

dpi = dpi-1+dpi-2+dpi-3

每一步计算就要取模:dpi = (( (dpi - 1 + dpi - 2) % MOD ) + dpi - 3) % MOD,防止int溢出。

  1. 初始化

dp1=1, dp2=2, dp3=4

i=1,2,3无法用递推,会访问负下标,手动初始化。

  1. 填表顺序:从左往右。

  2. 返回值:返回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];
    }
};

知识点

  1. 大数取模:每一次加法后就要取模,不要最后一次性取模,会整数溢出报错signed integer overflow。

  2. 递推和泰波那契公式一样,但是初始值不同,初始值由题意决定。

3、使用最小花费爬楼梯(LeetCode 746 easy)

两种dp思路:

思路1:dpi到达i位置的最小花费(从左往右)

思路2:dpi从i位置出发到达楼顶最小花费(从右往左)

题目描述

提示:

  • 2 <= cost.length <= 1000
  • 0 <= cost[i] <= 999

解法一:dpi表示到达i位置的最小花费,从左往右填表

  1. 状态表示

dpi:到达i位置时的最小花费。到达i位置不用算i位置的cost。

  1. 状态转移方程

到达i,要么i‑1爬一步上来,要么i‑2爬两步上来。

dpi = min(dpi-1+costi-1, dpi-2+costi-2)

  1. 初始化

dp0=0, dp1=0

站在0、1台阶,不需要花钱。

  1. 填表顺序:从左往右,i从2到n。

  2. 返回值: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位置出发到达楼顶的最小花费,从右往左填表

  1. 状态表示

dpi:从i位置出发,到达楼顶的最小花费。

  1. 状态转移方程

站i,必须先付costi;之后可以走一步到i+1,或者两步到i+2。

dpi=min(dpi+1, dpi+2)+costi

  1. 初始化

数组最后两个元素,直接就是本身cost:

dpn‑1 = costn‑1; dpn‑2 = costn‑2;

  1. 填表顺序:从右往左,i从n‑3遍历到0。

  2. 返回值: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]);
    }
};

知识点

  1. dp的状态定义不唯一:可以定义「到达i」,也可以定义「从i出发」,填表方向会改变。

  2. 审题关键点:楼顶不在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 <= 100
  • s 只包含数字,并且可能包含前导零。

算法思路

  1. 状态表示

dpi:字符串区间0,i一共有多少种解码方法。

  1. 状态转移方程

两种解码方式:

① 当前字符si单独解码:数字必须 1 <= si <= 9,则 dpi += dpi‑1

② 和前一个字符组合解码:数字必须 10 <= 组合数 <= 26,则 dpi += dpi‑2

两种条件都不满足:dpi=0,代表该位置无法解码。

  1. 初始化两种写法

方式1:直接初始化dp0、dp1,手动处理前两个字符边界。

方式2:增加辅助虚拟结点dp0=1,下标映射偏移,简化边界初始化。

  1. 填表顺序:从左往右。

  2. 返回值: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":

  1. dp0=0

  2. dp1 = 1(字符'1')

  3. i=2

情况1:'2'单独解码:dp2 += dp1 → dp2=1

情况2:拼成12合法:dp2 += dp0 → dp2 +=0

最终 dp2=1,正确答案应该是2,直接算错。

因为合并两位的那一条路径直接消失了。

逻辑理解

dp0 代表空串,空串不是输入的字符串,是动态规划的边界条件。

空字符串的解码方案数定义为 1(数学上的乘法单位元,类似阶乘里 0! =1)。

如果写成0,所有"最后两位合并"的分支全部失效。

知识点

  1. 字符串dp,下标映射很容易出错;增加虚拟辅助结点,可以减少边界if。

  2. 0的坑:字符'0'不能单独解码;组合只能是10~26,0x全部无效。

  3. 转移是累加满足条件的分支,条件不成立该分支不加。

整体总结【线性DP通用套路】

  1. 状态定义二选一

dpi:以i位置结尾,从左往右填表(绝大多数)

dpi:从i位置出发,从右往左填表(最小花费爬楼梯解法二)

  1. 转移方程:看最近一步,思考最后一步怎么来到i;把所有来源相加/min/max。

  2. 初始化:防止下标越界,前1‑2项手动赋值。

技巧:增加虚拟辅助下标,简化初始化,尤其字符串类dp。

  1. 填表顺序由转移方程决定,从已知推未知。

  2. 注意溢出:大数题目每一步都要取模。

  3. 空间优化:如果dpi只依赖前面固定k项,可以用滚动变量代替数组O(1)。


动态规划------路径问题

题型分类:路径计数、路径最值、反向DP(地下城游戏)

统一模式:dpij 表示到达(i,j)位置的某种状态;填表顺序:从上往下,从左往右;地下城游戏例外:从下往上、从右往左

5. 不同路径(LeetCode 62 medium)

题目描述

示例 4:

复制代码
输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109

算法思路

  1. 状态表示

dpij:从起点出发,走到位置(i,j)的路径总数量。

  1. 状态转移方程

到达(i,j)只有两个来源:上方(i‑1,j)向下走、左边(i,j‑1)向右走。

dpij=dpi-1j+dpij-1

  1. 初始化(辅助数组技巧:多一行多一列)

创建 dpm+1n+1,全部初始化为0;

dp11=1 (起点位置路径数为1)

  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)

题目描述

算法思路

  1. 状态表示

dpij:从起点走到(i,j)的路径数量。

  1. 状态转移方程

如果当前格子是障碍物:dpij=0(无法到达)

否则:dpij=dpi-1j+dpij-1

  1. 初始化

dpm+1n+1,全部0;dp10=1;

注意:输入数组下标是obstacleGridi‑1j‑1,因为dp数组多了一行一列。

  1. 填表顺序:从上往下,从左往右。

  2. 返回值: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)

题目描述

算法思路

  1. 状态表示

dpij:走到(i,j)位置时,能拿到礼物的最大价值。

  1. 状态转移方程

可以从上(i‑1,j)或者左(i,j‑1)过来,取二者最大值,加上当前格子价值:

dpij=max(dpi-1j,dpij-1)+framei-1j-1

  1. 初始化

dpm+1n+1全部初始化为0。

  1. 填表顺序:从上往下,从左往右。

  2. 返回值: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)

题目描述

算法思路

  1. 状态表示

dpij:到达(i,j)位置,所有下降路径的最小和。

  1. 状态转移方程

上一行有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

  1. 初始化

dpn+1n+2全部初始化为INT_MAX(无穷大);

dp0j=0,第一行初始为0。

  1. 填表顺序:从上往下逐行。

  2. 返回值: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.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 200

算法思路

  1. 状态表示

dpij:到达(i,j)位置的最小路径和。

  1. 状态转移方程

dpij=min(dpi-1j,dpij-1)+gridi-1j-1

  1. 初始化

dpm+1n+1全部初始化为INT_MAX;

dp01=dp10=0。

  1. 填表顺序:从上往下,从左往右。

  2. 返回值: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:从终点倒推回起点。

  1. 状态表示

dpij:从(i,j)位置出发走到终点,所需要的最小初始生命值。

  1. 状态转移方程

下一步可以向右(i,j+1),向下(i+1,j)。

dpij=min(dpi+1j,dpij+1)-dungeonij

骑士血量不能小于1:如果算出来小于等于0,强制设置为1。

dpij=max(1, dpij)

  1. 初始化

dpm+1n+1全部初始化为INT_MAX;

终点旁边:dpmn‑1=dpm‑1n=1。

  1. 填表顺序:从下往上,一行一行;每一行从右往左

  2. 返回值: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通用模板

  1. 定义dpij:到达(i,j)的XX状态(大部分题正向);部分题目需要反向定义。

  2. 分析来源:可以从哪些格子走到当前(i,j),写出转移方程。

  3. 初始化:多一行一列辅助数组,规避边界判断;最值问题初始化为无穷大。

  4. 填表顺序:

正向:从上往下,每行从左往右

地下城游戏:从下往上,每行从右往左

  1. 返回对应dp的值。
相关推荐
落魄大学生之流水线上谋生计2 小时前
接口测试实习指南:从基础方法到面试与现场难题
面试·职场和发展
一只旭宝2 小时前
数据结构与算法复习手册:从写过,到讲清楚
开发语言·数据结构·c++·算法
夏幻灵2 小时前
每日一题:LeetCode 20:有效的括号——栈与哈希映射详解
算法·leetcode·哈希算法
ab1237683 小时前
OpenCV 学习笔记
笔记·opencv·学习
hetao17338373 小时前
2026-10-07 hetao1733837 的刷题记录
c++·算法
程序员清风3 小时前
AI时代建议学点自己真正感兴趣的!
java·后端·面试
Nil2083 小时前
leetcode 279完全平方数
算法·leetcode·职场和发展
longlongzihan3 小时前
前缀积与后缀积 —— LeetCode 238. 除了自身以外数组的乘积
开发语言·c++·leetcode
All for pursuit.3 小时前
【动态规划-1】121.买卖股票的最佳时机
数据结构·c++·算法·leetcode·动态规划