题目描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

1
2
3
4
5
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶

示例2:

1
2
3
4
5
6
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1 阶 + 1 阶 + 1 阶
2.  1 阶 + 2 阶
3.  2 阶 + 1 阶

题解

实际上这道题跟斐波那契数列很像,用斐波那契的方法实现就成。

这道题可以倒着思考,我们考虑最终到楼梯顶时有2两种方式,即要么还有1阶到顶,要么还有2阶到顶。

那么从后往前思考,如果现在站在离顶还有1阶的地方,我们有多少种方法到这一阶?我们依旧可以将问题拆成上面那种方式思考,即有两种情况,要么走一1阶到这里,要么走2阶到这里。

如此循环下去,直到第1阶与第2阶。这些就是重叠的子问题,这里可以画一个递归树,网上盗一张图,如下。

f(20)可以理解为爬到20阶楼梯的方式,有2种方式,即在19阶,再走一步到顶,或在18阶,再走2步到顶。之后再以18、19阶为基础,继续向前去问有几种方式,直到最底的1、2层。

直接实现的话,就像下面的这个代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int climbStairs(int n) {
   if (n == 0) {
       return 0;
   }

   if (n == 1) {
       return 1;
   }

   if (n == 2) {
       return 2;
   }

   return climbStairs(n - 1) + climbStairs(n - 2);
}

然而在leetcode上提交后,会发现运行超时,原因在于重复的运算太多了,仔细想一下它的递归树有多大,有多少重复的计算。针对这个问题,需要对整棵树进行剪枝,降低复杂度。代码如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int interClimbStairs(int *data, int n) {
   if (data[n] != 0) {
       return data[n];
   }

   data[n] = interClimbStairs(data, n - 1) + interClimbStairs(data, n - 2);
   return data[n];
}

int climbStairs(int n) {
   if (n == 0) return 0;
   if (n == 1) return 1;
   if (n == 2) return 2;

   int *data = new int[n + 1];
   memset(data, 0, sizeof(int) * (n + 1));
   data[1] = 1;
   data[2] = 2;

   auto value = interClimbStairs(data, n);
   delete[] data;
   return value;
}

这里对中间运算的结果进行了存储,但耗费的内存稍微有点多,仔细考虑一下,我们的结果只依赖前面的两个状态,如果我们自底向上计算的话,只需要2个变量就够了,代码如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int climbStairs(int n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    if (n == 2) return 2;
    
    int a = 1;
    int b = 2;
    int v = 0;
    for (int i = 3; i <= n; i++) {
        v = a + b;
        a = b;
        b = v;
    }

    return v;
}

一般来说,动态规划都是自底向上计算的,当然思考时可以反过来思考,即从结果逆向思考到最开始的状态。如果使用递归逆向实现也行,只是这样的话,就需要将整个中间状态全都保存下来,稍微浪费了空间。而自底向上实现的话,只需要使用最少的中间变量即可。

补充

leetcode的题解真特么的好。