题目概述:
题目链接:点我做题
题解
这个题不同于其他动态规划的关键在在于边界条件。
定义f(i,j)为从左上角到下标为
(
i
,
j
)
(i,j)
(i,j)的位置所需的最小路径和。
当
i
=
=
0
i == 0
i==0,也就是说在第0行时,只能通过起始位置往右走,所以
f
(
0
,
j
)
=
∑
k
=
0
j
g
r
i
d
[
0
]
[
k
]
f(0,j) = \sum_{k=0}^{j}{grid[0][k]}
f(0,j)=∑k=0jgrid[0][k],而不是像普通的矩阵动态规划那样
f
(
0
,
j
)
=
g
r
i
d
[
0
]
[
j
]
f(0,j) = grid[0][j]
f(0,j)=grid[0][j]
同理,当
j
=
=
0
j==0
j==0时,也就是说在第0列时,只能从起始位置往下走才能到达,所以
f
(
i
,
o
)
=
∑
k
=
0
i
g
r
i
d
[
k
]
[
0
]
f(i,o) = \sum_{k=0}^{i}{grid[k][0]}
f(i,o)=∑k=0igrid[k][0],而不是像普通的矩阵动态规划那样
f
(
i
,
0
)
=
g
r
i
d
[
i
]
[
0
]
f(i,0) = grid[i][0]
f(i,0)=grid[i][0]
其他情况下,到达
(
i
,
j
)
(i,j)
(i,j)要么是从
(
i
−
1
,
j
)
(i-1,j)
(i−1,j)向右走一步过来的,要么是从
(
i
,
j
−
1
)
(i,j-1)
(i,j−1)向下走一步过来的,到达
(
i
,
j
)
(i,j)
(i,j)的最短路径是到达(i-1,j)的最短路径与到达
(
i
,
j
−
1
)
(i,j-1)
(i,j−1)的最短路径中的较小值与
g
r
i
d
[
i
]
[
j
]
grid[i][j]
grid[i][j]的和,状态转移方程为:
f
(
i
,
j
)
=
m
i
n
(
f
(
i
−
1
,
j
)
,
f
(
i
,
j
−
1
)
)
+
g
r
i
d
[
i
]
[
j
]
f(i,j)=min(f(i-1,j), f(i,j-1))+grid[i][j]
f(i,j)=min(f(i−1,j),f(i,j−1))+grid[i][j]
符合最优子结构,所以可以使用动态规划,代码如下:
class Solution {
public:
int minPathSum(vector<vector<int>>& grid)
{
int rows = grid.size();
int cols = grid[0].size();
vector<vector<int>> dp(rows, vector<int>(cols));
//dp表示到达这个格子的最小路径
int sum1 = 0;
for (int j = 0; j < cols; ++j)
{
//第一行的格子都只能是往右走过来的 不可能是其他路径过来的
sum1 += grid[0][j];
dp[0][j] = sum1;
}
int sum2 = 0;
for (int i = 0; i < rows; ++i)
{
//同理 第一列的格子只能是从左上角往下走才能到达
//不可能是其他路径过来的
sum2 += grid[i][0];
dp[i][0] = sum2;
}
for (int i = 1; i < rows; ++i)
{
for (int j = 1; j < cols; ++j)
{
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[rows - 1][cols - 1];
}
};
时间复杂度:
O
(
m
n
)
O(mn)
O(mn)
空间复杂度:
O
(
m
n
)
O(mn)
O(mn)
注意到,如果先遍历行再遍历列,本题的状态转移方程仅和上一轮外层循环,即前一行的状态,以及遍历列时上一次的计算结果有关;
同理,如果先遍历列再遍历行,本题的状态转义方程仅和上一轮的外层循环,即前一列的状态,以及遍历行是上一次的计算结果有关;
所以我们可以先比较行数和列数的大小,如果行数小,就在内层循环遍历行,开辟一个数组
d
p
dp
dp来储存上一轮外层遍历,即在上一列时的行遍历结果;如果列数小,就在内层循环遍历列,开闭一个数组
d
p
dp
dp来储存上一轮的外层遍历结果,即在上一行时的列遍历结果。这样空间复杂度就可以优化到
O
(
m
i
n
(
m
,
n
)
)
O(min(m,n))
O(min(m,n))
注意控制边界条件,如果是行数等于0,那么此时的这一行上每一列对应的最短路径仅和前一列的值以及
g
r
i
d
[
i
]
[
j
]
grid[i][j]
grid[i][j]有关;如果列数等于0,那么此时这一列上每一行的对应对端路径仅和前一行的值以及
g
r
i
d
[
i
]
[
j
]
grid[i][j]
grid[i][j]有关,需要注意的是储存行状态时,
d
p
[
i
−
1
]
dp[i - 1]
dp[i−1]等价与
f
(
i
−
1
,
j
)
f(i-1,j)
f(i−1,j),
d
p
[
i
]
dp[i]
dp[i]等价于
f
(
i
,
j
−
1
)
f(i,j-1)
f(i,j−1);储存列状态时,
d
p
[
j
]
dp[j]
dp[j]等价于
f
(
i
−
1
,
j
)
f(i-1,j)
f(i−1,j),
d
p
[
j
−
1
]
dp[j-1]
dp[j−1]等价于
f
(
i
,
j
−
1
)
f(i,j-1)
f(i,j−1),注意据此控制边界条件。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid)
{
int rows = grid.size();
int cols = grid[0].size();
int ret = 0;
if (rows > cols)
{
vector<int> dp(cols);
//如果cols小 就把它放在内层循环
for (int i = 0; i < rows; ++i)
{
for (int j = 0; j < cols; ++j)
{
//dp[j]相当于f(i - 1, j)
//dp[j - 1]相当于f(i, j - 1)
if (i == 0 && j == 0)
{
//首个元素
dp[j] = grid[i][j];
}
else if (i == 0)
{
//如果i==0 表明在第一行
//只能是和前继求和 即dp[j - 1]
dp[j] = dp[j - 1] + grid[i][j];
}
else if (j == 0)
{
//如果j==0 表明是在第1列
//只和上一层的有关
//所以和dp[j]求和
dp[j] = dp[j] + grid[i][j];
}
else
{
//否则 f(i-1, j)和f(i,j-1)中取小
dp[j] = min(dp[j], dp[j - 1]) + grid[i][j];
}
}
}
ret = dp[cols - 1];
}
else
{
vector<int> dp(rows);
//如果rows小 就把rows放在内层循环
for (int j = 0; j < cols; ++j)
{
for (int i = 0; i < rows; ++i)
{
//dp[i]相当于f(i, j - 1)
//dp[i - 1]相当于f(i - 1, j)
if (i == 0 && j == 0)
{
//起始位置
dp[i] = grid[i][j];
}
else if (j == 0)
{
//如果在第1列
//只和第1列前一行的数有关 即f(i - 1, j)
//即dp[i - 1]
dp[i] = dp[i - 1] + grid[i][j];
}
else if (i == 0)
{
//如果在第1行
//只和第一行前一列的数有关 即f(i,j - 1)
//即dp[i]
dp[i] = dp[i] + grid[i][j];
}
else
{
//否则 f(i - 1,j) f(i, j - 1)取小
dp[i] = min(dp[i - 1], dp[i]) + grid[i][j];
}
}
}
ret = dp[rows - 1];
}
return ret;
}
};