动态规划_leetcode.120.三角形最小路径和

🌸题目

🍁给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。

1
2
3
4
5
6
7
8
9
例如,给定三角形:

[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

说明:

如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。

🌸分析

本题是一道非常经典且历史悠久的动态规划题,其作为算法题出现,最早可以追溯到 1994 年的 IOI(国际信息学奥林匹克竞赛)的 The Triangle。时光飞逝,经过 20 多年的沉淀,往日的国际竞赛题如今已经变成了动态规划的入门必做题,不断督促着我们学习和巩固算法。

这道题当我看到自顶向下四个字就知道是动态规划类题目,然而首先读完题在脑海里的却是深度优先DFS(暴力搜索解法),若定义 f(i, j)(i, j)点到底边的最小路径和,则易知递归求解式为:

f(i, j) = min(f(i + 1, j), f(i + 1, j + 1)) + triangle [ i ] [ j ]

由此,我们将任一点到底边的最小路径和,转化成了与该点相邻两点到底边的最小路径和中的较小值,再加上该点本身的值。这样本题的 递归解法(解法一) 就完成了。最终超时,果然还是得动态规划呀

解法一:DFS递归/暴力解法

暴力求解,不解释。

暴力搜索会有大量的重复计算,导致 超时,因此在 解法二 中结合记忆化数组进行优化。

1
2
3
4
5
6
7
8
9
10
11
public int minimumTotal(List<List<Integer>> triangle) {
return dfs(triangle, 0, 0);
}
private int dfs(List<List<Integer>> triangle, int rows, int cols) {
// TODO Auto-generated method stub
if(rows == triangle.size()) {
return 0;
}

return Math.min(dfs(triangle, rows + 1, cols), dfs(triangle, rows + 1, cols + 1)) + triangle.get(rows).get(cols);
}

解法二:递归+记忆化

在解法一的基础上,定义了二维数组进行记忆化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Integer[][] memory;
public int minimumTotal2(List<List<Integer>> triangle) {
memory = new Integer[triangle.size()][triangle.size()];

return dfs2(triangle, 0, 0);
}
private int dfs2(List<List<Integer>> triangle, int rows, int cols) {
// TODO Auto-generated method stub
if(rows == triangle.size()) {
return 0;
}
if(memory[rows][cols] != null) {
return memory[rows][cols];
}

return memory[rows][cols] = Math.min(dfs2(triangle, rows + 1, cols), dfs2(triangle, rows + 1, cols + 1)) + triangle.get(rows).get(cols);
}

解法三: 自顶向下的动态规划

分析

我们用f[i] [j]示从三形顶部走到位置(i, j)的最小路径和。这里的位置(i,j)指的是三角形中第 i行第 j列(均从0开始编号)的位置。于每-织能移动到下一行「相邻的节点,上, 因此要想走到位置(i,j),上一步就只能在位置(i- 1,j- 1)或者位置(i-1,j)。我们在这两个位置中选择一个路径和较小的来进行转移, 状态转移方程为:
f[i] [j]= min(f[i- 1] [j- 1],f[i- 1] [j]) + c[i] [j]
c[i] [j]示位置(i, j)对应的元素值。注意第i行有i+ 1个元素,它们对应的j的范围为[0,i]。当 j=0或j= i时, .上述状态转移方程中有一 些项是没有意义的。例如当j = 0时,f[i- 1] [j- 1]没有意义,因此状态转移方程为:f[i] [0]= f[i- 1] [0]+c[i] [0]即当我们在第i行的最左侧时,我们只能从第i - 1 行的最左侧移动过来。当j=i, f[i- 1][j]没有意义,因此状态转移方程为:

f[i] [i]= f[i-1] [i- 1] + c[i] [i]
即当我们在第i行的最右侧时,我们只能从第i - 1行的最右侧移动过来。
最终的答案即为f[n- 1] [0]f[n- 1 ] [n- 1]中的最大值,n .是三角形的行数。

细节

状态转移方程的边界条件是什么?于我们已经却除了所有没有意义」
的状态,因此边界条件可以定为:
f[0][0] = c[0][0]
即在三角形的顶部时,最小路径和就等于对应位置的元素值。这样一来,
我们从1开始递增地枚举i,并在[0, i]的范围内递增地枚举j,就可以
完成所有状态的计算。

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[][] f = new int[n][n];
f[0][0] = triangle.get(0).get(0);
for (int i = 1; i < n; ++i) {
f[i][0] = f[i - 1][0] + triangle.get(i).get(0);
for (int j = 1; j < i; ++j) {
f[i][j] = Math.min(f[i - 1][j - 1], f[i - 1][j]) + triangle.get(i).get(j);
}
f[i][i] = f[i - 1][i - 1] + triangle.get(i).get(i);
}
int minTotal = f[n - 1][0];
for (int i = 1; i < n; ++i) {
minTotal = Math.min(minTotal, f[n - 1][i]);
}
return minTotal;
}

python

1
2
3
4
5
6
7
8
9
10
11
12
def minimumTotal(self, triangle: List[List[int]]) -> int:
n = len(triangle)
f = [[0] * n for _ in range(n)]
f[0][0] = triangle[0][0]

for i in range(1, n):
f[i][0] = f[i - 1][0] + triangle[i][0]
for j in range(1, i):
f[i][j] = min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j]
f[i][i] = f[i - 1][i - 1] + triangle[i][i]

return min(f[n - 1])

解法四: 自顶向下的空间优化

我们从i到0递减地枚举j,这样我们只要一个长度为n的一维数组f,就可以完成状态转移。

为什么只有在递减地枚举j时,才能省去一个一维数组?当我们在计算位置(i,j)时, f[j+ 1]f[i]是第i行的值,而f[0]f[i]仍然是第i一1行的值。此时我们直接通过

  • f[j] = min(f[j - 1],f[j])+ c[i[j]

进行转移,恰好就是在(i - 1,j - 1)(i- 1,j)中进行选择。但如果我们递增地枚举j,那么在计算位置(i,j)时,f(0)f[j - 1]经是第i行的值。如果我]仍然使用上述状态转移方程,那么是在(i,j-1)(i- 1,i)中进行选择,就产生了错误。

这样虽然空间复杂度仍然为0(n),但我们只使用了n的空间存储状态,
减少了一半的空间消耗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[] f = new int[n];
f[0] = triangle.get(0).get(0);
for (int i = 1; i < n; ++i) {
f[i] = f[i - 1] + triangle.get(i).get(i);
for (int j = i - 1; j > 0; --j) {
f[j] = Math.min(f[j - 1], f[j]) + triangle.get(i).get(j);
}
f[0] += triangle.get(i).get(0);
}
int minTotal = f[0];
for (int i = 1; i < n; ++i) {
minTotal = Math.min(minTotal, f[i]);
}
return minTotal;
}

python

1
2
3
4
5
6
7
8
9
10
11
12
def minimumTotal(self, triangle: List[List[int]]) -> int:
n = len(triangle)
f = [0] * n
f[0] = triangle[0][0]

for i in range(1, n):
f[i] = f[i - 1] + triangle[i][i]
for j in range(i - 1, 0, -1):
f[j] = min(f[j - 1], f[j]) + triangle[i][j]
f[0] += triangle[i][0]

return min(f)

解法五:自底向上的动态规划

分析

定义二维 dp 数组,将解法二中「自顶向下的递归」改为「自底向上的递推」。

dp[i][j]表示从点 (i, j)到底边的最小路径和。

状态转移:
dp[i][j] = min(dp[i+ 1][i],dp[i + 1][j + 1]) + triangle[i][j][][j]

java

1
2
3
4
5
6
7
8
9
10
11
12
public int minimumTotal3(List<List<Integer>> triangle) {
int len = triangle.size();
//dp[i][j]表示从点(i,j)到底边的最小路径和
int[][] dp = new int[len + 1][len + 1];
//从三角形的最后一行开始递推
for(int i = len - 1; i >= 0; i--) {
for(int j = 0; j <= i; j++) {
dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle.get(i).get(j);
}
}
return dp[0][0];
}

解法五:自底向上的动态规划+空间优化

分析

在上述代码中,我们定义了-一个N行N列的dp数组(N三角形的行数)
但是在实际递推中我们发现,计算dp[i][j]时,只用到了下一行的dp[i+ 1][j]和dp[i+ 1][j+ 1]
因此dp数组不需要定义N行,只要定义1行就阔以。
所以我们稍微修改一下上述代码,将i所在的维度去掉(如下),就可以
将O(N^2^)的空间复杂度优化成O(N)

java

1
2
3
4
5
6
7
8
9
10
11
12
public int minimumTotal4(List<List<Integer>> triangle) {
int len = triangle.size();

int[] dp = new int[len + 1];
//从三角形的最后一行开始递推
for(int i = len - 1; i >= 0; i--) {
for(int j = 0; j <= i; j++) {
dp[j] = Math.min(dp[j], dp[j + 1]) + triangle.get(i).get(j);
}
}
return dp[0];
}

最后,不经历风雨,怎能在计算机的大山之顶看见彩虹呢! 无论怎样,相信明天一定会更好!!!!!

-------------本文结束感谢您的阅读-------------
云澈 wechat
扫一扫,用手机访问哦
坚持原创技术分享,您的支持将鼓励我继续创作!
0%