120. 三角形最小路径和 (triangle)

3,509 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第30天,点击查看活动详情

120. 三角形最小路径和 题目描述:给定一个三角形 triangletriangle,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。相邻的结点在这里指的是下标与上一层结点下标 相同 或者等于 上一层结点下标 +1+ 1 的两个结点。也就是说,如果正位于当前行的下标 ii,那么下一步可以移动到下一行的下标 iii+1i + 1

示例1示例2
输入triangle=[[2],[3,4],[6,5,7],[4,1,8,3]]triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出1111
解释:自顶向下的最小路径和为 1111(即,2 + 3 + 5 + 1 =112 + 3 + 5 + 1 = 11)。
输入triangle=[[10]]triangle = [[-10]]
输出10-10

中规中矩的动态规划

从容易理解的二维 dpdp 结构开始。

1、确定 dp 状态数组

定义 dp[i][j]dp[i][j] 为从 (0,0)(0,0) 位置走到 (i,j)(i,j) 位置的最小路径和,其中 0ji<n 0 \le j \le i < nn=triangle.lengthn = triangle.length

2、确定 dp 状态方程

根据题意推断出,只能从 (i1,j1)(i - 1, j - 1) 位置 或 (i1,j)(i - 1, j) 位置走到 (i,j)(i, j) 位置,因此 dp[i][j]dp[i][j] 的状态方程为,

dp[i][j]=min(dp[i1][j1],dp[i1][j])+triangle[i][j]dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j]

3、确定 dp 初始状态

dpdp 状态方程中涉及到 i1i - 1j1j - 1,因此需提前设置好边界值 dp[i][0]dp[i][0]dp[i][i]dp[i][i] 这些元素。

  • dp[i][0]dp[i][0] 代表从 (0,0)(0,0) 位置走到 (i,0)(i,0) 位置的最小路径和,即 dp[i][0]=dp[i1][0]+triangle[i][0]dp[i][0] = dp[i - 1][0] + triangle[i][0]

  • dp[i][i]dp[i][i] 代表从 (0,0)(0,0) 位置走到 (i,i)(i, i) 位置的最小路径和,即 dp[i][i]=dp[i1][i1]+triangle[i][i]dp[i][i] = dp[i-1][i-1] + triangle[i][i]

4、确定遍历顺序

如图所示,在计算 dp[i][0]dp[i][0] 时,已经将 2>3>6>42->3->6->4 这条路径计算完毕;而计算 dp[i][i]dp[i][i] 时,已经将 2>4>7>32->4->7->3 这条路径计算完毕,所以

  • 外层循环应从 i=2i = 2 遍历到 i=n1i = n - 1

  • 内层循环应从 j=1j = 1 遍历到 j=i1j = i - 1

内层循环为什么只需要遍历到 j=i1j = i - 1,因为当 i=2i = 2 时,只需要计算 (2,1)(2,1) 即可,i=3i = 3 时,只需要计算 (3,1)(3, 1)(3,2)(3, 2) 即可,以此类推。

5、确定最终返回值

三角形底边的所有数值,即 dp[n1]dp[n-1],仅仅是从 (0,0)(0,0) 位置到 dp[n1][j]dp[n - 1][j] 的最小路径值,所以需要统一比较 dp[n1]dp[n - 1] 数组的所有值,即

min(...dp[n1])min(...dp[n - 1])

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 状态数组

这次我们定 dp[i][j]dp[i][j] 为从 (i,j)(i,j) 位置到达 triangletriangle 数组最后一行的最小路径和,其中 0ji<n0 \le j \le i < nn=triangle.lenghtn = triangle.lenght

2、确定 dp 状态方法

(i,j)(i, j) 位置只能经过 (i+1,j+1)(i + 1, j + 1) 位置或 (i+1j)(i + 1, j) 位置,即,

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

3、确定 dp 初始状态

triangletriangle 数组的最后一行就是 dpdp 数组的初始状态,即

dp[n1]=triangle[n1]dp[n - 1] = triangle[n - 1]

NOTE: 我们这里完全不需要额外申请 n×nn \times n 的数组空间,直接复用 triangletriangle 数组,将其当成 dpdp 状态数组即可。

4、确定遍历顺序

  • 外层循环倒序遍历,从 i=n2i = n - 2 遍历到 i=0i = 0

  • 内层循环正序遍历,从 j=0j = 0 遍历到 j=ij = i

5、确定最终返回值

回归到状态定义中,dp[0][0]dp[0][0] 为从 (0,0)(0,0) 位置到达 triangletriangle 数组最后一行的最小路径和。

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];
};

参考

# 重识动态规划