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数组保存的最后一行的每一格的最短路径,然后取出该数据的最小值即为答案的解了。才了上述使用一维数组的递推方程。
具体的解题过程如下:
- 先计算出第二行的两个格子的最短路径
因为如果只计算出第一行的话,此时的dp数组只有一个数组,在接下去的i-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)。
- 计算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,比用贪心的思想的路径更加短。