1-2走台阶问题
问题描述
假设有n个台阶,每跨一步只能上1阶或者2阶台阶。求总共有多少种走法?
问题分解
假设楼梯共有n级,总共的走法为:f(n)
当n = 1时的走法为: (1)。f(1) = 1
当n = 2时的走法为: (1,1),(2)。f(2) = 2
当n >= 2时,第一步有两种选择,可以选择走1级或者2级,当选择1级时,走完第一步后,还有n-1级,此时的走法为f(n-1)。当选择2级时,后面还有n-2级,此时的走法为f(n-2),那么总共的走法f(n)=f(n-1) + f(n-2)
因此递推公式为:f(n)=f(n-1) + f(n-2),结束条件是我们已知的f(1)=1, f(2)=2
因此使用递归的方法求解为:
/*
* 1-2
* use recursion
*/
long stairs(int n)
{
if (n == 1) {
return 1;
} else if (n == 2) {
return 2;
} else {
return stairs(n -1) + stairs(n - 2);
}
}
使用动态规划优化
当n比较大时,会发现上面的方法会很慢,比如当n=50,在我的电脑上要运行数分钟之久。
通过分析上面代码发现,return stairs(n -1) + stairs(n - 2);
, 在这一步分别递归调用stairs(n-1)
和 stairs(n-2)
,其中直到stairs(n-1)
调用返回才调用stairs(n-2)
,但是通过分析发现stairs(n-1)
又可以分解成stairs(n-1-1) + stairs(n-1-2)
, 也即stairs(n-2) + stairs(n-3)
,所以stairs(n-2)
在调用stairs(n-1)
的过程中已经计算过了,同理除了stairs(n-2)
还有大量的重复计算,如果递归的过程中将所有结果存储下来,那么将节省大量时间。
这是典型的用空间换时间的策略:
/*
* 1-2
* use dp
*/
long stairs_dp(int n, long *counter)
{
if (n <= 0)
return 0;
if (counter[n]) {
return counter[n];
} else {
if (n <= 2) {
counter[n] = n;
} else {
counter[n] = stairs_dp(n -1, counter) + stairs_dp(n -2, counter);
}
return counter[n];
}
}
进一步优化
进一步分析,发现n级的计算结果等于n-1级和n-2级计算结果之和,这不就是斐波那契数列?
因此,我们只要知道1级和2级的计算结果,那么n级的结果就可以通过1-n的循环计算出来
/*
* 1-2
* us dp with loop
*/
long stairs_dp_loop(int n)
{
if (n <= 0) {
return 0;
}
long step1 = 0;
long step2 = 1;
long step3 = 0;
for (int i = 1; i <= n; i++) {
step3 = step1 + step2;
step1 = step2;
step2 = step3;
}
return step3;
}
总结
1-2走台阶问题是典型的动态规划问题,解决问题的关键就是找到递推公式,即将问题的求值过程分解成数个更小问题的求值,最后将这些小问题的求值结果合并得到最终问题的求值结果;而这些小问题的求值又可以进一步分解,只到分解成简单的终点值(如果1级、2级台阶,我们看到能直接说出答案)。
得到递推公式以及终点值(结束条件),那么我们直接写递归函数就可以求得结果。
然而由于递归函数不注意剪枝,中间有大量的重复计算,因此可以采用空间换时间的方式进行优化,将中间计算结果保存起来,后面通过查表直接得到结果而不用重复计算。
有递推公式和终点值,前面我们都是从问题一步步往前递推直到终点值,然后再从终点值回溯到问题。那么我们是否可以直接从终点值反过来往问题来一步步求解呢?由此有了后面的通过循环来计算结果的方法。