DP 四板斧
以跳台阶的题目为例, 题目是以每次跳1层或2层的方式跳到n台阶, 问一共有多少种跳法
开始我觉得这是求路径个数,而通常dp算最大或最小值, 不适用。
但后来换个角度想,求最大最小必须得试过所有方法才得出来的, 而将所有的试过的方法计数,就是这题的解
写出递归
dp第一步要先写出递归来, 写递归得写收敛条件
这里的收敛条件是当传参小于0,则说明解法失败(多跳了),返回0。 当恰好为0则解法有效返回1
递归体就是跳1层和跳2层
1 2 3 4 5 6 7 8 9 10 11 12
| static int jump(int number) { if (number < 0) { return 0; } else if (number == 0) { return 1; } return jump(number - 1) + jump(number - 2); } int jumpFloor(int number) { return jump(number - 1) + jump(number - 2); }
|
用缓存来优化递归
递归显然会重复计算, 可以用缓存计算结果来避免, 当有缓存时使用缓存,否则才去计算, 最后将结果缓存
并且使用缓存时应该指定尾递归的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| int jump(int number, int *cache) { if (number < 3) { return cache[number]; } int ret1 = cache[number - 1] != -1 ? cache[number - 1] : jump(number - 1, cache); int ret2 = cache[number - 2] != -1 ? cache[number - 2] : jump(number - 2, cache); cache[number] = ret1 + ret2; return cache[number]; }
int jumpFloor(int number) { int cache[number + 1]; for (int i = 0; i < number + 1; i++) { cache[i] = -1; }
cache[0] = 0; cache[1] = 1; cache[2] = 2; return jump(number, cache); }
|
递归改for循环
递归的调用流程是自上而下,先计算(n)->计算(n -1)->再计算(n-2) … 0
现在要反过来, 先计算(0)->计算(1)->计算(2) … n
即自下而上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int jumpFloor(int number) { if (number < 3) { return number; }
int cache[number + 1]; cache[1] = 1; cache[2] = 2;
for (int i = 3; i <= number; i++) { cache[i] = cache[i - 1] + cache[i - 2]; }
return cache[number]; }
|
缩小缓存大小
仔细看, 其实每次计算只涉及最大的三个数, 所以数组并不需要那么大,只用3个单元就足够,故直接写成3个变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int jumpFloorD(int number) { if (number < 3) { return number; }
int total = 0; int minors2 = 1; int minors1 = 2;
for (int i = 3; i <= number; i++) { total = minors1 + minors2; minors2 = minors1; minors1 = total; }
return total; }
|
这样DP解法就出来了, 这个套路是我从力扣上学到的, 非常有效简单
总结
从上可见,你需要训练如何发现递归关系,剩下的工作就是缓存递归调用的结果,最后是将从上到下的解决思路变为从下到上。这样我们就可以用O(N)的时间复杂度去遍历
参考
https://leetcode.com/discuss/study-guide/1490172/dynamic-programming-is-simple