爬樓梯問題

接觸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閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容

  • 正如前文所說,我們把爬上N個臺階共有有多少種方法這一問題通過遞歸的方法得以了解決,但問題雖然解決了可我們想過...
    Leon_Geo閱讀 313評論 0 1
  • 最近看到很有意思的一道題目,問的是?有一座高度10級臺階的樓梯,從下往上走,每跨一步只能向上1級或者2級臺階...
    Leon_Geo閱讀 1,025評論 2 5
  • 問題來源 可愛的小明特別喜歡爬樓梯,他有的時候一次爬一個臺階,有的時候一次爬兩個臺階,有的時候一次爬三個臺階。如果...
    yigoh閱讀 3,862評論 3 5
  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經驗。 張土汪:刷leetcod...
    土汪閱讀 12,763評論 0 33
  • 這個刪除起來還比較麻煩,直接卸載是卸載不干凈的。必須先安裝上,然后如下操作 之后再對garageband進行卸載就...
    鴨梨山大哎閱讀 1,567評論 0 2