0%

三角形最小路径和


题目描述

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。相邻的结点,在这里指的是下标与上一层结点下标相同或者等于上一层结点下标 + 1 的两个结点。

例如,给定三角形:

1
2
3
4
5
6
[
[2],
[3, 4],
[6, 5, 7],
[4, 1, 8, 3]
]

自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

递归

给定的三角形的行数为 n,并且第 i 行(从 0 开始编号)包含了 i + 1 个数。如果将每一行的左端对齐,那么会形成一个等腰直角三角形,如下所示:

1
2
3
4
[2],
[3, 4],
[6, 5, 7],
[4, 1, 8, 3]

这样,我们可以从顶点自上而下递归处理最小路径和,首先从顶点开始,他的最小路径和是顶点的数2加上下面的以3为顶点的最短路径和,或者顶点2加上下面以4为顶点的最短路径和,取其中的最小值,并不断递归处理。这里需要注意的一点是,对于第二行的顶点3和4,计算他们下一层的最短路径和的时候,会计算两遍第三行的以5为顶点的三角形最短路径和。所以我们在递归的同时,记录下计算过的路径和,防止重复计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public int minimumTotal(List<List<Integer>> triangle) {
if (triangle == null || triangle.isEmpty() || triangle.get(0).isEmpty()) {
return 0;
}
// 用来保存某个顶点计算过的最短路径和
int[][] minValue = new int[triangle.size() + 1][triangle.size() + 1];
for (int i = 0; i < minValue.length; i++) {
Arrays.fill(minValue[i], Integer.MAX_VALUE);
}
int value = triangle.get(0).get(0);
int leftValue = value + minimumTotal(triangle, 1, 0, minValue);
int rightValue = value + minimumTotal(triangle, 1, 1, minValue);
return Math.min(leftValue, rightValue);
}

public int minimumTotal(List<List<Integer>> triangle, int row, int column, int[][] minValue) {
if (row >= triangle.size() || column >= triangle.get(row).size()) {
return 0;
}
int value = triangle.get(row).get(column);
int leftValue = value + (minValue[row + 1][column] < Integer.MAX_VALUE ? minValue[row + 1][column]
: minimumTotal(triangle, row + 1, column, minValue));
int rightValue = value + (minValue[row + 1][column + 1] < Integer.MAX_VALUE ? minValue[row + 1][column + 1]
: minimumTotal(triangle, row + 1, column + 1, minValue));
return minValue[row][column] = Math.min(leftValue, rightValue);
}

复杂度分析

  • 时间复杂度:O(n²),其中 n 是三角形的行数。
  • 空间复杂度:O(n²),我们需要一个 n ∗ n 的二维数组存放记录的值。

动态规划-自顶向下

我们用 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 = 0j = 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,就可以完成所有状态的计算。

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[][] dp = new int[n][n];
dp[0][0] = triangle.get(0).get(0);
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0] + triangle.get(i).get(0);
for (int j = 1; j < i; j++) {
dp[i][j] = Math.min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle.get(i).get(j);
}
dp[i][i] = dp[i - 1][i - 1] + triangle.get(i).get(i);
}
int minTotal = dp[n - 1][0];
for (int i = 1; i < n; ++i) {
minTotal = Math.min(minTotal, dp[n - 1][i]);
}
return minTotal;
}

复杂度分析

  • 时间复杂度:O(n²),其中 n 是三角形的行数。
  • 空间复杂度:O(n²),我们需要一个 n ∗ n 的二维数组存放所有的状态。

动态规划-自底向上

跟上述的方法一致,只不过我们采用自底向上的方法,状态转移方程为:

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

1
2
3
4
5
6
7
8
9
10
11
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
// 多申请1空间防止越界
int[][] dp = new int[n + 1][n + 1];
for (int i = n - 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];
}

复杂度分析

  • 时间复杂度:O(n²),其中 n 是三角形的行数。
  • 空间复杂度:O(n²),我们需要一个 n ∗ n 的二维数组存放所有的状态。

动态规划-空间优化

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

1
2
3
4
5
6
7
8
9
10
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[] dp = new int[n + 1];
for (int i = n - 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];
}

复杂度分析

  • 时间复杂度:O(n²),其中 n 是三角形的行数。
  • 空间复杂度:O(n),我们需要一个 n 的一维数组存放所有的状态。

来源

三角形最小路径和 | 力扣(LeetCode)
三角形最小路径和 | 题解(LeetCode)


分享精彩,留下足迹

欢迎关注我的其它发布渠道