三角形最小路径和

题目链接

120. 三角形最小路径和

解题过程

我觉得这道题已经可以当作如何优化动态规划的经典例题了,首先从路径上采用的是从上到下的方式,我是用了我自己能想出来的方法,也就是二维数组去解决的,接下来进行优化,将O(n2)的空间复杂度降低为O(n),使用 2n 的空间存储状态,接下来再次进行优化,保持O(n2)的空间复杂度不变,但是只使用 n 的空间存储状态。最后又采取了从下到上的策略。一共用了4种方法。

方法1:二维数组做法

通读题目,稍加思考,我们不难得出以下公式,其中 f[i][j] 的意义为从起点到i行的j点的最小路径和,因为我们想求得就是这个,所以想出这个意义应该不算难吧。

在这里插入图片描述
既然已经得到了转移公式,下面要做得就是代码实现了,第一次看这个代码可能不太好理解得就是外层循环和内层循环之间得那两个赋值语句,注意看上面的公式,otherwise是普遍情况,但是上面有两种特殊情况,上面的赋值语句就是先处理了以下 j == 0 的情况,下面的赋值语句就是处理了 j == i 的情况,其实 j == i 就是到了一行的最后那个元素,而中间处理的就是otherwise的普遍情况。

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)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

方法2:O(n)的空间复杂度 + 2n 的空间存储状态

首先解释一下什么叫

O(n)的空间复杂度 + 2n 的空间存储状态

O(n)的空间复杂度:简单地说,二维数组的空间复杂度就是O(n^2),一维数组就是O(n)
2n 的空间存储状态: 同样简单地说,用一个一维数组就是n,用两个就是2n了
也就是说我们需要把方法1的dp数组从二维改为一维,并且需要用到两个一维数组。但是要注意的是我现在的分析思路是在我已经知道这种方法该怎么做了的前提下,但是真正做题的时候谁会从答案开始分析,如果真能那样那可是太厉害了,所以我们还是从头开始分析。

优化要从哪里找到突破口?

答案是:在这里插入图片描述
为什么说是这个,通过仔细观察可以发现,我们在得到当前的答案时,只使用到了上一个状态的信息,体现在等号后面f的第一维度全部是(i - 1),也就是说 i - 2 时的数据是多少完全不重要,所以我们就没有必要留着这个数据,所以这不就是通过变量迭代的方式不断更新答案吗(这个思想在求斐波那契数列的时候使用到过),但是上一个状态不像求斐波那契数列时就一个数字,在本题中上一个状态会保存多个数据,就是同一行的不同点保存的数据是不同的,所以需要一个数组来保存。

我们需要做的就是用一个数组保存上一行的信息,另一个数组保存下一行的信息,一次循环即可更新一行的答案,我们使用两个数组不停迭代即可。

在下面的代码中,front保存上一行信息,behind保存上一行信息。

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        if(triangle.empty() || triangle[0].empty()) return 0;
        int front[500], behind[500];
        int row = triangle.size();
        front[0] = triangle[0][0];
        for(int i = 1; i < row; ++i)
        {
            for(int j = 0; j < triangle[i].size(); ++j)
            {
                if(j == 0) behind[j] = front[j] + triangle[i][j];
                else if(j == i) behind[j] = front[j - 1] + triangle[i][j];
                else behind[j] = min(front[j - 1], front[j]) + triangle[i][j];
            }
            for(int j = 0; j < triangle[i].size(); ++j) front[j] = behind[j];
        }
        int minn = 1e9 + 7;
        for(int i = 0; i < triangle[row - 1].size(); ++i)
            minn = min(minn, front[i]);
        return minn;
    }
};

方法3:O(n)的空间复杂度 + n 的空间存储状态

这种优化策略,可谓是真的巧妙,我是通过下面这句话理解的:

对于同一行的点,后一个点信息的更新需要利用前面的点的数据,后一个点信息的改变不会影响到前面点信息更改结果的正确性

话说的挺绕的,但是一旦明白这种优化策略,再读这句话就会感觉到豁然开朗

怎么继续优化?

看下面这个图,虽然有点丑,但能说明问题。

10号点答案的更新会用到6号点,9号点答案的更新会用到6号点和5号点,8号点答案的更新会用到5号点和4号点,7号点答案的更新会用到4号点。

假设我现在有一个数组f[n],f[0] 存到4号点的最短路径长度,f[1]存储到5号点的,f[2]存储到6号点的,现在问能不能仅仅通过这个数组来获得下一行的答案呢?

为了更加方便理解,我们先不考虑边界,只看8,9号点,按照之前的方法,对于9号点来说,现在的更新策略就是从5和6号中找到最小的,然后加上自己,那在现在的情况下,就是min(f[1],f[2])+ 9号点的值,为什么是这样呢?不懂的话看一下f[1]和f[2]的意义是什么。这样做完后,如果按照456那行,存储9号点答案的应该是f[2],那假设我们真就把答案放在f[2]里面,那接下来求8号点的答案会不会受到影响?答案是不会,因为8号点会用到f[0]和f[1],并没用到f[2]。同样,我们可以把8号点答案放在f[1]里面。

回过头来看一下,我们居然只用了一个f数组就成功获得了8号,9号点的数据,其实这就是这个优化策略的核心所在,在一行中,如果我们采用从后向前遍历来更新答案的方式,就可以只使用一个数组就可以完成从上一行到下一行的转化。这么做的道理是什么呢?就是开头我说的那句话,9号点导致f[2]的改变并不会影响到8号点的求解,而且在考虑8号,9号点那行的时候,f数组保存的恰好是上一行的数据,恰恰是我们需要的,我们没有必要再拿一个数组保存信息,在原来的数组上进行修改即可。
在这里插入图片描述

我说的有些乱,在看代码的同时来理解会好一些。同样的,在内外层循环中间的那两个单独的赋值语句就是为了处理开头和结尾的特殊情况。

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        if(triangle.empty() || triangle[0].empty()) return 0;
        int f[500];
        int row = triangle.size();
        f[0] = triangle[0][0];
        for(int i = 1; i < row; ++i)
        {
            f[i] = f[i - 1] + triangle[i][i];
            for(int j = i - 1; j > 0; --j)
            {
                f[j] = min(f[j], f[j - 1]) + triangle[i][j];
            }
            f[0] += triangle[i][0];
        }
        int minn = 1e9 + 7;
        for(int i = 0; i < triangle[row - 1].size(); ++i)
            minn = min(minn, f[i]);
        return minn;
    }
};

方法4:从下到上更新答案

从上到下 和 从下到上 有什么区别?

如果我们从上到下更新答案的话,最后那一行会有多个答案,我们还需要一个循环找到最优那一个

但是如果从下到上,最终的答案就最上面那一个,就不用再循环一遍了

怎么做?

使用1个数组,从下到上不停更新答案,

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

其实采用这种方法比上面更简单,因为不用单独处理边界,如果理解了方法3为什么可以用1个数组来做,那在这个里面用1个数组的方法就更好理解了。
在这里插入图片描述

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        if(triangle.empty() || triangle[0].empty()) return 0;
        int f[500];
        int row = triangle.size();
        for(int i = 0; i < triangle[row - 1].size(); ++i) f[i] = triangle[row - 1][i];
        for(int i = row - 2; i >= 0; --i)
        {
            for(int j = 0; j < triangle[i].size(); ++j)
            {
                f[j] = min(f[j], f[j + 1]) + triangle[i][j];
            }
        }
        return f[0];
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值