Dynamic Programming(動(dòng)態(tài)規(guī)劃)

Introduction

作為科班出身的程序員,算法還是得懂一點(diǎn)點(diǎn)的底循。------佚名(我)踱卵。
動(dòng)態(tài)規(guī)劃是一個(gè)看起來(lái)很高大上的名字,讓人一聽(tīng)就很想知道這到底是個(gè)啥郑临,所以我常常需要跟身邊好奇的朋友們解釋一下栖博。成功的讓朋友們理解了動(dòng)態(tài)規(guī)劃的基本思想后,我決定寫(xiě)一篇博客來(lái)幫助更多好奇的人??厢洞。
動(dòng)態(tài)規(guī)劃既是一種數(shù)學(xué)優(yōu)化方法仇让,也是一種計(jì)算機(jī)編程方法, 該方法由理查德·貝爾曼于20世紀(jì)50年代開(kāi)發(fā)躺翻,并已在航空航天工程和經(jīng)濟(jì)學(xué)等多個(gè)領(lǐng)域得到應(yīng)用丧叽。
本文將以計(jì)算斐波那契數(shù)和爬樓梯問(wèn)題為例,描述記憶化搜索及動(dòng)態(tài)規(guī)劃的基本思想公你,使讀者對(duì)動(dòng)態(tài)規(guī)劃有一個(gè)基本的理解踊淳。

Top-down Approach

斐波那契數(shù)列是一個(gè)人稱(chēng)“兔子數(shù)列”的數(shù)列,形如1陕靠,1迂尝,2,3剪芥,5垄开,......從數(shù)列的第n(n >= 3)個(gè)數(shù)開(kāi)始,第n個(gè)數(shù)的值為第n-1和第n-2個(gè)數(shù)的和税肪。由于斐波那契數(shù)列非常的簡(jiǎn)單直觀溉躲,它常被用作講解記憶化搜索思想的例子。
在一個(gè)斐波那契額數(shù)列中益兄,給定任意索引n, 如何求該位置的斐波那契數(shù)呢锻梳?最簡(jiǎn)單的方法,用遞歸:

 private static int fibonacci(int n) {
        if (n == 1 || n == 2) {
            return 1;
        } else {
            return fibonacci(n - 1) + fibonacci(n - 2);
        }
    }

這個(gè)算法的時(shí)間復(fù)雜度是怎么樣的呢净捅,直觀的來(lái)看可能看不出來(lái)疑枯,如果將遞歸調(diào)用使用樹(shù)來(lái)進(jìn)行表示的話:


使用遞歸計(jì)算斐波那契數(shù)的遞歸樹(shù)

可以發(fā)現(xiàn)一共進(jìn)行了n層的調(diào)用,每往下走一層灸叼,節(jié)點(diǎn)數(shù)翻一倍(當(dāng)然這棵二叉樹(shù)不是滿的神汹,不論是國(guó)內(nèi)定義的還是國(guó)外定義的滿),這表示這個(gè)算法的時(shí)間復(fù)雜度達(dá)到了指數(shù)級(jí)別古今。
仔細(xì)觀察遞歸樹(shù)我們可以發(fā)現(xiàn)屁魏,這棵樹(shù)中有很多重復(fù)的節(jié)點(diǎn),或許我們可以使用緩存來(lái)減少遞歸調(diào)用捉腥,比如氓拼,使用某種數(shù)據(jù)結(jié)構(gòu)記錄n-2的值:

public class Main {
    private static int[] memoryTable;
    public static void main(String[] args) {
        int n = 0;
        memoryTable = new int[n + 1];
        if (n >= 1) memoryTable[1] = 1;
        if (n >= 2) memoryTable[2] = 1;
        System.out.println("n's Fibonacci: " + fibonacci(n));
    }

    private static int fibonacci(int n) {
        if (n <= 0) {
            return -1;
        }
        if (memoryTable[n] == 0) {
            memoryTable[n] = fibonacci(n - 1) + fibonacci(n - 2);
        }
        return memoryTable[n];
    }
}

那么,根節(jié)點(diǎn)的右邊那個(gè)子樹(shù)就可以直接被省略掉,遞歸樹(shù)就變成了這樣:


優(yōu)化之后的遞歸樹(shù)

使用一個(gè)大小為n + 1的數(shù)組來(lái)緩存計(jì)算過(guò)了的值犧牲了O(n)的空間(通常來(lái)說(shuō)完全可以接受)桃漾,算法被優(yōu)化到了線性時(shí)間復(fù)雜度坏匪。這種存儲(chǔ)已計(jì)算值的方法被稱(chēng)為“ memoization”。在Top-down approach里我們自頂向下的將一個(gè)問(wèn)題分解成子問(wèn)題并逐個(gè)求解撬统,通過(guò)記錄子問(wèn)題的解來(lái)進(jìn)行優(yōu)化适滓。

Bottom-up Approach

給定一個(gè)樓梯,你可以一次爬一階或者一次爬兩階恋追,但是每一次離開(kāi)第n階的時(shí)候需要花費(fèi)cost[n]的體力凭迹,求如何花費(fèi)最少的體力爬到頂,你可以從第一階或者第二階開(kāi)始網(wǎng)上爬苦囱。比如[30, 20, 60]嗅绸。花費(fèi)體力最少的方式就是從第二階往上爬一步撕彤,共耗費(fèi)20體力鱼鸠。通過(guò)舉例分析我們可以發(fā)現(xiàn)這個(gè)問(wèn)題有這樣的幾個(gè)特性:
1.該問(wèn)題最終的最優(yōu)解可以通過(guò)結(jié)合子問(wèn)題的最優(yōu)解來(lái)得到。在第n階的時(shí)候羹铅,我們只能從第n - 1階和第n - 2階爬上來(lái)蚀狰,所以我們只需要遞歸的求解爬到第n - 1階和第n - 2階最少需要多少步,然后取其中小的那一個(gè)就能得到爬到第n階最少需要多少步睦裳。
2.嘗試遞歸的求解的時(shí)候會(huì)發(fā)現(xiàn)我們會(huì)不斷的計(jì)算重復(fù)的子問(wèn)題造锅。從1我們可以看出來(lái)這個(gè)問(wèn)題的遞歸樹(shù)和求斐波那契數(shù)的問(wèn)題的遞歸樹(shù)是一樣的,所以我們可以跟之前一樣通過(guò)用一個(gè)數(shù)組來(lái)存儲(chǔ)已經(jīng)計(jì)算過(guò)的值來(lái)進(jìn)行優(yōu)化廉邑。
這個(gè)問(wèn)題的第一個(gè)特性叫做“Optimal substructure”,第二個(gè)特性叫做“Overlapping sub-problems”倒谷。同時(shí)具有這兩個(gè)特性的問(wèn)題才可能能夠通過(guò)動(dòng)態(tài)規(guī)劃來(lái)進(jìn)行求解蛛蒙。
通過(guò)以上分析,我們可以得出一個(gè)求解這個(gè)問(wèn)題的規(guī)律:

狀態(tài)轉(zhuǎn)移方程

這個(gè)規(guī)律又叫狀態(tài)轉(zhuǎn)移方程渤愁,其中dp代表離開(kāi)某一階需要消耗的體力牵祟。
有了狀態(tài)轉(zhuǎn)移方程之后,我們其實(shí)可以不用寫(xiě)遞歸抖格,直接用遞推的方式從第一步推到第n步就可以得到結(jié)果:

 public int minCostClimbingStairs(int[] cost) {
        int[] dp = new int[cost.length];
        for (int i = 0; i < cost.length; i++) {
            if (i == 0 || i == 1)  dp[i] = cost[i];
            else                   dp[i] = cost[i] + Math.min(dp[i - 1], dp[i - 2]);
        }
        return Math.min(dp[dp.length - 1], dp[dp.length - 2]);
  }

這種根據(jù)狀態(tài)轉(zhuǎn)移方程從最小的子問(wèn)題開(kāi)始一步一步推出最終結(jié)果的方式叫做Bottom-up Approach诺苹。
觀察代碼我們發(fā)現(xiàn)每一次我們?cè)谟?jì)算一個(gè)新的狀態(tài)的時(shí)候都只用前面兩個(gè)狀態(tài),所以我們其實(shí)可以不用數(shù)組雹拄,直接用兩個(gè)變量記錄前兩個(gè)狀態(tài)的值就行了:

 public int minCostClimbingStairs(int[] cost) {
        int b = cost[1], c = cost[0];
        for (int i = 2, a; i < cost.length; ++i, c=b, b = a) a = cost[i] + Math.min(b, c);
        return Math.min(b, c);
  }

我們發(fā)現(xiàn)Bottom-up Approach比Top-down Approach更進(jìn)一步收奔,把空間復(fù)雜度從O(n)優(yōu)化到了O(1)。

Conclusion and Prospect

本文通過(guò)兩個(gè)例子對(duì)動(dòng)態(tài)規(guī)劃的思想進(jìn)行了一個(gè)基本描述滓玖,重點(diǎn)描述了實(shí)現(xiàn)動(dòng)態(tài)規(guī)劃的Top-down Approach和Bottom-up Approach坪哄,針對(duì)具體問(wèn)題進(jìn)行了算法的時(shí)間復(fù)雜度和空間復(fù)雜度分析。本文還講解了適合使用動(dòng)態(tài)規(guī)劃進(jìn)行解法優(yōu)化的問(wèn)題的特性。
動(dòng)態(tài)規(guī)劃的應(yīng)用非常廣泛翩肌,而且具體問(wèn)題可能會(huì)比較復(fù)雜模暗,希望熟練掌握的同學(xué)可以去各大OJ找題自虐一下??。懶得找的同學(xué)可以直接試一下這道谷歌的面試題念祭。

References

[1] Dynamic programming的維基百科
[2]Min Cost Climbing Stairs

Acknowledgments

感謝那些在看到我讀算法書(shū)的時(shí)候跑過(guò)來(lái)問(wèn)我你懂動(dòng)態(tài)規(guī)劃嗎或者是在聽(tīng)說(shuō)動(dòng)態(tài)規(guī)劃后忍不住要刨根問(wèn)底的好奇寶寶們兑宇,是你們引發(fā)了我的思考。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末粱坤,一起剝皮案震驚了整個(gè)濱河市顾孽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌比规,老刑警劉巖若厚,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蜒什,居然都是意外死亡测秸,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)灾常,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)霎冯,“玉大人,你說(shuō)我怎么就攤上這事钞瀑∩蜃玻” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵雕什,是天一觀的道長(zhǎng)缠俺。 經(jīng)常有香客問(wèn)我,道長(zhǎng)贷岸,這世上最難降的妖魔是什么壹士? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮偿警,結(jié)果婚禮上躏救,老公的妹妹穿的比我還像新娘。我一直安慰自己螟蒸,他們只是感情好盒使,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著七嫌,像睡著了一般少办。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抄瑟,一...
    開(kāi)封第一講書(shū)人閱讀 49,772評(píng)論 1 290
  • 那天凡泣,我揣著相機(jī)與錄音枉疼,去河邊找鬼。 笑死鞋拟,一個(gè)胖子當(dāng)著我的面吹牛骂维,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播贺纲,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼航闺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了猴誊?” 一聲冷哼從身側(cè)響起潦刃,我...
    開(kāi)封第一講書(shū)人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎懈叹,沒(méi)想到半個(gè)月后乖杠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡澄成,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年胧洒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墨状。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡卫漫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出肾砂,到底是詐尸還是另有隱情列赎,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布镐确,位于F島的核電站包吝,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏辫塌。R本人自食惡果不足惜漏策,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望臼氨。 院中可真熱鬧,春花似錦芭届、人聲如沸储矩。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)持隧。三九已至,卻和暖如春逃片,著一層夾襖步出監(jiān)牢的瞬間屡拨,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呀狼,地道東北人裂允。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像哥艇,于是被迫代替她去往敵國(guó)和親绝编。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

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

  • #動(dòng)態(tài)規(guī)劃 關(guān)于動(dòng)態(tài)規(guī)劃貌踏,先摘一段[wiki][1]的描述: ``` 動(dòng)態(tài)規(guī)劃(英語(yǔ):Dynamic progra...
    itrek閱讀 1,056評(píng)論 0 0
  • (歡迎轉(zhuǎn)載十饥,但請(qǐng)注明出處并附帶鏈接)算法好久沒(méi)復(fù)習(xí)了,今天看見(jiàn)一妹子在辦公室刷Leetcode祖乳,頓時(shí)我也來(lái)了興趣逗堵,...
    Nick_Zuo閱讀 658評(píng)論 0 3
  • 動(dòng)態(tài)規(guī)劃法與分治方法 ??動(dòng)態(tài)規(guī)劃(Dynamic Programming)與分治方法相似,都是通過(guò)組合子問(wèn)題的解...
    山陰少年閱讀 2,041評(píng)論 0 4
  • 動(dòng)態(tài)規(guī)劃的關(guān)鍵點(diǎn):最優(yōu)化原理眷昆,也就是最優(yōu)子結(jié)構(gòu)性質(zhì)蜒秤。這指的是一個(gè)最優(yōu)化策略具有這樣的性質(zhì),無(wú)論過(guò)去狀態(tài)和決策如何隙赁,...
    EboyWang閱讀 1,437評(píng)論 0 6
  • 愛(ài)大海垦藏,但好像沒(méi)了緣分,但我一定是命里有海伞访,且愛(ài)得深沉愛(ài)得純粹...青島掂骏,我們雖然得以重逢,但卻已不是當(dāng)初的模樣厚掷。...
    s颸i閱讀 135評(píng)論 0 1