爬樓梯問題

接觸DP最早的應(yīng)該就是這道題了吧泪电,翻了翻leetcode submission發(fā)現(xiàn)最早的是在一年前... 而且是最基礎(chǔ)的DP解法。在膜拜了大神們用矩陣甚至直接上斐波那契數(shù)列公式解法后覺得腦容量有點(diǎn)不太夠用。睬澡。曙砂。嗯,需要寫點(diǎn)東西捋一捋狐蜕,所以這是一個(gè)學(xué)渣(p.s. 話癆)關(guān)于爬樓梯類型題目的總結(jié)(如果有不對的地方歡迎米娜桑批評指正( ̄. ̄))宠纯。

題目: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:達(dá)到最高層的步數(shù)(Integer)

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

Output:

達(dá)到最高層可以用多少種不同的方法(Integer)

1. 暴力遞歸

Intuitive:

想法很簡單,既然下一步只有兩種選擇:1步和2步层释, 那么下步走的方法也就是2種婆瓜。總的方法數(shù)即走一步的方法 + 走兩步的方法湃累。

Edge cases:

當(dāng)前走的步數(shù)等于或者大于0時(shí)結(jié)束遞歸

Recursion Tree

例如目標(biāo)層數(shù)是5勃救,那么recursion tree應(yīng)該是這個(gè)樣子滴

image.png

可以看到每個(gè)節(jié)點(diǎn)我們都有兩個(gè)選擇碍讨,走一步或者兩步。那么一共有 2^n 個(gè)節(jié)點(diǎn)蒙秒,時(shí)間復(fù)雜度 2^n. 至于空間復(fù)雜度則是樹的深度勃黍,最壞的情況,我們每次只走一步晕讲,深度達(dá)到n覆获。另外也不難看出在遞歸的過程中存在很多重復(fù)的計(jì)算,這就是下一個(gè)算法優(yōu)化的點(diǎ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:

所以弄息,怎么避免重復(fù)計(jì)算呢?很明顯的想法就是把算過的存起來嘛勤婚,下次計(jì)算前先查一下摹量,有結(jié)果就跳過,沒結(jié)果就算馒胆。就是這樣一個(gè)小小的優(yōu)化缨称, 就把復(fù)雜度從2^n縮減到n(p.s. array 查詢的復(fù)雜度是O(1)),你敢信!嗯祝迂,所以在面試官followup的時(shí)候睦尽,別老想有多fancy或者tricky的變化,逮著最明顯的那個(gè)劣勢優(yōu)化就好~

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. 動態(tài)規(guī)劃

Intuitive:

那么就來到高大上的DP了当凡,曾經(jīng)有一度我都認(rèn)為按照我的水平面試如果遇到DP的題就基本可以跪了。雖然現(xiàn)在依然有點(diǎn)怵纠俭,但是發(fā)現(xiàn)DP他不是玄學(xué)沿量,一上來就給你整個(gè)高大上的狀態(tài)轉(zhuǎn)移方程,自然會一臉懵逼冤荆。但是欧瘪,如果了解了DP是怎么從暴力遞歸,記憶化搜索一步步進(jìn)化而來的匙赞,那么就可以不再害怕DP這個(gè)磨人的小妖精啦佛掖。不記得從哪里看到一句話,DP是一種思想而不是算法涌庭,嗯芥被,所以要想的多...

DP的本質(zhì)就是當(dāng)前結(jié)果依賴于之前結(jié)果,或者說子問題坐榆。那么我們在第i步時(shí)候的方法數(shù)就依賴于之前走的步數(shù)拴魄,在這道題就是:
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];
}

其實(shí)關(guān)于動態(tài)規(guī)劃和記憶搜索的區(qū)別匹中,我并不是分的很清楚夏漱,有的資料說一個(gè)是top down一個(gè)是bottom up。嗯顶捷,那么對于倒著填的DP又該怎么解釋呢挂绰?在這里mark一下,以后找找資料做個(gè)記憶化搜索和動態(tài)規(guī)劃的比較v( ̄︶ ̄)y

其實(shí)從狀態(tài)轉(zhuǎn)移方程不難看出來服赎,結(jié)果dp[i]只依賴于前兩個(gè)數(shù)dp[i - 1]和dp[i - 2]葵蒂。所以在不要求打印路徑的情況下,我們真的需要一個(gè)O(n)的空間么重虑?是不是只要維護(hù)兩個(gè)變量滾動更新就好了践付?那么之前的DP解法可以進(jìn)一步簡化了。

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~~~ (= ̄ω ̄=)
其實(shí)這部分才是我寫這篇又臭又長的博文的動機(jī)所在缺厉,那么就讓我試試用我捉襟見肘的語言表達(dá)能力和慘不忍睹的數(shù)學(xué)功底能不能解釋清楚吧...

從之前的DP解法我們知道遞推公式是這樣滴:
f(n) = f(n - 1) + f(n - 2)
那么對于這樣一個(gè)二階的遞推公式我們能不能借用一個(gè)二階的矩陣來表示這個(gè)遞推關(guān)系呢永高?試一試哈,設(shè)我們的謎之矩陣為

image.png

我們想促成這樣的一個(gè)表達(dá)式:

image.png

已知的一些關(guān)系為:

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

把這些已知的數(shù)代入這個(gè)遞推關(guān)系式中:

image.png

四個(gè)未知數(shù)四個(gè)方程,那么可以求出謎之矩陣為

image.png

那么通式為:

image.png

通式右邊的部分我們可以繼續(xù)用公式替換:
例如把:

image.png

代入通式提针,得:

image.png

以此類推通式可以化簡為:

image.png

那么原問題就可以被轉(zhuǎn)化成求二階矩陣n - 2次方的問題了乏梁。先不考慮矩陣,從最簡單的來哈关贵,如果求一個(gè)數(shù)的n次方,讓你用O(lgn)的算法求出卖毁,最直接的想法是什么揖曾?沒錯轉(zhuǎn)化成二進(jìn)制計(jì)算。
舉個(gè)例子, 求10 ^ 35, 可以分解為:

image.png

而35轉(zhuǎn)化為二進(jìn)制為:

image.png

不難看出亥啦,只有在二進(jìn)制位上是1的時(shí)候我們才需要乘以該位對應(yīng)的數(shù)值炭剪,而遇到為0的情況就直接skip,運(yùn)算次數(shù)減少了很多呀翔脱。

那么對于矩陣的乘法也是一樣的道理滴奴拦。最多需要你implement一個(gè)計(jì)算兩個(gè)矩陣相乘的helper function 而已。時(shí)間復(fù)雜度分析也很清楚了届吁,矩陣的n次方可以由矩陣n/2次方的平方得出错妖,以此類推可以有這樣一個(gè)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;
}

//計(jì)算矩陣 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;
}

//計(jì)算兩矩陣相乘
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. 斐波那契公式解法

最后一種是借助用斐波那契數(shù)列的解來解題疚沐,更多數(shù)學(xué)...
為了方便用二元一次方程的通解我們把斐波那契的公司小小改動一下暂氯,表達(dá)的意思還是一樣的哈:

image.png
image.png

則原方程轉(zhuǎn)化為:

image.png

兩邊都除以相同項(xiàng)得:

image.png

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

image.png

則f(n) 可以寫為:

image.png

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

image.png

代入之前的公式得:

image.png

大功告成~ 所以code也就是直接套用這個(gè)公式一下就好,偷個(gè)懶不寫了亮蛔。另外從這個(gè)公式也不難看出斐波那契數(shù)列問題如果用brute force方法解的話復(fù)雜度是2^n的痴施。

補(bǔ)充:

嗯,之所以花時(shí)間來總結(jié)這個(gè)題,就是因?yàn)楹芏郉P的題其實(shí)本質(zhì)就是個(gè)爬樓梯問題辣吃,或者說本質(zhì)就是斐波那契數(shù)列問題动遭。那么只要我們了解了這個(gè)方法論之后,那么在遇到類似的題目就可以見招拆招神得,萬變不離其宗啦厘惦。
舉個(gè)??:
有一對兔子,每個(gè)月都生一對兔子循头,小兔子長到第三個(gè)月后每個(gè)月又生一對兔子绵估,假如兔子都不死,問每個(gè)月的兔子總數(shù)為多少卡骂?

貢獻(xiàn)于第n個(gè)月的兔子數(shù)量的是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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末全跨,一起剝皮案震驚了整個(gè)濱河市缝左,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌浓若,老刑警劉巖渺杉,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異挪钓,居然都是意外死亡是越,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進(jìn)店門碌上,熙熙樓的掌柜王于貴愁眉苦臉地迎上來倚评,“玉大人,你說我怎么就攤上這事馏予√煳啵” “怎么了?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵霞丧,是天一觀的道長呢岗。 經(jīng)常有香客問我,道長蛹尝,這世上最難降的妖魔是什么后豫? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮突那,結(jié)果婚禮上硬贯,老公的妹妹穿的比我還像新娘。我一直安慰自己陨收,他們只是感情好饭豹,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布鸵赖。 她就那樣靜靜地躺著,像睡著了一般拄衰。 火紅的嫁衣襯著肌膚如雪它褪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天翘悉,我揣著相機(jī)與錄音茫打,去河邊找鬼。 笑死妖混,一個(gè)胖子當(dāng)著我的面吹牛老赤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播制市,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼抬旺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了祥楣?” 一聲冷哼從身側(cè)響起开财,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎误褪,沒想到半個(gè)月后责鳍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡兽间,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年历葛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘀略。...
    茶點(diǎn)故事閱讀 40,742評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡恤溶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屎鳍,到底是詐尸還是另有隱情,我是刑警寧澤问裕,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布逮壁,位于F島的核電站,受9級特大地震影響粮宛,放射性物質(zhì)發(fā)生泄漏窥淆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一巍杈、第九天 我趴在偏房一處隱蔽的房頂上張望忧饭。 院中可真熱鬧,春花似錦筷畦、人聲如沸词裤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吼砂。三九已至逆航,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間渔肩,已是汗流浹背因俐。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留周偎,地道東北人抹剩。 一個(gè)月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像蓉坎,于是被迫代替她去往敵國和親澳眷。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 正如前文所說袍嬉,我們把爬上N個(gè)臺階共有有多少種方法這一問題通過遞歸的方法得以了解決境蔼,但問題雖然解決了可我們想過...
    Leon_Geo閱讀 308評論 0 1
  • 最近看到很有意思的一道題目,問的是?有一座高度10級臺階的樓梯伺通,從下往上走箍土,每跨一步只能向上1級或者2級臺階...
    Leon_Geo閱讀 1,018評論 2 5
  • 問題來源 可愛的小明特別喜歡爬樓梯,他有的時(shí)候一次爬一個(gè)臺階罐监,有的時(shí)候一次爬兩個(gè)臺階吴藻,有的時(shí)候一次爬三個(gè)臺階。如果...
    yigoh閱讀 3,855評論 3 5
  • 背景 一年多以前我在知乎上答了有關(guān)LeetCode的問題, 分享了一些自己做題目的經(jīng)驗(yàn)弓柱。 張土汪:刷leetcod...
    土汪閱讀 12,748評論 0 33
  • 這個(gè)刪除起來還比較麻煩沟堡,直接卸載是卸載不干凈的。必須先安裝上矢空,然后如下操作 之后再對garageband進(jìn)行卸載就...
    鴨梨山大哎閱讀 1,548評論 0 2