论爬楼梯能有多少种姿势(Java题解)

题目:

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

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

——题源力扣题库70.爬楼梯

代码模板:

 class Solution {
     public int climbStairs(int n) {
 
    }
 }

分析:

看到题目没什么头绪,那就先列举几种简单的情况看看:

  • n=1时,显然就一种方法

  • n=2时,就有两种方法:一口气爬完两个;或者一个一个慢慢爬

  • n=3时,有三种方法:先爬一个,再爬两个;或者先爬两个,再爬一个;又或是全是只爬一个

  • n=4时,同理可以推出五种方法

  • ……………………

1,2,3,5,8…… 等等,这些数据看上去有点眼熟——斐波那契数列Fibonacci sequence)!

你也或许能够发现:爬n阶的方法数,就是将爬到(n-2)阶与(n-1)阶的方法数加到一起,即:

$$
f(n)=f(n-2)+f(n-1)
$$

 

也证明了这就是斐波那契数列。

 

你可能觉得分析题目速度太快了,有点顶不住,那就慢下来看看这里:

  • 爬到n台阶之前一步,我可以选择走一阶或两阶

  • 那么这前一步便是在n-1处或n-2处

  • 在n-1处和n-2处同样可以往前推导

  • 直到推到n=1与n=2直接能获得答案的地方

  • 这样的思想会自然引出递归

 

  • 反过来想:在n-1处走一阶和在n-2处走两阶到n处,都有且仅各有一种,所以可以放心把这两类的数据加起来

  • (不要把两阶拆成两个一阶,走一个一阶已经算进了n-1阶的方法数中)

  • (到n-2走两阶,到n-1走一阶,保证了这两大类里的方法绝对完全不同)

  • 举个例子,假设要爬3阶,只要把最后爬1阶(爬到2阶的方法数)加上最后爬2阶(爬到1阶的方法数)即可

  • 这种思想引出自底向上会更方便

还顶不住的话可以多举一些例子,并且画图来辅助理解。


解法一:递归(自顶向下)

刚刚结束大一上的学习,可能就会想到用递归的方法:

 public int climbStairs(int n) {
     if(n <= 2){
         return n;
    }
     return climbStairs(n-1) + climbStairs(n-2);
 }
 //该解法的时间复杂度 = O(1) * O(2^n) = O(2^n)
 //(递归时间复杂度 = 解决一个子问题时间*子问题个数)
 //提交超出时间限额,我们需要想其它的办法
解法一优化(记忆化搜索):

上面的基础递归存在许多重复运算,不如用数组或表存放已知数据,时间复杂度变为O(n):

 //使用哈希map,记录计算过的数据
 //在力扣里把创建表的语句放在方法内似乎会超时欸,蹲一个犇犇来解释
 HashMap<Integer, Integer> temp = new HashMap();
 public int climbStairs(int n) {
        if(n <= 2){
         return n;
    }
 
     //先判断有没计算过
     if (temp.containsKey(n)) {
        //表中有,即计算过,直接返回
        return temp.get(n);
    } else {
         //表中没有,即没有计算过,进行递归计算,并且把结果保存到表中
         temp.put(n, (climbStairs(n - 1) + climbStairs(n - 2)));
         return temp.get(n);
    }
 }

 

解法二:动态规划(自底向上)

作为初学者,想到斐波那契已经跃跃欲试了,从1到n算斐波那契数,浅浅暴力一下:

 public int climbStairs(int n) {
     int pre=1,last=1,i=1;
     while(i<n){        //用一个循环来算出对应n位置上的斐波那契数
         int k=last;
         last+=pre;
         pre=k;
         i++;
    }
     return last;
 }
 //(这是我当时第一次做的代码,其实思路很接近滚动数组了,但是在写法上明显很生涩)

很明显,这样的代码不够专业。既然是斐波那契,一定有它的解法特点。

 

解法二优化1(简单动态规划):

把现有问题拆解为更小单元或更简单的问题从而求解的方法叫做动态规划Dynamic programming,简称 DP)。动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算

标准的dp写法如下:

 public int climbStairs(int n) {
     // 因为我这里将n与到达台阶数对应(忽略0),所以如果不加这个判断,当n=1时,dp[2]=2会导致空指针异常
     int[] dp = new int[n+1];
     if(n<=2){
         return n;
    }
     dp[1] =1;
     dp[2] =2;
     for(int i=3;i<=n;i++){
         dp[i] = dp[i-1]+dp[i-2];
    }
     return dp[n];
 }

简单解释一下:用n+1大的数组对应下表储存斐波那契数,n位上即前两位的数字相加之和,通过循环更新数组里的数据。

image-20220227215729858

这样得到的时间复杂度为O(n),但占用空间内存很大。

 

解法二优化2(官方标准做法):

因为全程只用到n,n-1,n-2三个位置的数字,不如创建一个大小为3的数组,让数据滚动起来:

 public int climbStairs(int n) {
     int p = 0, q = 0, r = 1;
     for (int i = 1; i <= n; ++i) {
         p = q;
         q = r;
         r = p + q;
        }
     return r;
 }
 //时间复杂度:循环执行 n 次,每次花费常数的时间代价,故渐进时间复杂度为 O(n)。
 //空间复杂度:这里只用了常数个变量作为辅助空间,故渐进空间复杂度为 O(1)。

官方给出的动图解释非常清晰:fig1

 

解法三:数学公式

柏拉图说过:数学是一切知识中的最高形式。

斐波那契数列能表示为如下数学式(证明略XD):

$$
a_n=\frac{1}{\sqrt{5}}[(\frac{1+\sqrt{5}}{2})^n-(\frac{1-\sqrt{5}}{2})^n]
$$

 

直接一通计算猛如虎:

 public int climbStairs(int n) {
     double sqrt5 = Math.sqrt(5);
     //记得这里是n+1次方,n台阶对应的是n+1位的斐波那契数
     double fib = Math.pow((1 + sqrt5) / 2, n + 1) - Math.pow((1 - sqrt5) / 2,n + 1);
     return (int)(fib / sqrt5);
 }

解法四:矩阵快速幂

(承认一下 这个方法我还不会ww 所以略了wwwwwwwwwwwwww)

随着 n的不断增大,O(n) 可能已经不能满足我们的需要了,我们可以用「矩阵快速幂」的方法把算法加速到 O(log n)。

数学大佬们可以看这里:爬楼梯 - 爬楼梯 - 力扣(LeetCode) (leetcode-cn.com)

 

 

平台题目对n有大小限制,所以也可以做成打表题。

当然啦,做题千人又有千解,这里只是列举出了最常见的几种方法,无论选择哪种,核心都在于理解并掌握方法本质。

这道简单的题中却能用到递归/哈希表/动态规划/滚动数组/数学……的方法,希望大家能多多探讨学习一道题的多种解答方式,不拘泥于做完的刷题形式,这样才能更好地提升自己嘛!

posted @ 2022-02-28 13:38  Meowki  阅读(74)  评论(3)    收藏  举报