LeetCode第四天之《120 三角形最小路径和》

LeetCode120 三角形最小路径和

题目描述

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

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

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

提示:数组,动态规划

示例

输入:[[2], [3, 4], [6, 5, 7], [4, 1, 8, 3]]

输出:自顶向下的最小路径和为11 即 2 + 3 + 5 + 1 = 11。

题目解析

根据题目的意思可以将示例转化为如下三角形:

示例图示

  将三角形转化为这种形式之后,根据题目的意思,每一次走只能向正下,右下一格走。同时值得注意的是,测试用例给出的是一个等腰的三角形,即二维数组的长度和最后一个数组的长度是相同的。明确这一点可能有助于我们考虑特殊的情况。
  

解题思路

   题目描述中提到自顶向下的计算,并且是找到一条最短的路径,这就是一个最优解问题,这种问题常用得比较多的就是动态规划了,有时候贪心算法也行。写的时候想起了《算法导论》中讲动态规划的时候就有讲到一种名为“带备忘的自顶向下”的动态规划的实现方式。但是这种实现方式通过递归来实现比较多。这里用的是该书提到的另外一种实现方式:“自底向上”,即将问题转化为更小规模的子问题的求解。

自底向上法:这种方法一般需要定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”的子问题的求解。因而我们可以将子问题按规模排序,按小到大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果都已经保存。每个子问题只需求解一次。当我们求解它时,它的所有子问题已经提前求解完毕。

   在这道题中,我们可以很明显的发现题目正是按照这种方式来描述的。先算第1行的最小路径(就是该值本身),再算第二行,第三,第四行…。每一行的最小路径本身都是由它更小的行算来的。因此本题就可以使用这种方法来求解最小路径和(并不是采用和题目描述名词的“自顶向下”的方法哈,哎呀,这些名词不管啦)。但是在真正实现的时候我们并不清楚你每一步会往哪边走(这里可能会使用贪心的思想来写,这种思想是不正确的,后面会讲到),因此需要将每一行中的每个格子的最短路径都给计算出来(显然这种时间复杂度应该为O(n^2)),最后再取其最小值。

   “自底向上”的动态规划方式一般都是要写出递推方程,例如

dp[i] = max(dp[i], dp[i - 1]);

本题主要的递推方程如下:

dp[i] = min(dp[i], dp[i - 1]) + currentElement;

   dp数组就是用来保存每一行中每一格的最短路径。可能一开始会采用二维数组的方式来保存每一个值。但是按照题目给的提示最好能用O(n)的额外空间实现,因此我们这里可以采用“滚动数组”的概念,即重复给dp赋值。之所以能这样是因为我们并不关心最后一行之前保存了什么数据,我们关心的仅仅是dp数组保存的最后一行的每一格的最短路径,然后取出该数据的最小值即为答案的解了。才了上述使用一维数组的递推方程。

具体的解题过程如下:

  1. 先计算出第二行的两个格子的最短路径

   因为如果只计算出第一行的话,此时的dp数组只有一个数组,在接下去的i-1的索引可能就会导致索引越界了,因此可以先处理一下。

  1. 使用双重循环遍历

   第一重循环遍历整个输入列表,这里只需从第3行开始遍历,因为第一行和第二行都已经处理掉了。第二重循环就遍历数组项的每一个数据,循环内部只需要从第二个元素遍历到倒数第二个元素。第一个元素和倒数最后一个元素需要特殊处理,上面描述的递推方程不太适用,具体如下:

计算第一个元素:

dp[0] = dp[0] + triangle.get(i).get(0);

 因为第一元素只能由它上面那个元素走到。

计算最后一个元素:

dp[itemLen-1] = dp[itemLen - 2] + triangle.get(i).get(itemLen - 1);

   这里itemLen指的是该数组的长度。因为最后一个元素只能由它的左上角那个元素走到(例如示例中的7只能是 4->7)。

  1. 计算dp数组的最小值,将结果返回
      

程序实现

public int minimumTotal(List<List<Integer>> triangle) {
    int len = triangle.size();
    //先处理0, 1  长度的情况
    if (len == 0) {
    	return 0;
    }
    if (len == 1) {
    	return triangle.get(0).get(0);
    }
    int[] dp =  new int[len];
    //1.提前计算 第二行的数据
    dp[0] = triangle.get(1).get(0) +triangle.get(0).get(0);
    dp[1] = triangle.get(1).get(1) + triangle.get(0).get(0);

    int[] temp = new int[len];		//创建一个临时数组
    for (int i = 2; i < len; i++) {
        int itemLen = triangle.get(i).size();
        for (int j = 1; j < itemLen - 1; j++) {
        	temp[j] = Math.min(dp[j], dp[j - 1]) + triangle.get(i).get(j);	//递推式
    	}
        dp[0] = dp[0] + triangle.get(i).get(0);	//计算第一个数组
        dp[itemLen - 1] = dp[itemLen - 2] + triangle.get(i).get(itemLen - 1);	//计算倒数一个数据
        for (int j = 1; j < itemLen - 1; j++) {	//将dp重复赋值
            dp[j] = temp[j];
        }
    }
	
    //计算最小值
    int minVal = dp[0];
    for (int i = 1; i < len; i++) {
        if (minVal > dp[i]) {
            minVal = dp[i];
        }
    }
    return minVal;
}

程序运行结果

程序运行结果

  结果不是很好,原因应该就在于创建了一个临时数组,并且用一个循环将该数组的值赋值给dp数组,并且在空间上导致其复杂度应该为O(2n)。

  

错误的思路

   其实一开始在写的时候,思绪自然而然的就按照题目给的路径,每一步都去下面相邻的两个格子的最小值加起来,这样其实是贪心的思想,即每一次都取最小的值相加,但是在我们这道题目中是错误的。可以看下面例子:
贪心算法错误的路径

按照这种思路的结果是0,但是正确的应该是如下路径:
动态规划正确的路径

采用这种方式的结果是-1,比用贪心的思想的路径更加短。

猜你喜欢

转载自blog.csdn.net/weixin_44184990/article/details/108567237
今日推荐