爬楼梯问题

接触DP最早的应该就是这道题了吧,翻了翻leetcode submission发现最早的是在一年前... 而且是最基础的DP解法。在膜拜了大神们用矩阵甚至直接上斐波那契数列公式解法后觉得脑容量有点不太够用。。。嗯,需要写点东西捋一捋,所以这是一个学渣(p.s. 话痨)关于爬楼梯类型题目的总结(如果有不对的地方欢迎米娜桑批评指正( ̄. ̄))。

题目:Climbing Stairs

You are climbing a stair case. It takes n steps to reach to the top.Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

Input:

Argument:达到最高层的步数(Integer)

Condition: 每次只能爬一步或者两步

Output:

达到最高层可以用多少种不同的方法(Integer)

1. 暴力递归

Intuitive:

想法很简单,既然下一步只有两种选择:1步和2步, 那么下步走的方法也就是2种。总的方法数即走一步的方法 + 走两步的方法。

Edge cases:

当前走的步数等于或者大于0时结束递归

Recursion Tree

例如目标层数是5,那么recursion tree应该是这个样子滴

image.png

可以看到每个节点我们都有两个选择,走一步或者两步。那么一共有 2^n 个节点,时间复杂度 2^n. 至于空间复杂度则是树的深度,最坏的情况,我们每次只走一步,深度达到n。另外也不难看出在递归的过程中存在很多重复的计算,这就是下一个算法优化的点啦。

Code:
TC:O(2^n) SC:O(n)
public int climbStairs(int target){
  return helper(0, target);
}

public int helper(int step, int target){
  //edge cases
  if(step == target){
    return 1;
  }

  if(step > target){
    return 0;
  }
  
  return helper(step + 1, target) + helper(step + 2, target);
}

2. 记忆化搜索

Intuitive:

所以,怎么避免重复计算呢?很明显的想法就是把算过的存起来嘛,下次计算前先查一下,有结果就跳过,没结果就算。就是这样一个小小的优化, 就把复杂度从2^n缩减到n(p.s. array 查询的复杂度是O(1)),你敢信!嗯,所以在面试官followup的时候,别老想有多fancy或者tricky的变化,逮着最明显的那个劣势优化就好~

Edge cases:

Edge case 是跟暴力递归的一样的。

Code:
TC: O(n) SC: O(n)
public int climbStairs(int target){
  int[] memo = new int[target + 1];
  return helper(0, target, memo);
}

public int helper(int step, int target, int[] memo){
  if (memo[step] != 0){
    return memo[step];
  }
  if(step == target){
    return 1;
  }

  if(step > target){
    return 0;
  }
  
  memo[i] = helper(step + 1, target, memo) + helper(step + 2, target, memo);
  return memo[i];
}

3. 动态规划

Intuitive:

那么就来到高大上的DP了,曾经有一度我都认为按照我的水平面试如果遇到DP的题就基本可以跪了。虽然现在依然有点怵,但是发现DP他不是玄学,一上来就给你整个高大上的状态转移方程,自然会一脸懵逼。但是,如果了解了DP是怎么从暴力递归,记忆化搜索一步步进化而来的,那么就可以不再害怕DP这个磨人的小妖精啦。不记得从哪里看到一句话,DP是一种思想而不是算法,嗯,所以要想的多...

DP的本质就是当前结果依赖于之前结果,或者说子问题。那么我们在第i步时候的方法数就依赖于之前走的步数,在这道题就是:
dp[i] = dp[i - 1] + dp[i - 2]

Edge Cases:

那么edge cases 就很明显了,总不能让index小于0吧╮(-_-)╭。

  • dp[1] = 1
  • dp[2] = 2
Code:
TC: O(n) SC:O(n)
public int climbStairs(int target){
  if (target == 1 || target == 2){
    return target;
  }

  int[] dp = new int[target + 1];
  dp[1] = 1;
  dp[2] = 2;
  for (int i = 3; i <= target; i++){
    dp[i] = dp[i - 1] + dp[i - 2];
  }
  return dp[target];
}

其实关于动态规划和记忆搜索的区别,我并不是分的很清楚,有的资料说一个是top down一个是bottom up。嗯,那么对于倒着填的DP又该怎么解释呢?在这里mark一下,以后找找资料做个记忆化搜索和动态规划的比较v( ̄︶ ̄)y

其实从状态转移方程不难看出来,结果dp[i]只依赖于前两个数dp[i - 1]和dp[i - 2]。所以在不要求打印路径的情况下,我们真的需要一个O(n)的空间么?是不是只要维护两个变量滚动更新就好了?那么之前的DP解法可以进一步简化了。

Code:
TC: O(n) SC:O(1)
public int climbStairs(int target){
  if (target == 1 || target == 2){
    return target;
  }

  int pre1 = 1;
  int pre2 = 2;
  for (int i = 3; i <= target; i++){
    int temp = pre1 + pre2
    pre1 = pre2;
    pre2 = temp;
  }
  return pre2;
}

4. 矩阵解法

Here comes the fun part~~~ (= ̄ω ̄=)
其实这部分才是我写这篇又臭又长的博文的动机所在,那么就让我试试用我捉襟见肘的语言表达能力和惨不忍睹的数学功底能不能解释清楚吧...

从之前的DP解法我们知道递推公式是这样滴:
f(n) = f(n - 1) + f(n - 2)
那么对于这样一个二阶的递推公式我们能不能借用一个二阶的矩阵来表示这个递推关系呢?试一试哈,设我们的谜之矩阵为

image.png

我们想促成这样的一个表达式:

image.png

已知的一些关系为:

f(1) = 1
f(2) = 2
f(3) = 3
f(4) = 5

把这些已知的数代入这个递推关系式中:

image.png

四个未知数四个方程,那么可以求出谜之矩阵为

image.png

那么通式为:

image.png

通式右边的部分我们可以继续用公式替换:
例如把:

image.png

代入通式,得:

image.png

以此类推通式可以化简为:

image.png

那么原问题就可以被转化成求二阶矩阵n - 2次方的问题了。先不考虑矩阵,从最简单的来哈,如果求一个数的n次方,让你用O(lgn)的算法求出,最直接的想法是什么?没错转化成二进制计算。
举个例子, 求10 ^ 35, 可以分解为:

image.png

而35转化为二进制为:

image.png

不难看出,只有在二进制位上是1的时候我们才需要乘以该位对应的数值,而遇到为0的情况就直接skip,运算次数减少了很多呀。

那么对于矩阵的乘法也是一样的道理滴。最多需要你implement一个计算两个矩阵相乘的helper function 而已。时间复杂度分析也很清楚了,矩阵的n次方可以由矩阵n/2次方的平方得出,以此类推可以有这样一个recrusion tree:

image.png

树的高度是lg(n), 那么我们需要做的乘法是也就是lg(n)次。

Edge Cases:

与之前差不多

Code:
TC: O(lg(n)) SC:O(1)
public int climStairs(int target){
  if(target < 1){
    return 0;
  }
  if (target == 1 || target == 2){
    return target;
  }
  int[][] matrix = {{1, 1},{1, 0}};
  int[][] res = matrixPow(matrix, target - 2);
  //别忘记乘以最开始的f(2) 与 f(1)
  return res[0][0] * 2 + res[0][1] * 1;
}

//计算矩阵 n 次方
public int[][] matrixPow(int[][] matrix, int n){
  int r = matrix.length;
  int c = matrix[0].length;
  int[][] res = new int[r][c];
  //单位矩阵
  for (int i = 0; i < r; i++){
    res[i][i] = 1;
  }
  int[][] placeHolder = matrix;
  for(; n > 0; n >>>= 1){
    if ((n & 1)  == 1){
      res = matrixMulti(res, placeHolder);
    }
    placeHolder = matrixMulti(placeHolder, placeHolder);
  }
  return res;
}

//计算两矩阵相乘
public int[][] matrixMulti(int[][] m1, int[][] m2){
  int m1Row = m1.length;
  int m1Col = m1[0].length;
  int m2Col = m2[0].length;
  int[][] res = new int[m1Row][m2Col];
  for (int i = 0; i < m1Row; i++){
    for (int k = 0; k < m1Col; k++){
      if (m1[i][k] != 0){
        for (int j = 0; j < m2Col; j++){
          if(m2[k][j] != 0){
            res[i][j] += m1[i][k] + m2[k][j];
          }
        }
      }
    }
  }
  return res;
}

5. 斐波那契公式解法

最后一种是借助用斐波那契数列的解来解题,更多数学...
为了方便用二元一次方程的通解我们把斐波那契的公司小小改动一下,表达的意思还是一样的哈:

image.png
image.png

则原方程转化为:

image.png

两边都除以相同项得:

image.png

套用二元一次方程通解得:

image.png

则f(n) 可以写为:

image.png

代入已知的f(0) = 0, f(1) = 1等... 可得:

image.png

代入之前的公式得:

image.png

大功告成~ 所以code也就是直接套用这个公式一下就好,偷个懒不写了。另外从这个公式也不难看出斐波那契数列问题如果用brute force方法解的话复杂度是2^n的。

补充:

嗯,之所以花时间来总结这个题,就是因为很多DP的题其实本质就是个爬楼梯问题,或者说本质就是斐波那契数列问题。那么只要我们了解了这个方法论之后,那么在遇到类似的题目就可以见招拆招,万变不离其宗啦。
举个🌰:
有一对兔子,每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?

贡献于第n个月的兔子数量的是f(n - 1) + f(n - 3)
递推公式是 f(n) = f(n - 1) + f(n - 3)
那么三阶递推公式就可以试试用三阶递推矩阵表示了,其余的步骤就大同小异啦。

Reference:

https://discuss.leetcode.com/topic/30625/easiest-java-solution
https://leetcode.com/problems/climbing-stairs/solution/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • 正如前文所说,我们把爬上N个台阶共有有多少种方法这一问题通过递归的方法得以了解决,但问题虽然解决了可我们想过...
    Leon_Geo阅读 287评论 0 1
  • 最近看到很有意思的一道题目,问的是☞有一座高度10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶...
    Leon_Geo阅读 966评论 2 5
  • 问题来源 可爱的小明特别喜欢爬楼梯,他有的时候一次爬一个台阶,有的时候一次爬两个台阶,有的时候一次爬三个台阶。如果...
    yigoh阅读 3,706评论 3 5
  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,663评论 0 33
  • 这个删除起来还比较麻烦,直接卸载是卸载不干净的。必须先安装上,然后如下操作 之后再对garageband进行卸载就...
    鸭梨山大哎阅读 1,478评论 0 2