开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第30天,点击查看活动详情
120. 三角形最小路径和 题目描述:给定一个三角形 ,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。相邻的结点在这里指的是下标与上一层结点下标 相同 或者等于 上一层结点下标 的两个结点。也就是说,如果正位于当前行的下标 ,那么下一步可以移动到下一行的下标 或 。
| 示例1 | 示例2 |
|---|---|
| 输入: 输出: 解释:自顶向下的最小路径和为 (即,)。 | 输入: 输出: |
中规中矩的动态规划
从容易理解的二维 结构开始。
1、确定 dp 状态数组
定义 为从 位置走到 位置的最小路径和,其中 ,。
2、确定 dp 状态方程
根据题意推断出,只能从 位置 或 位置走到 位置,因此 的状态方程为,
3、确定 dp 初始状态
状态方程中涉及到 和 ,因此需提前设置好边界值 和 这些元素。
-
代表从 位置走到 位置的最小路径和,即
-
代表从 位置走到 位置的最小路径和,即
4、确定遍历顺序
如图所示,在计算 时,已经将 这条路径计算完毕;而计算 时,已经将 这条路径计算完毕,所以
-
外层循环应从 遍历到 ;
-
内层循环应从 遍历到 。
内层循环为什么只需要遍历到 ,因为当 时,只需要计算 即可, 时,只需要计算 和 即可,以此类推。
5、确定最终返回值
三角形底边的所有数值,即 ,仅仅是从 位置到 的最小路径值,所以需要统一比较 数组的所有值,即
6、代码示例
/**
* 空间复杂度 O(n^2)
* 时间复杂度 O(N^2)
*/
function minimumTotal(triangle: number[][]): number {
const n = triangle.length;
const dp = Array.from({ length: n }, () => new Array(n).fill(0));
dp[0][0] = triangle[0][0];
for (let i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0] + triangle[i][0];
dp[i][i] = dp[i - 1][i - 1] + triangle[i][i];
}
for (let i = 2; i < n; i++) {
for (let j = 1; j < i; j++) {
dp[i][j] = Math.min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j]
}
}
return Math.min(...dp[n - 1]);
};
逆向思维的动态规划
“中规中矩的动态规划”中需要考虑的边界逻辑太多,很容易遗漏,而且空间复杂度比较高。我们在“逆向思维”中探寻一种更为理想的求解方法。
1、确定 dp 状态数组
这次我们定 为从 位置到达 数组最后一行的最小路径和,其中 ,。
2、确定 dp 状态方法
从 位置只能经过 位置或 位置,即,
3、确定 dp 初始状态
数组的最后一行就是 数组的初始状态,即
NOTE: 我们这里完全不需要额外申请 的数组空间,直接复用 数组,将其当成 状态数组即可。
4、确定遍历顺序
-
外层循环倒序遍历,从 遍历到 ;
-
内层循环正序遍历,从 遍历到 。
5、确定最终返回值
回归到状态定义中, 为从 位置到达 数组最后一行的最小路径和。
6、代码示例
/**
* 空间复杂度 O(1)
* 时间复杂度 O(n^2)
*/
function minimumTotal(triangle: number[][]): number {
const n = triangle.length;
const dp = triangle;
for (let i = n - 2; i >= 0; i--) {
for (let j = 0; j <= i; j++) {
dp[i][j] += Math.min(dp[i + 1][j + 1], dp[i + 1][j])
}
}
return dp[0][0];
};