这道算法题是leetcode120题,求三角形最小路径和。
题目描述如下:
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。
>
例如,给定三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/triangle
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
我看了看,觉得和二叉树的先序遍历很类似,所以我写了如下的代码,其实就是用递归进行先序遍历,这个代码应该挺蠢的,这也是我绞尽脑汁写的,哎。。
public int minimumTotal(List<List<Integer>> triangle) {
Set<Integer> set = new HashSet<>();
preOrder(triangle, 0 ,0 , 0, set);
int length = Integer.MAX_VALUE;
for (int value : set) {
if (value < length) {
length = value;
}
}
return length;
}
void preOrder(List<List<Integer>> triangle, int col, int row, int value, Set<Integer> set) {
if (col > triangle.size() - 1) {
set.add(value);
return;
}
value = triangle.get(col).get(row) + value;
preOrder(triangle, col + 1, row, value, set);
preOrder(triangle, col + 1, row + 1, value, set);
}
结果如下:
超时之后,我没有意识到这个代码的复杂度(我不太会计算这个代码的复杂度,有点麻烦,各位看到的话帮忙看下)有多大,我只是把递归遍历改成了迭代的方式,另外也不引入 set,修改后代码如下:
public int minimumTotal(List<List<Integer>> triangle) {
int maxRow = triangle.size() - 1;
int mixValue = Integer.MAX_VALUE;
Stack<Cell> s = new Stack<>();
s.push(new Cell(0,0, 0));
while (!s.empty()) {
Cell cell = s.pop();
int sum = triangle.get(cell.row).get(cell.col) + cell.sum;
if ((cell.row+1) <= maxRow) {
s.push(new Cell(cell.col+1, cell.row+1, sum));
s.push(new Cell(cell.col, cell.row+1, sum));
} else {
if (sum < mixValue) {
mixValue = sum;
}
}
}
return mixValue;
}
static class Cell {
int col;
int row;
int sum;
public Cell(int col, int row, int sum) {
this.col = col;
this.row = row;
this.sum = sum;
}
}
虽然没有引入额外的set保存结果,这儿又引入了一个小对象Cell保存元素的坐标,引入Stack压入待遍历的元素。这段代码的复杂度和上面的一致,相当于下面的位置还是遍历了多次。另外根据我观察,空间复杂度也是巨大,在我的电脑上5分钟没有运行完,甚至还发生了多次GC(捂脸)。
那官方的解法是怎样的呢?
官方解法如下,使用动态规划法解决,我试了下只用了几毫秒就跑完了:
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[][] f = new int[n][n];
f[0][0] = triangle.get(0).get(0);
for (int i = 1; i < n; ++i) {
f[i][0] = f[i - 1][0] + triangle.get(i).get(0);
for (int j = 1; j < i; ++j) {
f[i][j] = Math.min(f[i - 1][j - 1], f[i - 1][j]) + triangle.get(i).get(j);
}
f[i][i] = f[i - 1][i - 1] + triangle.get(i).get(i);
}
int minTotal = f[n - 1][0];
for (int i = 1; i < n; ++i) {
minTotal = Math.min(minTotal, f[n - 1][i]);
}
return minTotal;
}
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/triangle/solution/san-jiao-xing-zui-xiao-lu-jing-he-by-leetcode-solu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
回到题目,我为什么开始担心自己的职业生涯?
这道题,我1不懂动态规划,2不够聪明。只找到了暴力破解法。这两个原因都让我离自己目标渐行渐远,有的人说慢慢学,算法都有些套路就好了,可是我现在越来越怀疑我还有多少东西要学?我已经不小了。。。之前学到的很多东西都逐渐忘了,又得学新东西,真的是压力大。而且我在想,我以前写的多少代码都是“暴力解法”,浪费了多少计算机性能和时间。真的是变成了自己讨厌的人,屎山制造者。
附:测试该算法超时的二维数组,点击查看,直接使用json反序列化就可以,注意这个字符串超过编译器规定的最大字符串字面量长度,拆分成两个拼接即可。
另外,但是学习还是得继续,时不我待,那什么是动态规划呢?
动态规划法是5大基础算法之一。5大基础算法分别是:分治、贪婪、动态规划、回溯、穷举。
维基百科的解释:
动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题[1]和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。