首页 > 其他分享 >三角形最小路径和

三角形最小路径和

时间:2023-10-13 23:07:18浏览次数:36  
标签:triangle 答案 int 路径 最小 数组 三角形 号点 row

题目链接

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];
    }
};

标签:triangle,答案,int,路径,最小,数组,三角形,号点,row
From: https://blog.51cto.com/u_14882565/7851625

相关文章

  • burpsuite靶场----目录遍历----绝对路径
    burpsuite靶场----目录遍历----绝对路径靶场地址https://portswigger.net/web-security/file-path-traversal/lab-absolute-path-bypass正式开始1.随便打开一个图片2.然后filename先尝试./../../../../etc/passwd不行然后再尝试/etc/passed成功......
  • 代码随想录第二天|977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II
    977有序数组的平方题目链接:https://leetcode.cn/problems/squares-of-a-sorted-array/思路:双指针(实际是三指针),两个找最大值,一个确定平方后的位置。209.长度最小的子数组题目链接:https://leetcode.cn/problems/minimum-size-subarray-sum/思路:很像双指针,一个指向子数组开头,一......
  • 代码随想录训练营的第二天(Python)| 977.有序数组的平方、209.长度最小的子数组
    977.有序数组的平方暴力求解(O(n+logn))classSolution:defsortedSquares(self,nums:List[int])->List[int]:returnsorted(i**2foriinnums)双指针(O(n))由于列表是单调递增的,元素平方后的最大值要么在最前面,要么在最后面classSolution:defsort......
  • leet code 71. 简化路径
    71.简化路径题目描述给你一个字符串path表示指向某一文件或目录的Unix风格绝对路径(以'/'开头)请你将其转化为更加简洁的规范路径。在Unix风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点(..)表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分......
  • docker最小化jdk基础镜像
    一、说明1、使用的是 Oracle 的 JRE 不是 openjdk2、因为 java 依赖 glibc,所以基础镜像使用的是 alpine-glibc 而非 alpine,alpine-glibc 大概是11.1 M二、制作1、首先下载 jre,下载地址是https://www.java.com/en/download/manual.jsp,大概是77M。2、解压 jre ......
  • noip赛前20天冲刺集训 day2 ###寻找有向图中的最小疲惫路径###
    T1###寻找有向图中的最小疲惫路径###题目描述有一张n个点m条边的有向图,每条边上有一个正整数边权,你要顺着图上的有向边从1号点走到n号点。假设你经过的边边权依次为(w_1,w_2,\dots,w_t),则你的疲惫程度为\[\f(w)=\max_{i=1}^{t}w_i\timesi\,.\]你需要找到最......
  • 弗洛伊德(Floyd's)算法—解决最短路径经典算法
    弗洛伊德算法(Floyd'salgorithm)是一种用于解决图中最短路径问题的经典算法。由美国计算机科学家罗伯特·弗洛伊德于1962年提出,该算法通过动态规划的思想,在图中寻找任意两个节点之间的最短路径,具有广泛的应用。本文将详细介绍弗洛伊德算法的原理、实现细节以及应用案例。四、复杂度......
  • Go流程控制与快乐路径原则
    Go流程控制与快乐路径原则目录Go流程控制与快乐路径原则一、流程控制基本介绍二、if语句2.1if语句介绍2.2单分支结构的if语句形式2.3Go的if语句的特点2.3.1分支代码块左大括号与if同行2.3.2条件表达式不需要括号三、操作符3.1逻辑操作符3.2操作符的优先级三、if多......
  • 修改docker默认存储路径方法总结
    默认情况下,docker镜像的默认存储路径是/var/lib/docker或其他根目录,有的服务器本身硬盘容量不足需要挂载到数据盘中,所以总结一下修改docker的默认路径,方法如下:先创建新的docker目录mkdir/home/docker以挂载home目录为例,此处也可另外挂载一块磁盘,把新的docker目录建在新磁盘上......
  • 12_打印三角形
    一.打印三角1.图一#!/bin/bash#1#22#333#4444#55555#666666#7777777#88888888#999999999foriin$(seq9);dofor((j=1;j<=i;j++));doecho-n"$i"doneecho""done2.第二个图#!/bin/bash#|_#||......