【动态规划算法】(从入门到精通:路径问题)


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

你是否有过这样的瞬间:站在一个复杂的网格迷宫前,需要计算出从左上角到右下角有多少条不同的路径?或者面对一张城市地图,想在错综复杂的街道中找到那条最短的路线?这些看似直观的"寻路"谜题,背后其实都指向了同一种强大的算法思想------动态规划.动态规划常常被初学者视为算法学习路上的一道坎,"状态"和"转移方程"这些词听起来玄而又玄.但事实上,它并不是凭空创造的魔法,而是一种将大问题拆解成小问题、并巧妙地避免重复计算的朴素智慧.而"路径问题",正是打开这扇智慧之门最理想的钥匙.为什么是路径问题?因为它们足够具体、足够形象.你可以在脑海中清晰地想象出那个网格,每一步只能向右或向下,每到达一个格子,最优解都依赖于它左边和上边格子的结果.这种层层递进、天然自洽的结构,完美地契合了动态规划"最优子结构"和"无后效性"的核心特质.在这篇文章中,我将带你从最经典的"不同路径"问题出发,逐步深入,一路攻克各种路径问题.我们的目标不仅仅是让你记住几道题的解法,而是帮你建立起一套完整的动态规划思维方式:如何定义状态、如何找出状态转移方程、如何优化空间复杂度.从入门到精通,让我们一步步,把路径走通,把动态规划吃透.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.路径问题思想介绍

路径问题是计算机科学、图论和运筹学中的核心问题,其核心思想是在特定约束条件下,寻找从起点到终点的可行路径或最优路径 ,广泛应用于导航系统、网络路由、游戏AI、物流规划等领域.
(1)路径的本质定义

  • 通路(Walk):顶点与边的交替序列,允许重复顶点和边
  • 迹(Trail):无重复边的通路
  • 路径(Path):无重复顶点的迹(也称简单路径),是最常见的路径定义
  • 回路(Circuit):起点与终点相同的路径,也称环

(2)路径问题的分类体系-->按优化目标分类

  1. 最短路径问题:寻找权值总和最小的路径

    • 单源最短路径:从一个起点到所有其他顶点
    • 单对最短路径:从一个起点到一个终点
    • 全源最短路径:所有顶点对之间的最短路径
  2. 最长路径问题:寻找权值总和最大的路径

    • 适用于有向无环图(DAG),可通过拓扑排序解决
    • 一般图中为NP难问题,无多项式时间算法
  3. 路径计数问题:计算从起点到终点的所有可行路径数量

    • 典型如LeetCode"不同路径"问题,常用动态规划解决

(3)路径问题的分类体系-->按路径约束分类

  1. 欧拉路径问题:经过每条边恰好一次的路径

    • 欧拉回路:起点与终点相同的欧拉路径
    • 判定条件:无向图所有顶点度数为偶数;有向图每个顶点入度等于出度
  2. 哈密顿路径问题:经过每个顶点恰好一次的路径

    • 哈密顿回路:起点与终点相同的哈密顿路径
    • 目前无充要条件,属于NP完全问题
  3. 带约束路径问题:路径需满足特定条件(如禁止某些节点/边、时间窗口等)

    • 典型如中国邮递员问题(最短路径覆盖所有边)
    • 旅行商问题(TSP,最短哈密顿回路)

(4)路径问题的核心思想

  1. 状态表示:定义问题的状态空间,描述当前所处位置、已访问节点、累计代价等关键信息

    • 如最短路径中用dist[v]表示起点到v的最短距离
    • 动态规划中用dp[i][j]表示从起点到(i,j)的路径数或最优值
  2. 状态转移:建立状态之间的转换规则,描述如何从一个状态到达另一个状态

    • 如最短路径中的"松弛操作":dist[v] = min(dist[v], dist[u] + weight(u,v))
    • 矩阵路径中的dp[i][j] = dp[i-1][j] + dp[i][j-1](只能向下/向右)
  3. 边界条件:定义初始状态和终止状态,确保问题有明确的起点和终点

    • 如最短路径中起点dist[s] = 0,其他节点初始为无穷大
    • 矩阵路径中dp[0][0] = 1(起点只有1条路径)
  4. 最优子结构:问题的最优解包含其子问题的最优解,是动态规划和贪心算法的基础

    • 如最短路径中,从s到t的最短路径若经过u,则s到u和u到t的路径也必须是最短的
  5. 重叠子问题:子问题会被多次计算,通过记忆化或制表避免重复计算

    • 动态规划的核心优势,将指数级时间复杂度降至多项式级别

路径问题的本质是在约束条件下的状态空间搜索与优化,拆解一切路径问题:

  1. 建模:将问题抽象为图结构(顶点表示状态,边表示状态转换)
  2. 状态定义:明确描述当前状态的关键信息
  3. 转移规则:建立状态之间的转换关系
  4. 优化策略:根据问题类型选择贪心、动态规划、启发式搜索等方法
  5. 边界处理:确保问题有明确的起点和终点
  6. 状态与状态转移方程:这是把思想落实为代码的关键.状态,就是"到达某个格子时的结果",用dp[i][j]表示.状态转移方程,描述状态之间的关系.以最简单的"不同路径"为例,它就是:dp[i][j] = dp[i-1][j] + dp[i][j-1]

不同路径问题的解决方法虽有差异,但都遵循"分解问题→解决子问题→合并结果"的基本思路,通过高效的算法设计将复杂问题简化,实现从理论到实践的跨越.当你彻底吃透路径问题,会发现更复杂的问题,无非是状态的维度变多了、转移的规则变复杂了,但骨架思想完全相同.接下来,我们就将带着这套思想框架,深入到具体的题目变形中去,一步步实现从入门到精通.


2.不同路径(OJ题)


算法思路:解法(动态规划):

  1. 状态表示:

    对于这种路径类的问题,我们的状态表示一般有两种形式:

    i. 从 [i, j] 位置出发...

    ii. 从起始位置出发,到达 [i, j] 位置...

    这里选择第二种定义状态表示的方式:
    dp[i][j] 表示:走到 [i, j] 位置处,一共有多少种方式.

  2. 状态转移方程:

    简单分析一下.如果 dp[i][j] 表示到达 [i, j] 位置的方法数,那么到达 [i, j] 位置之前的一小步,有两种情况:

    i. 从 [i, j] 位置的上方([i - 1, j] 的位置)向下走一步,转移到 [i, j] 位置;

    ii. 从 [i, j] 位置的左方([i, j - 1] 的位置)向右走一步,转移到 [i, j] 位置.

    由于我们要求的是有多少种方法,因此状态转移方程就呼之欲出了:dp[i][j] = dp[i - 1][j] + dp[i][j - 1].

  3. 初始化:

    可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:

    i. 辅助结点里面的值要保证后续填表是正确的;

    ii. 下标的映射关系.

    在本题中,添加一行,并且添加一列后,只需将 dp[0][1] 的位置初始化为 1 即可.

  4. 填表顺序:

    根据状态转移方程的推导来看,填表的顺序就是从上往下填每一行,在填写每一行的时候从左往右.

  5. 返回值:

    根据状态表示,我们要返回 dp[m][n] 的值.

核心代码

cpp 复制代码
class Solution
{
public:
    //m: 网格的行数  n: 网格的列数
    //函数功能:计算从网格左上角到右下角的所有不同路径数(只能向右/向下走)
    int uniquePaths(int m, int n)
    {
        //1.创建dp表:
        //多开一行一列(辅助空间),避免处理边界越界问题
        //dp[i][j] 表示:从起点走到 (i,j) 位置的总路径数
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));

        //2.初始化:
        //关键初始化!让 dp[1][1] 能正确计算出 1(起点只有1种路径)
        dp[0][1] = 1;

        //3.填表:按照 从上往下、从左往右 的顺序填充dp表
        //遍历每一行
        for (int i = 1; i <= m; i++)
            //遍历每一列
            for (int j = 1; j <= n; j++)
                //4.状态转移方程:
                //到达 (i,j) 的路径数 = 从上方来的路径数 + 从左方来的路径数
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];

        //5.返回值:
        //网格右下角 (m,n) 位置的总路径数就是最终答案
        return dp[m][n];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution
{
public:
    int uniquePaths(int m, int n)
    {
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); // 创建 (m+1)行(n+1)列 的dp表,初始值全0
        dp[0][1] = 1; //初始化,保证起点(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]; //返回右下角位置的总路径数
    }
};

int main()
{
    Solution sol;

    //测试用例1:3行7列,预期结果 28
    int res1 = sol.uniquePaths(3, 7);
    cout << "3行7列的路径数:" << res1 << endl;

    //测试用例2:3行2列,预期结果 3
    int res2 = sol.uniquePaths(3, 2);
    cout << "3行2列的路径数:" << res2 << endl;

    //测试用例3:1行1列(起点即终点),预期结果 1
    int res3 = sol.uniquePaths(1, 1);
    cout << "1行1列的路径数:" << res3 << endl;

    //测试用例4:2行2列,预期结果 2
    int res4 = sol.uniquePaths(2, 2);
    cout << "2行2列的路径数:" << res4 << endl;

    return 0;
}

3.不同路径||(OJ题)


算法思路:解法(动态规划):

本题为不同路径的变型,只不过有些地方有障碍物,只要在状态转移上稍加修改就可解决.

  1. 状态表示:

    对于这种路径类的问题,我们的状态表示一般有两种形式:

    i. 从 [i, j] 位置出发...

    ii. 从起始位置出发,到达 [i, j] 位置...

    这里选择第二种定义状态表示的方式:
    dp[i][j] 表示:走到 [i, j] 位置处,一共有多少种方式.

  2. 状态转移:

    简单分析一下.如果 dp[i][j] 表示到达 [i, j] 位置的方法数,那么到达 [i, j] 位置之前的一小步,有两种情况:

    i. 从 [i, j] 位置的上方([i - 1, j] 的位置)向下走一步,转移到 [i, j] 位置;

    ii. 从 [i, j] 位置的左方([i, j - 1] 的位置)向右走一步,转移到 [i, j] 位置.

    但是,[i - 1, j][i, j - 1] 位置都是可能有障碍的,此时从上面或者左边是不可能到达 [i, j] 位置的,也就是说,此时的方法数应该是 0.

    由此我们可以得出一个结论,只要这个位置上有障碍物,那么我们就不需要计算这个位置上的值,直接让它等于 0 即可.

  3. 初始化:

    可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:

    i. 辅助结点里面的值要保证后续填表是正确的;

    ii. 下标的映射关系.

    在本题中,添加一行,并且添加一列后,只需将 dp[1][0] 的位置初始化为 1 即可.

  4. 填表顺序:

    根据状态转移的推导,填表的顺序就是从上往下填每一行,每一行从左往右.

  5. 返回值:

    根据状态表示,我们要返回的结果是 dp[m][n].

核心代码

cpp 复制代码
class Solution {
public:
    //ob: 障碍物网格(1表示有障碍,0表示无障碍)
    int uniquePathsWithObstacles(vector<vector<int>>& ob) {
        //获取网格的行数m和列数n
        int m = ob.size(), n = ob[0].size();

        //创建dp表:(m+1)行(n+1)列,用辅助边界简化初始化
        //dp[i][j] 表示到达网格中(i-1, j-1)位置的路径数
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));

        //初始化辅助结点:让起点(1,1)能正确计算出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];

        //返回右下角位置的总路径数
        return dp[m][n];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
    //参数ob:障碍物网格,1表示有障碍,0表示无障碍
    int uniquePathsWithObstacles(vector<vector<int>>& ob) {
        int m = ob.size(), n = ob[0].size();
        //创建dp表:(m+1)行(n+1)列,用辅助边界简化初始化和边界处理
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        dp[1][0] = 1; // 初始化辅助结点,让起点(1,1)能正确计算出1种路径

        //填表:从上往下、从左往右遍历每个位置
        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
                //如果当前位置没有障碍物,才计算路径数;有障碍时dp[i][j]保持默认的0
                if(ob[i - 1][j - 1] == 0)
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];

        return dp[m][n];
    }
};

int main() {
    Solution sol;

    // 测试用例1:经典示例(中间有障碍物)
    // 网格:
    // 0 0 0
    // 0 1 0
    // 0 0 0
    // 预期结果:2
    vector<vector<int>> test1 = {
            {0, 0, 0},
            {0, 1, 0},
            {0, 0, 0}
    };
    cout << "测试用例1(中间障碍)的路径数:" << sol.uniquePathsWithObstacles(test1) << endl;

    // 测试用例2:起点就是障碍物(直接无法出发)
    // 网格:1 0
    // 预期结果:0
    vector<vector<int>> test2 = {{1, 0}};
    cout << "测试用例2(起点障碍)的路径数:" << sol.uniquePathsWithObstacles(test2) << endl;

    // 测试用例3:单格无障碍物(起点即终点)
    // 网格:0
    // 预期结果:1
    vector<vector<int>> test3 = {{0}};
    cout << "测试用例3(单格无障碍)的路径数:" << sol.uniquePathsWithObstacles(test3) << endl;

    // 测试用例4:单格有障碍物(起点即终点但被挡住)
    // 网格:1
    // 预期结果:0
    vector<vector<int>> test4 = {{1}};
    cout << "测试用例4(单格有障碍)的路径数:" << sol.uniquePathsWithObstacles(test4) << endl;

    // 测试用例5:路径被完全阻断(无法到达终点)
    // 网格:
    // 0 1
    // 1 0
    // 预期结果:0
    vector<vector<int>> test5 = {
            {0, 1},
            {1, 0}
    };
    cout << "测试用例5(路径阻断)的路径数:" << sol.uniquePathsWithObstacles(test5) << endl;

    return 0;
}

4.珠宝的最高价值(OJ题)


算法思路:解法(动态规划):

  1. 状态表示:

    对于这种路径类的问题,我们的状态表示一般有两种形式:

    i. 从 [i, j] 位置出发...

    ii. 从起始位置出发,到达 [i, j] 位置...

    这里选择第二种定义状态表示的方式:
    dp[i][j] 表示:走到 [i, j] 位置处,此时的最大价值.

  2. 状态转移方程:

    对于 dp[i][j],我们发现想要到达 [i, j] 位置,有两种方式:

    i. 从 [i, j] 位置的上方 [i - 1, j] 位置,向下走一步,此时到达 [i, j] 位置能拿到的礼物价值为 dp[i - 1][j] + grid[i][j];

    ii. 从 [i, j] 位置的左边 [i, j - 1] 位置,向右走一步,此时到达 [i, j] 位置能拿到的礼物价值为 dp[i][j - 1] + grid[i][j];

    我们要的是最大值,因此状态转移方程为:
    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j].

  3. 初始化:

    可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:

    i. 辅助结点里面的值要保证后续填表是正确的;

    ii. 下标的映射关系.

    在本题中,添加一行,并且添加一列后,所有的值都为 0 即可.

  4. 填表顺序:

    根据状态转移方程,填表的顺序是从上往下填写每一行,每一行从左往右.

  5. 返回值:

    根据状态表示,我们应该返回 dp[m][n] 的值.

核心代码

cpp 复制代码
class Solution {
public:
    //frame:二维网格,每个元素代表当前位置的珠宝价值
    int jewelleryValue(vector<vector<int>>& frame) {
        //1.创建 dp 表
        //2.初始化
        //3.填表
        //4.返回结果

        //获取网格的行数 m 和列数 n
        int m = frame.size(), n = frame[0].size();
        //创建 (m+1)行(n+1)列 的dp表,默认初始化为0(辅助边界,简化边界判断)
        //dp[i][j]:到达网格 (i-1,j-1) 位置时,能获得的最大价值
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        
        //填表:从上往下遍历每一行,每行从左往右遍历
        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
                //状态转移:当前最大价值 = max(上方最大价值, 左方最大价值) + 当前位置珠宝价值
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + frame[i - 1][j - 1];
        
        //返回右下角位置的最大价值
        return dp[m][n];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm> 
using namespace std;

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];
    }
};

int main() {
    Solution sol;

    //测试用例1:标准3x3网格
    vector<vector<int>> test1 = {
            {1,3,1},
            {1,5,1},
            {4,2,1}
    };
    cout << "测试用例1 最大价值:" << sol.jewelleryValue(test1) << endl; // 预期结果:12

    //测试用例2:单行网格
    vector<vector<int>> test2 = {{1,2,3,4}};
    cout << "测试用例2 最大价值:" << sol.jewelleryValue(test2) << endl; // 预期结果:10

    //测试用例3:单列网格
    vector<vector<int>> test3 = {{1},{2},{3},{4}};
    cout << "测试用例3 最大价值:" << sol.jewelleryValue(test3) << endl; // 预期结果:10

    //测试用例4:单个格子
    vector<vector<int>> test4 = {{10}};
    cout << "测试用例4 最大价值:" << sol.jewelleryValue(test4) << endl; // 预期结果:10

    return 0;
}

5.下降路径最小和(OJ题)


算法思路:解法(动态规划):

关于这一类题,由于我们做过类似的,因此状态表示以及状态转移是比较容易分析出来的.比较难的地方可能就是对于边界条件的处理.

  1. 状态表示:

    对于这种路径类的问题,我们的状态表示一般有两种形式:

    i. 从 [i, j] 位置出发,到达目标位置有多少种方式;

    ii. 从起始位置出发,到达 [i, j] 位置,一共有多少种方式.

    这里选择第二种定义状态表示的方式:
    dp[i][j] 表示:到达 [i, j] 位置时,所有下降路径中的最小和.

  2. 状态转移方程:

    对于普遍位置 [i, j],根据题意得,到达 [i, j] 位置可能有三种情况:

    i. 从正上方 [i - 1, j] 位置转移到 [i, j] 位置;

    ii. 从左上方 [i - 1, j - 1] 位置转移到 [i, j] 位置;

    iii. 从右上方 [i - 1, j + 1] 位置转移到 [i, j] 位置;

    我们要的是三种情况下的最小值,然后再加上矩阵在 [i, j] 位置的值.

    于是状态转移方程为:
    dp[i][j] = min(dp[i - 1][j], min(dp[i - 1][j - 1], dp[i - 1][j + 1])) + matrix[i][j].

  3. 初始化:

    可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:

    i. 辅助结点里面的值要保证后续填表是正确的;

    ii. 下标的映射关系.

    在本题中,需要加上一行,并且加上两列.所有的位置都初始化为无穷大,然后将第一行初始化为 0 即可.

  4. 填表顺序:

    根据状态表示,填表的顺序是从上往下.

  5. 返回值:

    注意这里不是返回 dp[m][n] 的值!

    题目要求只要到达最后一行就行了,因此这里应该返回dp 表中最后一行的最小值.

核心代码

cpp 复制代码
class Solution
{
public:
    int minFallingPathSum(vector<vector<int>>& matrix)
    {
        int n = matrix.size();
        //创建dp表:n+1行(辅助行)、n+2列(辅助列),初始值为无穷大INT_MAX
        //dp[i][j] 表示到达矩阵第(i-1)行第(j-1)列位置时,下降路径的最小和
        vector<vector<int>> dp(n + 1, vector<int>(n + 2, INT_MAX));
        
        //初始化辅助行:dp[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;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm> 
using namespace std;

class Solution
{
public:
    int minFallingPathSum(vector<vector<int>>& matrix)
    {
        //1.获取矩阵边长(正方形矩阵)
        int n = matrix.size();
        //2.创建 dp 表:n+1行(辅助行)+ n+2列(左右辅助列),初始值为无穷大
        vector<vector<int>> dp(n + 1, vector<int>(n + 2, INT_MAX));

        //3.初始化:第一行辅助行全部置0,保证第一行元素计算正确
        for(int j = 0; j < n + 2; j++)
            dp[0][j] = 0;

        //4.填表:从上往下逐行计算
        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];

        //5.结果:最后一行的最小值就是答案
        int ret = INT_MAX;
        for(int j = 1; j <= n; j++)
            ret = min(ret, dp[n][j]);

        return ret;
    }
};

int main() {
    Solution sol;

    // 测试用例1:标准3x3矩阵
    // 预期结果:13
    vector<vector<int>> test1 = {
            {2, 1, 3},
            {6, 5, 4},
            {7, 8, 9}
    };
    cout << "测试用例1 最小下降路径和:" << sol.minFallingPathSum(test1) << endl;

    // 测试用例2:2x2矩阵(含负数)
    // 预期结果:-59
    vector<vector<int>> test2 = {
            {-19, 57},
            {-40, -5}
    };
    cout << "测试用例2 最小下降路径和:" << sol.minFallingPathSum(test2) << endl;

    // 测试用例3:1x1矩阵(边界场景)
    // 预期结果:1
    vector<vector<int>> test3 = {{1}};
    cout << "测试用例3 最小下降路径和:" << sol.minFallingPathSum(test3) << endl;

    return 0;
}

6.最小路径和(OJ题)


算法思路:解法(动态规划):

像这种表格形式的动态规划,是非常容易得到状态表示以及状态转移方程的,可以归结到不同路径一类的题里面.

  1. 状态表示:

    对于这种路径类的问题,我们的状态表示一般有两种形式:

    i. 从 [i, j] 位置出发...

    ii. 从起始位置出发,到达 [i, j] 位置...

    这里选择第二种定义状态表示的方式:
    dp[i][j] 表示:到达 [i, j] 位置处,最小路径和是多少.

  2. 状态转移:

    简单分析一下.如果 dp[i][j] 表示到达 [i, j] 位置处的最小路径和,那么到达 [i, j] 位置之前的一小步,有两种情况:

    i. 从 [i - 1, j] 向下走一步,转移到 [i, j] 位置;

    ii. 从 [i, j - 1] 向右走一步,转移到 [i, j] 位置.

    由于到 [i, j] 位置两种情况,并且我们要找的是最小路径,因此只需要这两种情况下的最小值,再加上 [i, j] 位置上本身的值即可.

    也就是:dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]

  3. 初始化:

    可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:

    i. 辅助结点里面的值要保证后续填表是正确的;

    ii. 下标的映射关系.

    在本题中,添加一行,并且添加一列后,所有位置的值可以初始化为无穷大,然后让 dp[0][1] = dp[1][0] = 1 即可.

  4. 填表顺序:

    根据状态转移方程的推导来看,填表的顺序就是从上往下填每一行,每一行从左往右.

  5. 返回值:

    根据状态表示,我们要返回的结果是 dp[m][n].

核心代码

cpp 复制代码
class Solution
{
public:
    int minPathSum(vector<vector<int>>& grid)
    {
        //获取网格的行数 m 和列数 n
        int m = grid.size(), n = grid[0].size();
        
        //创建 dp 表:(m+1)行(n+1)列,初始值为无穷大INT_MAX
        //dp[i][j] 表示到达原网格 (i-1, j-1) 位置的最小路径和
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
        
        //初始化辅助结点,保证起点(1,1)的计算正确
        dp[0][1] = dp[1][0] = 0;

        //填表:从上往下、从左往右遍历每个位置
        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
                //状态转移方程:
                //当前位置的最小路径和 = min(从上方来的路径和, 从左方来的路径和) + 当前位置的值
                dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];

        //返回右下角位置的最小路径和
        return dp[m][n];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

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];
    }
};

int main() {
    Solution sol;

    // 测试用例1:标准网格
    vector<vector<int>> test1 = {
            {1,3,1},
            {1,5,1},
            {4,2,1}
    };
    cout << "测试用例1 最小路径和:" << sol.minPathSum(test1) << endl; // 预期结果:7

    // 测试用例2:单行网格
    vector<vector<int>> test2 = {{1,2,3}};
    cout << "测试用例2 最小路径和:" << sol.minPathSum(test2) << endl; // 预期结果:6

    // 测试用例3:单列网格
    vector<vector<int>> test3 = {{1},{2},{3}};
    cout << "测试用例3 最小路径和:" << sol.minPathSum(test3) << endl; // 预期结果:6

    // 测试用例4:单个格子
    vector<vector<int>> test4 = {{10}};
    cout << "测试用例4 最小路径和:" << sol.minPathSum(test4) << endl; // 预期结果:10

    return 0;
}

7.地下城游戏(OJ题)


算法思路:解法(动态规划):

  1. 状态表示:

    这道题如果我们定义成:从起点开始,到达 [i, j] 位置的时候,所需的最低初始健康点数.

    那么我们分析状态转移的时候会有一个问题:那就是我们当前的健康点数还会受到后面的路径的影响.也就是从上往下的状态转移不能很好地解决问题.

    这个时候我们要换一种状态表示:从 [i, j] 位置出发,到达终点时所需要的最低初始健康点数.这样我们在分析状态转移的时候,后续的最佳状态就已经知晓.

    综上所述,定义状态表示为:
    dp[i][j] 表示:从 [i, j] 位置出发,到达终点时所需的最低初始健康点数.

  2. 状态转移方程:

    对于 dp[i][j],从 [i, j] 位置出发,下一步会有两种选择(为了方便理解,设 dp[i][j] 的最终答案是 x):

    i. 走到右边,然后走向终点

    那么我们在 [i, j] 位置的最低健康点数加上这一个位置的消耗,应该要大于等于右边位置的最低健康点数,也就是:x + dungeon[i][j] >= dp[i][j + 1].

    通过移项可得:x >= dp[i][j + 1] - dungeon[i][j].因为我们要的是最小值,因此这种情况下的 x = dp[i][j + 1] - dungeon[i][j];

    ii. 走到下边,然后走向终点

    那么我们在 [i, j] 位置的最低健康点数加上这一个位置的消耗,应该要大于等于下边位置的最低健康点数,也就是:x + dungeon[i][j] >= dp[i + 1][j].

    通过移项可得:x >= dp[i + 1][j] - dungeon[i][j].因为我们要的是最小值,因此这种情况下的 x = dp[i + 1][j] - dungeon[i][j];

    综上所述,我们需要的是两种情况下的最小值,因此可得状态转移方程为:
    dp[i][j] = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]

    但是,如果当前位置的 dungeon[i][j] 是一个比较大的正数的话,dp[i][j] 的值可能变成 0 或者负数.也就是最低点数会小于1,那么骑士就会死.因此我们求出来的 dp[i][j] 如果小于等于 0 的话,说明此时的最低初始值应该为1.处理这种情况仅需让 dp[i][j] 与 1 取一个最大值即可:
    dp[i][j] = max(1, dp[i][j])

  3. 初始化:

    可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:

    i. 辅助结点里面的值要保证后续填表是正确的;

    ii. 下标的映射关系.

    在本题中,在 dp 表最后面添加一行,并且添加一列后,所有的值都先初始化为无穷大,然后让 dp[m][n - 1] = dp[m - 1][n] = 1 即可.

  4. 填表顺序:

    根据状态转移方程,我们需要从下往上填每一行,每一行从右往左.

  5. 返回值:

    根据状态表示,我们需要返回 dp[0][0] 的值.

核心代码

cpp 复制代码
class Solution
{
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon)
    {
        int m = dungeon.size(), n = dungeon[0].size();
        
        //1.创建 dp 表:多开一行一列作为辅助边界,初始值为无穷大INT_MAX
        //dp[i][j] 表示:从位置(i,j)出发,到达终点所需的最低初始健康点数
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
        
        //2.初始化辅助边界:终点的下、右辅助位置设为1(保证终点计算正确)
        dp[m][n - 1] = dp[m - 1][n] = 1;

        //3.填表:从下往上、从右往左遍历每个位置
        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];
                
                //最低健康点数不能小于1(否则骑士死亡),因此取最大值
                dp[i][j] = max(1, dp[i][j]);
            }

        //4.返回起点位置所需的最低初始健康点数
        return dp[0][0];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

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];
    }
};

int main() {
    Solution sol;

    //测试用例1:标准示例
    //预期结果:7
    vector<vector<int>> test1 = {
            {-2, -3, 3},
            {-5, -10, 1},
            {10, 30, -5}
    };
    cout << "测试用例1 最低初始健康点数:" << sol.calculateMinimumHP(test1) << endl;

    //测试用例2:单个格子(负数)
    //预期结果:5
    vector<vector<int>> test2 = {{-4}};
    cout << "测试用例2 最低初始健康点数:" << sol.calculateMinimumHP(test2) << endl;

    //测试用例3:单个格子(正数)
    //预期结果:1
    vector<vector<int>> test3 = {{5}};
    cout << "测试用例3 最低初始健康点数:" << sol.calculateMinimumHP(test3) << endl;

    return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容:【动态规划算法】(简单多状态dp问题入门与经典题型解析)


每日心灵鸡汤:看淡看失,珍惜拥有;不负时光,不负自己.
人生没有最好的年龄,只有最好的心态,我们争不过岁月,也跑不过时间.唯有以自己喜欢的方式,过好每一个日出日落.人生要学会与自己和解,两分看透,三分释怀,五分爱自己,人生的下半场,就要善待自己,不生气,不取悦,永远不要再拿别人的错误来惩罚自己.

相关推荐
王的宝库1 小时前
【Ansible】变量与敏感数据管理:Vault 加密 + Facts 采集详解
笔记·学习·ansible
进击的荆棘1 小时前
C++起始之路——用哈希表封装myunordered_set和myunordered_map
开发语言·c++·stl·哈希算法·散列表·unordered_map·unordered_set
星幻元宇VR3 小时前
VR机动车模拟驾驶系统助力交通安全科普
科技·学习·安全·生活·vr
进击的荆棘3 小时前
C++起始之路——哈希表的实现
数据结构·c++·散列表·哈希
FakeOccupational8 小时前
【数学 密码学】量子通信:光的偏振&极化的量子不确定性特性 + 量子密钥分发 BB84算法步骤
算法·密码学
ZhengEnCi9 小时前
S10-蓝桥杯 17822 乐乐的积木塔
算法
贾斯汀玛尔斯9 小时前
每天学一个算法--拓扑排序(Topological Sort)
算法·深度优先
t***54410 小时前
如何配置Orwell Dev-C++使用Clang
开发语言·c++
大龄程序员狗哥10 小时前
第25篇:Q-Learning算法解析——强化学习中的经典“价值”学习(原理解析)
人工智能·学习·算法