来自力扣的文章解析:
作者:宫水三叶
链接:https://leetcode.cn/leetbook/read/path-problems-in-dynamic-programming/rtd7d2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
DP问题五个核心问题:
- 如何确定可以使用动态规划来求解问题
- 如何确定本题的状态定义
- 如何确定状态转移方程
- 对状态转移的要求
- 如何分析动态规划时间复杂度
本题动态规划
定义
f
[
i
]
[
j
]
f[i][j]
f[i][j]为到达位置
(
i
,
j
)
(i,j)
(i,j)的不同路径数量。
即
f
[
m
−
1
]
[
n
−
1
]
f[m-1][n-1]
f[m−1][n−1]为所求目标,
f
[
0
]
[
0
]
=
1
f[0][0]=1
f[0][0]=1初始条件。
限定只能往下或往右移动
- 下: f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i-1][j] f[i][j]=f[i−1][j]
- 右: f [ i ] [ j ] = f [ i ] [ j − 1 ] f[i][j] = f[i][j-1] f[i][j]=f[i][j−1]
- 能向下和向右,有: f [ i ] [ j ] = f [ i ] [ j − 1 ] + f [ i − 1 ] [ j ] f[i][j] = f[i][j-1]+f[i-1][j] f[i][j]=f[i][j−1]+f[i−1][j]
//代码是自己写的,比较简单
class Solution {
public:
int uniquePaths(int m, int n) {
int f[m][n];
f[0][0]=1;
for(int i=0;i<m;i++){
f[i][0]=1;
}
for(int j=0;j<n;j++){
f[0][j]=1;
}
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i>0&&j>0){
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
}
return f[m-1][n-1];
}
};
复盘五个问题
- 如何确定可以使用动态规划来求解问题
- [有无后效性]:对于某个状态,我们可以只关注状态的值,而不需要关注状态是如何转移过来的话,那么这就是一个无后效性的问题,可以考虑使用 DP 解决。
- 更实在的技巧:通过 数据范围 来猜测,因为DP是一个递推过程,若数据范围 1 0 5 − 1 0 6 10^5 - 10^6 105−106可以考虑是不是一维DP,如果数据范围 1 0 2 − 1 0 3 10^2 - 10^3 102−103可以考虑二维DP。
- 如何确定本题的状态定义
- 一部分题目的状态定义是与「结尾」或「答案」有所关联
- 如何确定状态转移方程
- 状态转移方程就是对「最后一步的分情况讨论」,一定程度上,状态转移方程可以反过来验证我们状态定义猜得是否正确。如果猜了一个状态定义,然后发现无法列出涵盖所有情况(不漏)的状态转移方程,多半就是状态定义猜错了。
- 对状态转移的要求
- 是求最值的话,我们只需要确保「不漏」
- 是求方案数的话,我们需要确保「不重不漏」
- 如何分析动态规划时间复杂度
复杂度/计算量分析,有多少个状态,复杂度/计算量就是多少。一维 DP 的复杂度通常是线性的 O ( n ) O(n) O(n),而二维 DP 的复杂度通常是平方的 O ( n 2 ) O(n^2) O(n2)
拓展
多了某些格子有障碍物(不可达)的限制
//代码是自己写的,比较简单
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
int f[m][n];
//f[0][0]=1;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
f[i][j]=0;
}
}
for(int i=0;i<m;i++){
if(obstacleGrid[i][0]==1){
f[i][0]=0;
break;
}
else{
f[i][0]=1;
}
}
for(int j=0;j<n;j++){
if(obstacleGrid[0][j]==1){
f[0][j]=0;
break;
}
else{
f[0][j]=1;
}
}
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(obstacleGrid[i][j]==1){
f[i][j]=0;
}
else if(i>0&&j>0){
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
}
return f[m-1][n-1];
}
};
作者:宫水三叶
链接:https://leetcode.cn/leetbook/read/path-problems-in-dynamic-programming/rtd7d2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
int f[m][n];
f[0][0]=grid[0][0];
for(int i=1;i<m;i++){
f[i][0]=f[i-1][0]+grid[i][0];
}
for(int j=1;j<n;j++){
f[0][j]=f[0][j-1]+grid[0][j];
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
if(i>0&&j>0){
f[i][j] = f[i - 1][j] < f[i][j - 1] ? f[i - 1][j] : f[i][j - 1];
f[i][j]+=grid[i][j];
}
}
}
return f[m-1][n-1];
}
};
解析
在不同路径上增加了路径成本概念。
定义
f
[
i
]
[
j
]
f[i][j]
f[i][j]为到达位置
(
i
,
j
)
(i,j)
(i,j)的最小总和。
即
f
[
m
−
1
]
[
n
−
1
]
f[m-1][n-1]
f[m−1][n−1]为所求目标,
f
[
0
]
[
0
]
=
g
i
r
d
[
0
]
[
0
]
f[0][0]=gird[0][0]
f[0][0]=gird[0][0]初始条件。
限定只能往下或往右移动
- 下: f [ i ] [ j ] = f [ i − 1 ] [ j ] + g i r d [ i ] [ j ] f[i][j] = f[i-1][j]+gird[i][j] f[i][j]=f[i−1][j]+gird[i][j]
- 右: f [ i ] [ j ] = f [ i ] [ j − 1 ] + g i r d [ i ] [ j ] f[i][j] = f[i][j-1]+gird[i][j] f[i][j]=f[i][j−1]+gird[i][j]
- 能向下和向右,有: f [ i ] [ j ] = m i n ( f [ i ] [ j − 1 ] , f [ i − 1 ] [ j ] ) + + g i r d [ i ] [ j ] f[i][j] =min( f[i][j-1],f[i-1][j] )++gird[i][j] f[i][j]=min(f[i][j−1],f[i−1][j])++gird[i][j]
时间复杂度:
O
(
n
∗
m
)
O(n*m)
O(n∗m)
空间复杂度:
O
(
n
∗
m
)
O(n*m)
O(n∗m)
进阶
如果要输出总和最低的路径呢
从原问题我们知道,我们需要从
(
0
,
0
)
(0,0)
(0,0)一步步到达位置
(
m
−
1
,
n
−
1
)
(m-1,n-1)
(m−1,n−1),也就是我们需要扫描完整个方块(转移完所有的状态),才能得到答案,可以使用额外的数据结构来记录,我们是如何一步步转移到
(
m
−
1
,
n
−
1
)
(m-1,n-1)
(m−1,n−1)。
当整个 DP 过程结束后,我们再用辅助记录的数据结构来回推我们的路径。由于照路径的过程是「倒着」,而输出需要「顺着」。如果希望简化找路径的过程,我们需要对原问题进行等价转换。
将「从 ( 0 , 0 ) (0,0) (0,0)到 ( m − 1 , n − 1 ) (m-1,n-1) (m−1,n−1)的最短路径」转换为「从 ( m − 1 , n − 1 ) (m-1,n-1) (m−1,n−1)到 ( 0 , 0 ) (0,0) (0,0)的最短路径」,同时移动方向从「向下 & 向右」转换为「向上 & 向左。
扩展
如果走动规则调整为「可以往任意方向」且「每个位置最多只能访问一次」,如何求解?
这时候问题就转换为**「图论最短路」**问题,而且是从「特定源点」到「特定汇点」的「单源最短路」问题。
需要根据是否存在「负权边」来分情况讨论:
- 不存在负权边:使用 Dijkstra 算法求解
- 存在负权边:使用 Bellman Ford 或 SPFA 求解