三角形最小路径和

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。

例如,给定三角形:

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

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

说明:
如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/triangle
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路一:动态规划

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

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/triangle/solution/san-jiao-xing-zui-xiao-lu-jing-he-by-leetcode-solu/
来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

代码实现一:

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n = triangle.size();
        vector<vector<int>> f(n, vector<int>(n));
        f[0][0] = triangle[0][0];
        for (int i = 1; i < n; ++i) {
            f[i][0] = f[i - 1][0] + triangle[i][0];
            for (int j = 1; j < i; ++j) {
                f[i][j] = min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j];
            }
            f[i][i] = f[i - 1][i - 1] + triangle[i][i];
        }
        return *min_element(f[n - 1].begin(), f[n - 1].end());
    }
};


> 作者:LeetCode-Solution
> 链接:https://leetcode-cn.com/problems/triangle/solution/san-jiao-xing-zui-xiao-lu-jing-he-by-leetcode-solu/
> 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

复杂度分析

时间复杂度:O(n^2),其中 n 是三角形的行数。

空间复杂度:O(n^2)。我们需要一个 n∗n 的二维数组存放所有的状态。

思路二:动态规划 + 空间优化

在题目描述中的「说明」部分,提到了可以将空间复杂度优化至 O(n)O(n)O(n)。

我们回顾方法一中的状态转移方程,可以发现,f[i][j] 只与 f[i−1][…] 有关,而与 f[i−2][…] 及之前的状态无关,因此我们不必存储这些无关的状态。具体地,我们使用两个长度为 n 的一维数组进行转移,将 i 根据奇偶性映射到其中一个一维数组,那么 i−1 就映射到了另一个一维数组。这样我们使用这两个一维数组,交替地进行状态转移。

代码实现

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n = triangle.size();
        vector<vector<int>> f(2, vector<int>(n));
        f[0][0] = triangle[0][0];
        for (int i = 1; i < n; ++i) {
            int curr = i % 2;
            int prev = 1 - curr;
            f[curr][0] = f[prev][0] + triangle[i][0];
            for (int j = 1; j < i; ++j) {
                f[curr][j] = min(f[prev][j - 1], f[prev][j]) + triangle[i][j];
            }
            f[curr][i] = f[prev][i - 1] + triangle[i][i];
        }
        return *min_element(f[(n - 1) % 2].begin(), f[(n - 1) % 2].end());
    }
};


> 作者:LeetCode-Solution
> 链接:https://leetcode-cn.com/problems/triangle/solution/san-jiao-xing-zui-xiao-lu-jing-he-by-leetcode-solu/
> 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

上述方法的空间复杂度为O(n),使用了2n 的空间存储状态。我们还可以继续进行优化吗?

答案是可以的。我们从 i到 0 递减地枚举 j,这样我们只需要一个长度为 n 的一维数组 f,就可以完成状态转移。

这样虽然空间复杂度仍然为 O(n),但我们只使用了 n 的空间存储状态,减少了一半的空间消耗。

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n = triangle.size();
        vector<int> f(n);
        f[0] = triangle[0][0];
        for (int i = 1; i < n; ++i) {
            f[i] = f[i - 1] + triangle[i][i];
            for (int j = i - 1; j > 0; --j) {
                f[j] = min(f[j - 1], f[j]) + triangle[i][j];
            }
            f[0] += triangle[i][0];
        }
        return *min_element(f.begin(), f.end());
    }
};


> 作者:LeetCode-Solution
> 链接:https://leetcode-cn.com/problems/triangle/solution/san-jiao-xing-zui-xiao-lu-jing-he-by-leetcode-solu/
> 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

复杂度分析

时间复杂度:O(n^2),其中 n 是三角形的行数。

空间复杂度:O(n)。