LeetCode 第120题:三角形最小路径和

143 阅读9分钟

LeetCode 第120题:三角形最小路径和

题目描述

给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 ii + 1

难度

中等

题目链接

点击在LeetCode中查看题目

示例

示例 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 <= 200
  • triangle[0].length == 1
  • triangle[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],第一列和最后一列需要特殊处理

具体步骤:

  1. 初始化dp数组,大小为triangle的行数和列数
  2. 设置dp[0][0] = triangle[0][0]
  3. 对于每一行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]
  4. 返回最后一行中的最小值

时间复杂度: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]

具体步骤:

  1. 初始化dp数组,将最后一行的值复制到dp数组中
  2. 从倒数第二行开始,逐层向上计算: a. 对于每一行i和每一列j:dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]
  3. 返回dp[0][0]

时间复杂度:O(n²),其中n是三角形的行数 空间复杂度:O(n²),需要一个二维dp数组

方法三:空间优化的动态规划

我们可以发现,在方法二中,计算当前行的dp值时,只需要下一行的dp值。因此,我们可以使用一维数组来优化空间复杂度。

关键点:

  • 使用一维数组dp,dp[j]表示从位置(i,j)到底部的最小路径和
  • 从右到左更新dp数组,避免覆盖还未使用的值

具体步骤:

  1. 初始化dp数组,将最后一行的值复制到dp数组中
  2. 从倒数第二行开始,逐层向上计算: a. 对于每一行i,从右到左遍历每一列j:dp[j] = min(dp[j], dp[j+1]) + triangle[i][j]
  3. 返回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 ms39.8 MB代码结构清晰,但性能较慢
Python60 ms17.2 MB代码简洁,性能适中
C++4 ms8.5 MB执行速度最快,内存占用适中

代码亮点

  1. 🎯 方法三通过空间优化,将空间复杂度从O(n²)降低到O(n)
  2. 💡 自底向上的动态规划方法避免了边界条件的处理,代码更加简洁
  3. 🔍 正确处理了三角形的特殊结构,确保了状态转移的正确性
  4. 🎨 三种方法各有优势,可以根据具体需求选择合适的实现

常见错误分析

  1. 🚫 没有正确处理三角形的边界情况,如第一列和最后一列
  2. 🚫 在空间优化方法中,没有从右到左更新dp数组,导致覆盖了还未使用的值
  3. 🚫 忘记初始化dp数组,导致计算错误
  4. 🚫 在寻找最小路径和时,没有考虑所有可能的路径

解法对比

解法时间复杂度空间复杂度优点缺点
自顶向下O(n²)O(n²)直观易懂,符合题目描述需要处理边界情况,代码较复杂
自底向上O(n²)O(n²)代码简洁,无需处理边界情况与题目描述的方向相反,可能不够直观
空间优化O(n²)O(n)空间效率高,代码简洁需要注意更新顺序

相关题目