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

你是否有过这样的瞬间:站在一个复杂的网格迷宫前,需要计算出从左上角到右下角有多少条不同的路径?或者面对一张城市地图,想在错综复杂的街道中找到那条最短的路线?这些看似直观的"寻路"谜题,背后其实都指向了同一种强大的算法思想------动态规划.动态规划常常被初学者视为算法学习路上的一道坎,"状态"和"转移方程"这些词听起来玄而又玄.但事实上,它并不是凭空创造的魔法,而是一种将大问题拆解成小问题、并巧妙地避免重复计算的朴素智慧.而"路径问题",正是打开这扇智慧之门最理想的钥匙.为什么是路径问题?因为它们足够具体、足够形象.你可以在脑海中清晰地想象出那个网格,每一步只能向右或向下,每到达一个格子,最优解都依赖于它左边和上边格子的结果.这种层层递进、天然自洽的结构,完美地契合了动态规划"最优子结构"和"无后效性"的核心特质.在这篇文章中,我将带你从最经典的"不同路径"问题出发,逐步深入,一路攻克各种路径问题.我们的目标不仅仅是让你记住几道题的解法,而是帮你建立起一套完整的动态规划思维方式:如何定义状态、如何找出状态转移方程、如何优化空间复杂度.从入门到精通,让我们一步步,把路径走通,把动态规划吃透.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!
目录
1.路径问题思想介绍
路径问题是计算机科学、图论和运筹学中的核心问题,其核心思想是在特定约束条件下,寻找从起点到终点的可行路径或最优路径 ,广泛应用于导航系统、网络路由、游戏AI、物流规划等领域.
(1)路径的本质定义
- 通路(Walk):顶点与边的交替序列,允许重复顶点和边
- 迹(Trail):无重复边的通路
- 路径(Path):无重复顶点的迹(也称简单路径),是最常见的路径定义
- 回路(Circuit):起点与终点相同的路径,也称环
(2)路径问题的分类体系-->按优化目标分类
-
最短路径问题:寻找权值总和最小的路径
- 单源最短路径:从一个起点到所有其他顶点
- 单对最短路径:从一个起点到一个终点
- 全源最短路径:所有顶点对之间的最短路径
-
最长路径问题:寻找权值总和最大的路径
- 适用于有向无环图(DAG),可通过拓扑排序解决
- 一般图中为NP难问题,无多项式时间算法
-
路径计数问题:计算从起点到终点的所有可行路径数量
- 典型如LeetCode"不同路径"问题,常用动态规划解决
(3)路径问题的分类体系-->按路径约束分类
-
欧拉路径问题:经过每条边恰好一次的路径
- 欧拉回路:起点与终点相同的欧拉路径
- 判定条件:无向图所有顶点度数为偶数;有向图每个顶点入度等于出度
-
哈密顿路径问题:经过每个顶点恰好一次的路径
- 哈密顿回路:起点与终点相同的哈密顿路径
- 目前无充要条件,属于NP完全问题
-
带约束路径问题:路径需满足特定条件(如禁止某些节点/边、时间窗口等)
- 典型如中国邮递员问题(最短路径覆盖所有边)
- 旅行商问题(TSP,最短哈密顿回路)
(4)路径问题的核心思想
-
状态表示:定义问题的状态空间,描述当前所处位置、已访问节点、累计代价等关键信息
- 如最短路径中用
dist[v]表示起点到v的最短距离 - 动态规划中用
dp[i][j]表示从起点到(i,j)的路径数或最优值
- 如最短路径中用
-
状态转移:建立状态之间的转换规则,描述如何从一个状态到达另一个状态
- 如最短路径中的"松弛操作":
dist[v] = min(dist[v], dist[u] + weight(u,v)) - 矩阵路径中的
dp[i][j] = dp[i-1][j] + dp[i][j-1](只能向下/向右)
- 如最短路径中的"松弛操作":
-
边界条件:定义初始状态和终止状态,确保问题有明确的起点和终点
- 如最短路径中起点
dist[s] = 0,其他节点初始为无穷大 - 矩阵路径中
dp[0][0] = 1(起点只有1条路径)
- 如最短路径中起点
-
最优子结构:问题的最优解包含其子问题的最优解,是动态规划和贪心算法的基础
- 如最短路径中,从s到t的最短路径若经过u,则s到u和u到t的路径也必须是最短的
-
重叠子问题:子问题会被多次计算,通过记忆化或制表避免重复计算
- 动态规划的核心优势,将指数级时间复杂度降至多项式级别
路径问题的本质是在约束条件下的状态空间搜索与优化,拆解一切路径问题:
- 建模:将问题抽象为图结构(顶点表示状态,边表示状态转换)
- 状态定义:明确描述当前状态的关键信息
- 转移规则:建立状态之间的转换关系
- 优化策略:根据问题类型选择贪心、动态规划、启发式搜索等方法
- 边界处理:确保问题有明确的起点和终点
- 状态与状态转移方程:这是把思想落实为代码的关键.状态,就是"到达某个格子时的结果",用dp[i][j]表示.状态转移方程,描述状态之间的关系.以最简单的"不同路径"为例,它就是:dp[i][j] = dp[i-1][j] + dp[i][j-1]
不同路径问题的解决方法虽有差异,但都遵循"分解问题→解决子问题→合并结果"的基本思路,通过高效的算法设计将复杂问题简化,实现从理论到实践的跨越.当你彻底吃透路径问题,会发现更复杂的问题,无非是状态的维度变多了、转移的规则变复杂了,但骨架思想完全相同.接下来,我们就将带着这套思想框架,深入到具体的题目变形中去,一步步实现从入门到精通.
2.不同路径(OJ题)

算法思路:解法(动态规划):
-
状态表示:
对于这种路径类的问题,我们的状态表示一般有两种形式:
i. 从
[i, j]位置出发...ii. 从起始位置出发,到达
[i, j]位置...这里选择第二种定义状态表示的方式:
dp[i][j]表示:走到[i, j]位置处,一共有多少种方式. -
状态转移方程:
简单分析一下.如果
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]. -
初始化:
可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:
i. 辅助结点里面的值要保证后续填表是正确的;
ii. 下标的映射关系.
在本题中,添加一行,并且添加一列后,只需将
dp[0][1]的位置初始化为1即可. -
填表顺序:
根据状态转移方程的推导来看,填表的顺序就是从上往下填每一行,在填写每一行的时候从左往右.
-
返回值:
根据状态表示,我们要返回
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题)

算法思路:解法(动态规划):
本题为不同路径的变型,只不过有些地方有障碍物,只要在状态转移上稍加修改就可解决.
-
状态表示:
对于这种路径类的问题,我们的状态表示一般有两种形式:
i. 从
[i, j]位置出发...ii. 从起始位置出发,到达
[i, j]位置...这里选择第二种定义状态表示的方式:
dp[i][j]表示:走到[i, j]位置处,一共有多少种方式. -
状态转移:
简单分析一下.如果
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即可. -
初始化:
可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:
i. 辅助结点里面的值要保证后续填表是正确的;
ii. 下标的映射关系.
在本题中,添加一行,并且添加一列后,只需将
dp[1][0]的位置初始化为1即可. -
填表顺序:
根据状态转移的推导,填表的顺序就是从上往下填每一行,每一行从左往右.
-
返回值:
根据状态表示,我们要返回的结果是
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题)

算法思路:解法(动态规划):
-
状态表示:
对于这种路径类的问题,我们的状态表示一般有两种形式:
i. 从
[i, j]位置出发...ii. 从起始位置出发,到达
[i, j]位置...这里选择第二种定义状态表示的方式:
dp[i][j]表示:走到[i, j]位置处,此时的最大价值. -
状态转移方程:
对于
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]. -
初始化:
可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:
i. 辅助结点里面的值要保证后续填表是正确的;
ii. 下标的映射关系.
在本题中,添加一行,并且添加一列后,所有的值都为
0即可. -
填表顺序:
根据状态转移方程,填表的顺序是从上往下填写每一行,每一行从左往右.
-
返回值:
根据状态表示,我们应该返回
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题)

算法思路:解法(动态规划):
关于这一类题,由于我们做过类似的,因此状态表示以及状态转移是比较容易分析出来的.比较难的地方可能就是对于边界条件的处理.
-
状态表示:
对于这种路径类的问题,我们的状态表示一般有两种形式:
i. 从
[i, j]位置出发,到达目标位置有多少种方式;ii. 从起始位置出发,到达
[i, j]位置,一共有多少种方式.这里选择第二种定义状态表示的方式:
dp[i][j]表示:到达[i, j]位置时,所有下降路径中的最小和. -
状态转移方程:
对于普遍位置
[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]. -
初始化:
可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:
i. 辅助结点里面的值要保证后续填表是正确的;
ii. 下标的映射关系.
在本题中,需要加上一行,并且加上两列.所有的位置都初始化为无穷大,然后将第一行初始化为 0 即可.
-
填表顺序:
根据状态表示,填表的顺序是从上往下.
-
返回值:
注意这里不是返回
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题)

算法思路:解法(动态规划):
像这种表格形式的动态规划,是非常容易得到状态表示以及状态转移方程的,可以归结到不同路径一类的题里面.
-
状态表示:
对于这种路径类的问题,我们的状态表示一般有两种形式:
i. 从
[i, j]位置出发...ii. 从起始位置出发,到达
[i, j]位置...这里选择第二种定义状态表示的方式:
dp[i][j]表示:到达[i, j]位置处,最小路径和是多少. -
状态转移:
简单分析一下.如果
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] -
初始化:
可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:
i. 辅助结点里面的值要保证后续填表是正确的;
ii. 下标的映射关系.
在本题中,添加一行,并且添加一列后,所有位置的值可以初始化为无穷大,然后让
dp[0][1] = dp[1][0] = 1即可. -
填表顺序:
根据状态转移方程的推导来看,填表的顺序就是从上往下填每一行,每一行从左往右.
-
返回值:
根据状态表示,我们要返回的结果是
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题)

算法思路:解法(动态规划):
-
状态表示:
这道题如果我们定义成:从起点开始,到达
[i, j]位置的时候,所需的最低初始健康点数.那么我们分析状态转移的时候会有一个问题:那就是我们当前的健康点数还会受到后面的路径的影响.也就是从上往下的状态转移不能很好地解决问题.
这个时候我们要换一种状态表示:从
[i, j]位置出发,到达终点时所需要的最低初始健康点数.这样我们在分析状态转移的时候,后续的最佳状态就已经知晓.综上所述,定义状态表示为:
dp[i][j]表示:从[i, j]位置出发,到达终点时所需的最低初始健康点数. -
状态转移方程:
对于
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]) -
初始化:
可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:
i. 辅助结点里面的值要保证后续填表是正确的;
ii. 下标的映射关系.
在本题中,在
dp表最后面添加一行,并且添加一列后,所有的值都先初始化为无穷大,然后让dp[m][n - 1] = dp[m - 1][n] = 1即可. -
填表顺序:
根据状态转移方程,我们需要从下往上填每一行,每一行从右往左.
-
返回值:
根据状态表示,我们需要返回
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问题入门与经典题型解析)
每日心灵鸡汤:看淡看失,珍惜拥有;不负时光,不负自己.
人生没有最好的年龄,只有最好的心态,我们争不过岁月,也跑不过时间.唯有以自己喜欢的方式,过好每一个日出日落.人生要学会与自己和解,两分看透,三分释怀,五分爱自己,人生的下半场,就要善待自己,不生气,不取悦,永远不要再拿别人的错误来惩罚自己.
