注意:斐波那契遞歸求解的時間複雜度爲O(2n)。python
子問題不獨立適合動態規劃算法設計。git
分治:將原問題劃分爲互不相交的子問題,遞歸求解子問題,再將它們的解組合起來。算法
動態規劃:子問題重疊的狀況,不一樣的子問題具備公共的子子問題數組
利用動態規劃須要知足:框架
。。。學習
求一堆不相鄰數字和最大,沒有要求選兩個,能夠選多個。優化
OPT(n)指的是數據到下標爲n的數字選取的最好的方案。網站
#遞歸方程實現 def rec_opt(arr, i): if i == 0: return arr[0] elif i == 1: return max(arr[0], arr[1]) else: A = rec_opt(arr, i - 2) + arr[i]#選擇當前值 B = rec_opt(arr, i - 1)#不選當前值 return max(A, B) #非遞歸實現:dp動態規劃 def dp_opt(arr): opt = [0 for i in range(len(arr))] opt[0] = arr[0] opt[1] = max(arr[0], arr[1]) for i in range(2,len(arr)): A = opt[i-2] + arr[i] B = opt[i-1] opt[i] = max(A,B) return opt[-1] arr1 = [4, 1, 1, 9, 1] arr2 = [1, 2, 4, 1, 7, 8, 3] print(rec_opt(arr1, len(arr1)-1))#13 print(dp_opt(arr1))#13 print(rec_opt(arr2, len(arr2)-1))#15 print(dp_opt(arr2))#15
#遞歸實現 def rec_subset(arr, i, s): if s == 0: return True elif i == 0: return arr[0] == s elif arr[i] > s: return rec_subset(arr, i-1, s) else: A = rec_subset(arr, i-1, s-arr[i]) B = rec_subset(arr, i-1, s) return A or B arr = [3, 34, 4, 12, 5, 2] print(rec_subset(arr, len(arr)-1, 9))#True print(rec_subset(arr, len(arr)-1, 10))#True print(rec_subset(arr, len(arr)-1, 13))#False
#非遞歸實現:dq動態規劃 def dp_subset(arr, S): import numpy as np subset = np.zeros((len(arr), S+1), dtype=bool) subset[:, 0] = True subset[0, :] = False subset[0, arr[0]] = True for i in range(1, len(arr)): for s in range(1, S+1): if arr[i] > s: subset[i, s] = subset[i-1, s] else: A = subset[i-1, s-arr[i]] B = subset[i-1, s] subset[i, s] = A or B r, c = subset.shape return subset[r-1, c-1] arr = [3, 34, 4, 12, 5, 2] print(dp_subset(arr, 9))#True print(dp_subset(arr, 10))#True print(dp_subset(arr, 13))#False
假設你是個小偷,揹着一個可裝4磅東西的揹包。你可盜竊的商品有以下3件。spa
爲了讓盜竊的商品價值最高,你該選擇哪些商品?設計
最簡單的算法以下:嘗試各類可能的商品組合,並找出價值最高的組合。
這樣可行,但速度很是慢。在有3件商品的狀況下,你須要計算8個不一樣的集合;有4件商品時,你須要計算16個集合。每增長一件商品,須要計算的集合數都將翻倍!這種算法的運行時間爲O(2n),真的是慢如蝸牛。
只要商品數量多到必定程度,這種算法就行不通。在第8章,你學習瞭如何找到近似解,這接近最優解,但可能不是最優解。那麼如何找到最優解呢?
答案是使用動態規劃!下面來看看動態規劃算法的工做原理。動態規劃先解決子問題,再逐步解決大問題。
對於揹包問題,你先解決小揹包(子揹包)問題,再逐步解決原來的問題。
動態規劃是一個難以理解的概念,若是你沒有當即搞懂,也不用擔憂,咱們將研究不少示例。
先來演示這種算法的執行過程。看過執行過程後,你內心將有一大堆問題!我將竭盡所能解答這些問題。
每一個動態規劃算法都從一個網格開始,揹包問題的網格以下。
網格的各行爲商品,各列爲不一樣容量(1~4磅)的揹包。全部這些列你都須要,由於它們將幫助你計算子揹包的價值。
網格最初是空的。你將填充其中的每一個單元格,網格填滿後,就找到了問題的答案!你必定要跟着作。請你建立網格,咱們一塊兒來填滿它。
1. 吉他行
後面將列出計算這個網格中單元格值的公式。咱們先來一步一步作。首先來看第一行。
這是吉他行,意味着你將嘗試將吉他裝入揹包。在每一個單元格,都須要作一個簡單的決定:偷不偷吉他?別忘了,你要找出一個價值最高的商品集合。
第一個單元格表示揹包的容量爲1磅。吉他的重量也是1磅,這意味着它能裝入揹包!所以這個單元格包含吉他,價值爲1500美圓。
下面來開始填充網格。
與這個單元格同樣,每一個單元格都將包含當前可裝入揹包的全部商品。
來看下一個單元格。這個單元格表示揹包的容量爲2磅,徹底可以裝下吉他!
這行的其餘單元格也同樣。別忘了,這是第一行,只有吉他可供你選擇。換言之,你僞裝現在還無法盜竊其餘兩件商品。
此時你極可能心存疑惑:原來的問題說的是4磅的揹包,咱們爲什麼要考慮容量爲1磅、2磅等的揹包呢?前面說過,動態規劃從小問題着手,逐步解決大問題。這裏解決的子問題將幫助你解決大問題。請接着往下讀,稍後你就會明白的。
此時網格應相似於下面這樣。
別忘了,你要作的是讓揹包中商品的價值最大。這行表示的是當前的最大價值。它指出,若是你有一個容量4磅的揹包,可在其中裝入的商品的最大價值爲1500美圓。
2. 音響行
咱們來填充下一行——音響行。你如今出於第二行,可偷的商品有吉他和音響。在每一行,可偷的商品都爲當前行的商品以及以前各行的商品。所以,當前你還不能偷筆記本電腦,而只能偷音響和吉他。咱們先來看第一個單元格,它表示容量爲1磅的揹包。在此以前,可裝入1磅揹包的商品的最大價值爲1500美圓。
該不應偷音響呢?
揹包的容量爲1磅,能裝下音響嗎?音響過重了,裝不下!因爲容量1磅的揹包裝不下音響,所以最大價值依然是1500美圓。
接下來的兩個單元格的狀況與此相同。在這些單元格中,揹包的容量分別爲2磅和3磅,而之前的最大價值爲1500美圓。
因爲這些揹包裝不下音響,所以最大價值保持不變。
揹包容量爲4磅呢?終於可以裝下音響了!原來的最大價值爲1500美圓,但若是在揹包中裝入音響而不是吉他,價值將爲3000美圓!所以仍是偷音響吧。
你更新了最大價值!若是揹包的容量爲4磅,就能裝入價值至少3000美圓的商品。在這個網格中,你逐步地更新最大價值。
3. 筆記本電腦行
下面以一樣的方式處理筆記本電腦。筆記本電腦重3磅,無法將其裝入容量爲1磅或2磅的揹包,所以前兩個單元格的最大價值仍是1500美圓。
對於容量爲3磅的揹包,原來的最大價值爲1500美圓,但如今你可選擇盜竊價值2000美圓的筆記本電腦而不是吉他,這樣新的最大價值將爲2000美圓!
對於容量爲4磅的揹包,狀況頗有趣。這是很是重要的部分。當前的最大價值爲3000美圓,你可不偷音響,而偷筆記本電腦,但它只值2000美圓。
價值沒有原來高。但等一等,筆記本電腦的重量只有3磅,揹包還有1磅的容量沒用!
在1磅的容量中,可裝入的商品的最大價值是多少呢?你以前計算過。
根據以前計算的最大價值可知,在1磅的容量中可裝入吉他,價值1500美圓。所以,你須要作以下比較。
你可能始終心存疑惑:爲什麼計算小揹包可裝入的商品的最大價值呢?希望你如今明白了其中的緣由!餘下了空間時,你可根據這些子問題的答案來肯定餘下的空間可裝入哪些商品。筆記本電腦和吉他的總價值爲3500美圓,所以偷它們是更好的選擇。最終的網格相似於下面這樣。
答案以下:將吉他和筆記本電腦裝入揹包時價值最高,爲3500美圓。
你可能認爲,計算最後一個單元格的價值時,我使用了不一樣的公式。那是由於填充以前的單元格時,我故意避開了一些複雜的因素。其實,計算每一個單元格的價值時,使用的公式都相同。這個公式以下。
你可使用這個公式來計算每一個單元格的價值,最終的網格將與前一個網格相同。如今你明白了爲什麼要求解子問題吧?你能夠合併兩個子問題的解來獲得更大問題的解。
你可能仍是以爲這像是變魔術。
假設你發現還有第四件商品可偷——一個iPhone!
此時須要從新執行前面所作的計算嗎?不須要。別忘了,動態規劃逐步計算最大價值。到目前爲止,計算出的最大價值以下。
這意味着揹包容量爲4磅時,你最多可偷價值3500美圓的商品。但這是之前的狀況,下面再添加表示iPhone的行。 最大價值可能發生變化!請嘗試填充這個新增的行,再接着往下讀。
咱們從第一個單元格開始。iPhone可裝入容量爲1磅的揹包。以前的最大價值爲1500美圓,但iPhone價值2000美圓,所以該偷iPhone而不是吉他。
在下一個單元格中,你可裝入iPhone和吉他。
對於第三個單元格,也沒有比裝入iPhone和吉他更好的選擇了。
對於最後一個單元格,狀況比較有趣。當前的最大價值爲3500美圓,但你可偷iPhone,這將
餘下3磅的容量。
3磅容量的最大價值爲2000美圓!再加上iPhone價值2000美圓,總價值爲4000美圓。新的最
大價值誕生了!
最終的網格以下。
問題:沿着一列往下走時,最大價值有可能下降嗎?
答案:不可能。每次迭代時,你都存儲當前的最大價值。最大價值不可能比之前低!
答案會隨之變化嗎?假設你按以下順序填充各行:音響、筆記本電腦、吉他。網格將會是什麼樣的?請本身動手填充這個網格,再接着往下讀。
網格將相似於下面這樣。
答案沒有變化。也就是說,各行的排列順序可有可無。
本身動手試試吧!就這個問題而言,這沒有任何影響,但對於其餘問題,可能有影響。
假設你還能夠偷一條項鍊,它重0.5磅,價值1000美圓。前面的網格都假設全部商品的重量爲整數,但如今你決定把項鍊給偷了,所以餘下的容量爲3.5磅。在3.5磅的容量中,可裝入的商品的最大價值是多少呢?不知道!由於你只計算了容量爲1磅、2磅、3磅和4磅的揹包可裝下的商品的最大價值。如今,你須要知道容量爲3.5磅的揹包可裝下的商品的最大價值。
因爲項鍊的加入,你須要考慮的粒度更細,所以必須調整網格。
假設你在雜貨店行竊,可偷成袋的扁豆和大米,但若是整袋裝不下,可打開包裝,再將揹包倒滿。在這種狀況下,再也不是要麼偷要麼不偷,而是可偷商品的一部分。如何使用動態規劃來處理這種情形呢?
答案是無法處理。使用動態規劃時,要麼考慮拿走整件商品,要麼考慮不拿,而無法判斷該不應拿走商品的一部分。
但使用貪婪算法可輕鬆地處理這種狀況!首先,儘量多地拿價值最高的商品;若是拿光了,再儘量多地拿價值次高的商品,以此類推。
例如,假設有以下商品可供選擇。
藜麥比其餘商品都值錢,所以要儘可能往揹包中裝藜麥!若是可以在揹包中裝滿藜麥,結果就是最佳的。
若是藜麥裝完後背包還沒滿,就接着裝入下一種最值錢的商品,以此類推。
假設你要去倫敦度假,假期兩天,但你想去遊覽的地方不少。你無法前往每一個地方遊覽,因此你列個單子。
對於想去遊覽的每一個名勝,都列出所需的時間以及你有多想去看看。根據這個清單,你能確定該去遊覽哪些名勝嗎?
這也是一個揹包問題!但約束條件不是揹包的容量,而是有限的時間;不是決定該裝入哪些商品,而是決定該去遊覽哪些名勝。請根據這個清單繪製動態規劃網格,再接着往下讀。
網格相似於下面這樣。
你畫對了嗎?請填充這個網格,決定該遊覽哪些名勝。答案以下。
假設你還想去巴黎,所以在前述清單中又添加了幾項。
去這些地方遊覽須要很長時間,由於你先得從倫敦前往巴黎,這須要半天時間。若是這3個地方都去玩,是否是要4.5天呢?
不是的,由於不是去每一個地方都得先從倫敦到巴黎。到達巴黎後,每一個地方都只需1天時間。所以玩這3個地方須要的總時間爲3.5天(半天從倫敦到巴黎,每一個地方1天),而不是4.5天。
將埃菲爾鐵塔加入「揹包」後,盧浮宮將更「便宜」:只要1天時間,而不是1.5天。如何使用動態規劃對這種狀況建模呢?
沒辦法建模。動態規劃功能強大,它可以解決子問題並使用這些答案來解決大問題。但僅當每一個子問題都是離散的,即不依賴於其餘子問題時,動態規劃才管用。這意味着使用動態規劃算法解決不了去巴黎玩的問題。
爲得到前述揹包問題的最優解,可能須要偷兩件以上的商品。但根據動態規劃算法的設計,最多隻需合併兩個子揹包,即根本不會涉及兩個以上的子揹包。不過這些子揹包可能又包含子揹包。
徹底可能。假設你還能夠偷一顆鑽石。
這顆鑽石很是大,重達3.5磅,價值100萬美圓,比其餘商品都值錢得多。你絕對應該把它給偷了!但當你這樣作時,餘下的容量只有0.5磅,別的什麼都裝不下。
經過前面的動態規劃問題,你獲得了哪些啓示呢?
要設計出動態規劃解決方案可能很難,這正是本節要介紹的。下面是一些通用的小貼士。
在這個例子中,只有兩個相似的單詞,真是過小兒科了。實際上,相似的單詞極可能有數千個。
Alex輸入了hish,那他本來要輸入的是fish仍是vista呢?
用於解決這個問題的網格是什麼樣的呢?要肯定這一點,你得回答以下問題。
在動態規劃中,你要將某個指標最大化。在這個例子中,你要找出兩個單詞的最長公共子串。hish和fish都包含的最長子串是什麼呢?hish和vista呢?這就是你要計算的值。
別忘了,單元格中的值一般就是你要優化的值。在這個例子中,這極可能是一個數字:兩個字符串都包含的最長子串的長度。
如何將這個問題劃分爲子問題呢?你可能須要比較子串:不是比較hish和fish,而是先比較his和fis。每一個單元格都將包含這兩個子串的最長公共子串的長度。這也給你提供了線索,讓你以爲座標軸極可能是這兩個單詞。所以,網格可能相似於下面這樣。
若是這在你看來猶如巫術,也不用擔憂。這些內容很難懂,但這也正是我到如今才介紹它們的緣由!
如今,你很清楚網格應是什麼樣的。填充該網格的每一個單元格時,該使用什麼樣的公式呢?因爲你已經知道答案——hish和fish的最長公共子串爲ish,所以能夠做點弊。
即使如此,你仍是不能肯定該使用什麼樣的公式。計算機科學家有時會開玩笑說,那就使用費曼算法(Feynman algorithm)。這個算法是以著名物理學家理查德·費曼命名的,其步驟以下。
(1) 將問題寫下來。
(2) 好好思考。
(3) 將答案寫下來。
計算機科學家真是一羣不按常理出牌的人啊!
實際上,根本沒有找出計算公式的簡單辦法,你必須經過嘗試才能找出管用的公式。有些算法並不是精確的解決步驟,而只是幫助你理清思路的框架。請嘗試爲這個問題找到計算單元格值的公式。給你一點提示吧:下面是這個單元格的一部分。
其餘單元格的值呢?別忘了,每一個單元格都是一個子問題的值。爲什麼單元格(3, 3)的值爲2呢?又爲什麼單元格(3, 4)的值爲0呢?
請找出計算公式,再接着往下讀。這樣即使你沒能找出正確的公式,後面的解釋也將容易理解得多。
最終的網格以下。
我使用下面的公式來計算每一個單元格的值。
實現這個公式的僞代碼相似於下面這樣。
if word_a[i] == word_b[j]:#兩個字母相同 cell[i][j] = cell[i-1][j-1] + 1 else:#兩個字母不一樣 cell[i][j] = 0
查找單詞hish和vista的最長公共子串時,網格以下。
須要注意的一點是,這個問題的最終答案並不在最後一個單元格中!對於前面的揹包問題,最終答案老是在最後的單元格中。但對於最長公共子串問題,答案爲網格中最大的數字——它可能並不位於最後的單元格中。
咱們回到最初的問題:哪一個單詞與hish更像?hish和fish的最長公共子串包含三個字母,而hish和vista的最長公共子串包含兩個字母。
所以Alex極可能本來要輸入的是fish。
假設Alex不當心輸入了fosh,他本來想輸入的是fish仍是fort呢?
咱們使用最長公共子串公式來比較它們。
最長公共子串的長度相同,都包含兩個字母!但fosh與fish更像。
這裏比較的是最長公共子串,但其實應比較最長公共子序列:兩個單詞中都有的序列包含的字母數。如何計算最長公共子序列呢?
下面是用於計算fish和fosh的最長公共子序列的網格的一部分。
你能找出填充這個網格時使用的公式嗎?最長公共子序列與最長公共子串很像,計算公式也很像。請試着找出這個公式——答案稍後揭曉。
最終的網格以下。
下面是填寫各個單元格時使用的公式。
僞代碼以下。
if word_a[i] == word_b[j]:#兩個字母相同 cell[i][j] = cell[i-1][j-1] + 1 else: #兩個字母不一樣 cell[i][j] = max(cell[i-1][j], cell[i][j-1])
本章到這裏就結束了!它絕對是本書最難理解的一章。動態規劃都有哪些實際應用呢?
練習
請繪製並填充用來計算blue和clues最長公共子串的網格。