这道题目是动态规划的经典入门级题目,因为最近遇到了动态规划的题目,所以网上查了一下,查到了这个,所以记录一下做题过程
描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例:
输入:n = 2
输出:2
输入:n = 7
输出:21
输入:n = 0
输出:1
思路1:
递归,这是自己最先想到的办法,设置递归的边界,即n=1和n=2。自顶而下,层层调用。不过时间复杂度和空间复杂度都过高,提交失败
public static int numWays1(int n) {
if (n == 1) {
return 1;
} else if (n == 2) {
return 2;
} else {
return (numWays1(n - 1) + numWays1(n - 2)) % 1000000007;
}
}
思路2:
仍然递归,记录每次递归的结果,顶层调用下层时,首先去“记录本”中查找,查到的话返回结果,查不到的话递归计算,并将计算结果存入“记录本”中。
static Map<Integer, Integer> step = new HashMap();
public static int numWays2(int n) {
if (n <= 1) {
return 1;
} else if (n == 2) {
return 2;
} else {
if (step.get(n) != null) {
return step.get(n);
} else {
int res = (numWays2(n - 1) + numWays2(n - 2)) % 1000000007;
step.put(n, res);
return res;
}
}
}
注意:
- ”记录本“必须定义在递归函数之外,否则每次递归都会创建记录本。
- ”记录本“可以选择数组或者集合,”记录本“的长度为n,但在递归函数之外无法确定长度,所以应该使用集合为”记录本“
思路3:
前两种都属于自顶而下的递归调用,而动态规划刚好与之相反,属于自底而上的解法。将问题层层拆分,拆分出小规模的子问题,解决子问题后层层叠加,逐步向顶层推进,决策出原始问题。下面是依然使用“记录表”来记录每次递归的结果,在函数内使用数组作为“记录本”
public static int numWays3(int n) {
if (n <= 1) {
return 1;
} else if (n == 2) {
return 2;
} else {
int[] step = new int[n + 1];
step[1] = 1;
step[2] = 2;
for (int i = 3; i <= n; i++) {
step[i] = (step[i - 1] + step[i - 2]) % 1000000007;
}
return step[n];
}
}
思路4:
使用动态规划自下而上递进的话,其实也可以不使用“记录本”,只需要保留计算每层所需要的变量即可。比如计算第三层的跳法,只需记录第一二层的跳法即可,计算第四层的跳法只需记录第二三层的跳法即可,所以引申出了以下这段更加优雅的代码
public static int numWays4(int n) {
int prev = 1, next = 2, temp = 0;
if (n <= 1) {
return 1;
}
if (n == 2) {
return 2;
}
for (int i = 3; i <= n; i++) {
temp = (prev + next) % 1000000007;
prev = next;
next = temp;
}
return next;
}
收获:
- 通过这道题,可以大致了解到动态规划是将复杂问题层层拆分成小规模问题来解决,记录子问题结果,然后自下而上,层层递进。
- 递归是自上而下层层调用,而动态规划是自下而上,将子问题层层堆叠,叠加出原始问题
- 不论递归还是动态规划,记录子问题结果可以优化算法
- 长度确定使用数组,长度不确定使用集合
再多做几道动态规划的题目之后打算出一篇《动态规划问题的一般解法》博客,感兴趣的小伙伴可以蹲一下