LeetCode 第120题:三角形最小路径和
题目描述
给定一个三角形 triangle ,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
难度
中等
题目链接
示例
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
示例 2:
输入:triangle = [[-10]]
输出:-10
提示
1 <= triangle.length <= 200triangle[0].length == 1triangle[i].length == triangle[i - 1].length + 1-10^4 <= triangle[i][j] <= 10^4
解题思路
方法一:动态规划(自顶向下)
这是一个典型的动态规划问题。我们可以定义状态dp[i][j]表示从三角形顶部到位置(i,j)的最小路径和。
关键点:
- 状态定义:dp[i][j]表示从三角形顶部到位置(i,j)的最小路径和
- 状态转移方程:dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
- 边界条件:dp[0][0] = triangle[0][0],第一列和最后一列需要特殊处理
具体步骤:
- 初始化dp数组,大小为triangle的行数和列数
- 设置dp[0][0] = triangle[0][0]
- 对于每一行i(从1开始): a. 对于第一列(j=0):dp[i][0] = dp[i-1][0] + triangle[i][0] b. 对于最后一列(j=i):dp[i][i] = dp[i-1][i-1] + triangle[i][i] c. 对于中间列:dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
- 返回最后一行中的最小值
时间复杂度:O(n²),其中n是三角形的行数,需要计算所有位置的最小路径和 空间复杂度:O(n²),需要一个二维dp数组
方法二:动态规划(自底向上)
我们也可以从三角形的底部开始,逐层向上计算最小路径和。这种方法更加简洁,不需要处理边界情况。
关键点:
- 状态定义:dp[i][j]表示从位置(i,j)到底部的最小路径和
- 状态转移方程:dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]
- 最终结果:dp[0][0]
具体步骤:
- 初始化dp数组,将最后一行的值复制到dp数组中
- 从倒数第二行开始,逐层向上计算: a. 对于每一行i和每一列j:dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]
- 返回dp[0][0]
时间复杂度:O(n²),其中n是三角形的行数 空间复杂度:O(n²),需要一个二维dp数组
方法三:空间优化的动态规划
我们可以发现,在方法二中,计算当前行的dp值时,只需要下一行的dp值。因此,我们可以使用一维数组来优化空间复杂度。
关键点:
- 使用一维数组dp,dp[j]表示从位置(i,j)到底部的最小路径和
- 从右到左更新dp数组,避免覆盖还未使用的值
具体步骤:
- 初始化dp数组,将最后一行的值复制到dp数组中
- 从倒数第二行开始,逐层向上计算: a. 对于每一行i,从右到左遍历每一列j:dp[j] = min(dp[j], dp[j+1]) + triangle[i][j]
- 返回dp[0]
时间复杂度:O(n²),其中n是三角形的行数 空间复杂度:O(n),只需要一个一维dp数组
图解思路
方法二:自底向上的动态规划过程
以示例1为例:
2
3 4
6 5 7
4 1 8 3
| 行 | dp数组 | 计算过程 |
|---|---|---|
| 初始化 | [4,1,8,3] | 最后一行的值 |
| i=2 | [4,1,8,3] -> [7,6,10,3] | dp[2][0] = min(dp[3][0], dp[3][1]) + triangle[2][0] = min(4, 1) + 6 = 7 |
| dp[2][1] = min(dp[3][1], dp[3][2]) + triangle[2][1] = min(1, 8) + 5 = 6 | ||
| dp[2][2] = min(dp[3][2], dp[3][3]) + triangle[2][2] = min(8, 3) + 7 = 10 | ||
| i=1 | [7,6,10,3] -> [9,10,10,3] | dp[1][0] = min(dp[2][0], dp[2][1]) + triangle[1][0] = min(7, 6) + 3 = 9 |
| dp[1][1] = min(dp[2][1], dp[2][2]) + triangle[1][1] = min(6, 10) + 4 = 10 | ||
| i=0 | [9,10,10,3] -> [11,10,10,3] | dp[0][0] = min(dp[1][0], dp[1][1]) + triangle[0][0] = min(9, 10) + 2 = 11 |
最终结果:dp[0][0] = 11
方法三:空间优化的动态规划过程
以示例1为例:
| 行 | dp数组 | 计算过程 |
|---|---|---|
| 初始化 | [4,1,8,3] | 最后一行的值 |
| i=2, j=2 | [4,1,8,3] -> [4,1,10,3] | dp[2] = min(dp[2], dp[3]) + triangle[2][2] = min(8, 3) + 7 = 10 |
| i=2, j=1 | [4,1,10,3] -> [4,6,10,3] | dp[1] = min(dp[1], dp[2]) + triangle[2][1] = min(1, 10) + 5 = 6 |
| i=2, j=0 | [4,6,10,3] -> [7,6,10,3] | dp[0] = min(dp[0], dp[1]) + triangle[2][0] = min(4, 6) + 6 = 10 |
| i=1, j=1 | [7,6,10,3] -> [7,10,10,3] | dp[1] = min(dp[1], dp[2]) + triangle[1][1] = min(6, 10) + 4 = 10 |
| i=1, j=0 | [7,10,10,3] -> [9,10,10,3] | dp[0] = min(dp[0], dp[1]) + triangle[1][0] = min(7, 10) + 3 = 10 |
| i=0, j=0 | [9,10,10,3] -> [11,10,10,3] | dp[0] = min(dp[0], dp[1]) + triangle[0][0] = min(9, 10) + 2 = 11 |
最终结果:dp[0] = 11
代码实现
C# 实现
public class Solution {
// 方法一:动态规划(自顶向下)
public int MinimumTotal(IList<IList<int>> triangle) {
int n = triangle.Count;
int[][] dp = new int[n][];
for (int i = 0; i < n; i++) {
dp[i] = new int[i + 1];
}
dp[0][0] = triangle[0][0];
for (int i = 1; i < n; i++) {
// 第一列
dp[i][0] = dp[i - 1][0] + triangle[i][0];
// 中间列
for (int j = 1; j < i; j++) {
dp[i][j] = Math.Min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j];
}
// 最后一列
dp[i][i] = dp[i - 1][i - 1] + triangle[i][i];
}
// 找出最后一行中的最小值
int minTotal = dp[n - 1][0];
for (int j = 1; j < n; j++) {
minTotal = Math.Min(minTotal, dp[n - 1][j]);
}
return minTotal;
}
// 方法二:动态规划(自底向上)
public int MinimumTotalBottomUp(IList<IList<int>> triangle) {
int n = triangle.Count;
int[][] dp = new int[n][];
for (int i = 0; i < n; i++) {
dp[i] = new int[n];
}
// 初始化最后一行
for (int j = 0; j < n; j++) {
dp[n - 1][j] = triangle[n - 1][j];
}
// 自底向上计算
for (int i = n - 2; 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[i][j];
}
}
return dp[0][0];
}
// 方法三:空间优化的动态规划
public int MinimumTotalOptimized(IList<IList<int>> triangle) {
int n = triangle.Count;
int[] dp = new int[n];
// 初始化最后一行
for (int j = 0; j < n; j++) {
dp[j] = triangle[n - 1][j];
}
// 自底向上计算
for (int i = n - 2; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
dp[j] = Math.Min(dp[j], dp[j + 1]) + triangle[i][j];
}
}
return dp[0];
}
}
Python 实现
class Solution:
# 方法一:动态规划(自顶向下)
def minimumTotal(self, triangle: List[List[int]]) -> int:
n = len(triangle)
dp = [[0] * (i + 1) for i in range(n)]
dp[0][0] = triangle[0][0]
for i in range(1, n):
# 第一列
dp[i][0] = dp[i - 1][0] + triangle[i][0]
# 中间列
for j in range(1, i):
dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j]
# 最后一列
dp[i][i] = dp[i - 1][i - 1] + triangle[i][i]
# 找出最后一行中的最小值
return min(dp[n - 1])
# 方法二:动态规划(自底向上)
def minimumTotalBottomUp(self, triangle: List[List[int]]) -> int:
n = len(triangle)
dp = [[0] * n for _ in range(n)]
# 初始化最后一行
for j in range(n):
dp[n - 1][j] = triangle[n - 1][j]
# 自底向上计算
for i in range(n - 2, -1, -1):
for j in range(i + 1):
dp[i][j] = min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j]
return dp[0][0]
# 方法三:空间优化的动态规划
def minimumTotalOptimized(self, triangle: List[List[int]]) -> int:
n = len(triangle)
dp = triangle[n - 1].copy()
# 自底向上计算
for i in range(n - 2, -1, -1):
for j in range(i + 1):
dp[j] = min(dp[j], dp[j + 1]) + triangle[i][j]
return dp[0]
C++ 实现
class Solution {
public:
// 方法一:动态规划(自顶向下)
int minimumTotal(vector<vector<int>>& triangle) {
int n = triangle.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
dp[0][0] = triangle[0][0];
for (int i = 1; i < n; i++) {
// 第一列
dp[i][0] = dp[i - 1][0] + triangle[i][0];
// 中间列
for (int j = 1; j < i; j++) {
dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j];
}
// 最后一列
dp[i][i] = dp[i - 1][i - 1] + triangle[i][i];
}
// 找出最后一行中的最小值
int minTotal = dp[n - 1][0];
for (int j = 1; j < n; j++) {
minTotal = min(minTotal, dp[n - 1][j]);
}
return minTotal;
}
// 方法二:动态规划(自底向上)
int minimumTotalBottomUp(vector<vector<int>>& triangle) {
int n = triangle.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
// 初始化最后一行
for (int j = 0; j < n; j++) {
dp[n - 1][j] = triangle[n - 1][j];
}
// 自底向上计算
for (int i = n - 2; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
dp[i][j] = min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j];
}
}
return dp[0][0];
}
// 方法三:空间优化的动态规划
int minimumTotalOptimized(vector<vector<int>>& triangle) {
int n = triangle.size();
vector<int> dp(n, 0);
// 初始化最后一行
for (int j = 0; j < n; j++) {
dp[j] = triangle[n - 1][j];
}
// 自底向上计算
for (int i = n - 2; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
dp[j] = min(dp[j], dp[j + 1]) + triangle[i][j];
}
}
return dp[0];
}
};
执行结果
C# 实现
- 执行用时:104 ms
- 内存消耗:39.8 MB
Python 实现
- 执行用时:60 ms
- 内存消耗:17.2 MB
C++ 实现
- 执行用时:4 ms
- 内存消耗:8.5 MB
性能对比
| 语言 | 执行用时 | 内存消耗 | 特点 |
|---|---|---|---|
| C# | 104 ms | 39.8 MB | 代码结构清晰,但性能较慢 |
| Python | 60 ms | 17.2 MB | 代码简洁,性能适中 |
| C++ | 4 ms | 8.5 MB | 执行速度最快,内存占用适中 |
代码亮点
- 🎯 方法三通过空间优化,将空间复杂度从O(n²)降低到O(n)
- 💡 自底向上的动态规划方法避免了边界条件的处理,代码更加简洁
- 🔍 正确处理了三角形的特殊结构,确保了状态转移的正确性
- 🎨 三种方法各有优势,可以根据具体需求选择合适的实现
常见错误分析
- 🚫 没有正确处理三角形的边界情况,如第一列和最后一列
- 🚫 在空间优化方法中,没有从右到左更新dp数组,导致覆盖了还未使用的值
- 🚫 忘记初始化dp数组,导致计算错误
- 🚫 在寻找最小路径和时,没有考虑所有可能的路径
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 自顶向下 | O(n²) | O(n²) | 直观易懂,符合题目描述 | 需要处理边界情况,代码较复杂 |
| 自底向上 | O(n²) | O(n²) | 代码简洁,无需处理边界情况 | 与题目描述的方向相反,可能不够直观 |
| 空间优化 | O(n²) | O(n) | 空间效率高,代码简洁 | 需要注意更新顺序 |
相关题目
- LeetCode 64. 最小路径和 - 中等
- LeetCode 931. 下降路径最小和 - 中等
- LeetCode 1289. 下降路径最小和 II - 困难