🌸题目
🍁给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。
1 | 例如,给定三角形: |
说明:
如果你可以只使用 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 | public int minimumTotal(List<List<Integer>> triangle) { |
解法二:递归+记忆化
在解法一的基础上,定义了二维数组进行记忆化。
1 | Integer[][] memory; |
解法三: 自顶向下的动态规划
分析
我们用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 | public int minimumTotal(List<List<Integer>> triangle) { |
python
1 | def minimumTotal(self, triangle: List[List[int]]) -> int: |
解法四: 自顶向下的空间优化
我们从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 | public int minimumTotal(List<List<Integer>> triangle) { |
python
1 | def minimumTotal(self, triangle: List[List[int]]) -> int: |
解法五:自底向上的动态规划
分析
定义二维 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 | public int minimumTotal3(List<List<Integer>> triangle) { |
解法五:自底向上的动态规划+空间优化
分析
在上述代码中,我们定义了-一个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 | public int minimumTotal4(List<List<Integer>> triangle) { |