在理解動態規劃、BFS和DFS一文中,只是初步講解了一下動態規劃,理解的並不到位,這裏再加深理解一下。html
本文主要參考什麼是動態規劃一文。python
相似於機器學習的步驟,對同一個問題,能夠用不一樣的模型建模,而後對於肯定的模型,能夠用不一樣的算法求解。程序員
通常的算法問題求解步驟,分爲兩步:算法
對應到動態規劃算法上,具體分爲這兩步:網絡
大事化小,小事化了。把一個複雜的問題分階段進行簡化,逐步化簡成簡單的問題。app
1.3.1 問題建模機器學習
1.3.2 問題求解的各個方法(從暴力枚舉 逐步優化到動歸)函數
暴力枚舉:
下面的樓梯問題,國王與金礦問題,還有最少找零硬幣數問題,均可以經過多層嵌套循環遍歷全部的可能,將符合條件的個數統計起來。只是時間複雜度是指數級的,因此通常 不推薦。性能
遞歸:
一、既然是從大到小,不斷調用狀態轉移方程,那麼就能夠用遞歸。
二、遞歸的時間複雜度是由階梯數和最優子結構的個數決定的。不一樣的問題,用遞歸的話可能效果會大不相同。
三、在階梯問題,最少找零問題中,遞歸的時間複雜度和空間複雜度都比動歸方法的差, 可是在國王與金礦的問題中,遞歸的時間複雜度和空間複雜度都比動歸方法好。這是須要注意的。學習
每一種算法都沒有絕對的好與壞,關鍵看應用場景。、
上面這句話說的很好,不止於遞歸和動歸,通常的算法也是,好比通常的排序算法,在不一樣的場景中,效果也大不相同。
備忘錄算法:
一、在階梯數N比較多的時候,遞歸算法的缺點就顯露出來了:時間複雜度很高。若是畫出遞歸圖(像二叉樹同樣),會發現有不少不少重複的節點。然而傳統的遞歸算法並不能識別節點是否是重複的,只要不到終止條件,它就會一直遞歸下去。
二、爲了不上述狀況,使遞歸算法可以不重複遞歸,就把已經獲得的節點都存起來,下次再遇到的時候,直接用存起來的結果就好了。這就是備忘錄算法。
三、備忘錄算法的時間複雜度和空間複雜度都獲得了簡化。
正經的動歸算法:
一、上述的備忘錄算法,儘管已經不錯了,可是依然仍是從最大的問題,遍歷獲得全部的最小子問題,空間複雜度是O(N)。
二、爲了再次縮小空間複雜度,咱們能夠自底向上的構造遞歸問題,經過分析最優子結構與最終問題之間的關係,咱們能夠獲得【狀態轉移方程】。
而後從最小的問題不斷往上迭代,即便一直到最大的原問題,也是隻依賴於前面的幾個最優子結構。這樣,空間複雜度就大大簡化。也就獲得了正經的動歸算法。
下面經過幾個例題,來具體瞭解動歸問題。
leetcode原題:你正在爬一個有n個臺階的樓梯,每次只能上 1個 或者 2個臺階,那麼到達頂端共有多少種不一樣的方法?
結束條件爲F(1) = 1,F(2) = 2
1.2.一、 解法1:遞歸
先用比較容易理解的遞歸求解(結束條件已知,遞歸公式已知,能夠直接寫代碼了)
class Solution: def climbStairs(self, n): """ :type n: int :rtype: int """ if n == 1: return 1 elif n == 2: return 2 else: return self.climbStairs(n-1) + self.climbStairs(n-2)
回想前面所說,遞歸的時間複雜度是由階梯數和最優子結構的個數決定的。這裏的階梯數是 N ,最優子結構個數是 2 。若是想象成一個二叉樹,那麼就能夠認爲是一個高度爲N-1,節點個數接近 2 的 N-1 次方的樹,所以此方法的時間複雜度能夠近似的看做是O(2N) 。
1.2.二、 解法2:備忘錄算法
參考什麼是動態規劃中遞歸的圖,發現有不少相同的參數被重複計算,重複的太多了。
因此這裏咱們想到了把重複的參數存儲起來,下次遞歸遇到時就直接返回該參數的結果,也就是備忘錄算法了,這裏須要用到一個哈希表,解決方法就是對類用init進行初始化。
class Solution: def __init__(self): self.map = {} def climbStairs(self, n): """ :type n: int :rtype: int """ if n == 1: return 1 if n == 2: return 2 if n in self.map: return self.map[n] else: value = self.climbStairs(n-1) + self.climbStairs(n-2) self.map[n] = value return value
這裏哈希表裏存了 N-2 個結果,時間複雜度和空間複雜度都是O(N)。程序性能獲得了明顯優化。
1.2.三、 解法3:動態規劃
以前都是自頂向下的求解,考慮一下自底向上的求解過程。從F(1)和F(2)邊界條件求,可知F(3) = F(1)+F(2)。不斷向上,可知F(N)只依賴於前兩個狀態F(N-1)和F(N-2)。因而咱們只須要保留前兩個狀態,就能夠求得F(N)。相比於備忘錄算法,咱們再一次簡化了空間複雜度。
這就是動態規劃了。(具體的細節看漫畫比較好理解。)
具體代碼實現中,能夠令F(N-2)=a,F(N-1)=b,則temp等於a+b,而後把a向前挪一步等於b,b向前挪一步等於temp。那麼下一次迭代時,temp就依然等於a+b。
代碼以下:
class Solution: def climbStairs(self, n): """ :type n: int :rtype: int """ if n == 1: return 1 if n == 2: return 2 a = 1 b = 2 for i in range(3,n+1): temp = a + b a = b b = temp return temp
參考Dynamic Programming中,用最少的硬幣數目找零錢的一個例子。
問題描述:
假設你是一家自動售貨機制造商的程序員。你的公司正設法在每一筆交易 找零時都能提供最少數目的硬幣以便工做能更加簡單。已知硬幣有四種(1美分,5美分,10美分,25美分)。假設一個顧客投了1美圓來購買37美分的物品 ,你用來找零的硬幣的最小數量是多少?
(這個問題用貪心算法也能解,具體細節看參考文獻)
就以動歸做爲解題的算法來創建模型吧。
模型已經獲得,接下來就運用算法進行求解。
這裏依然能夠按照例1的解法,由模型,很天然的想到用遞歸求解。
2.2.一、解法1,遞歸
邊界條件已知,模型已知,能夠直接寫代碼了。
def recMC(coinValueList,change): minCoins = change if change in coinValueList: return 1 else: for i in [c for c in coinValueList if c <= change]: numCoins = 1 + recMC(coinValueList,change-i) if numCoins < minCoins: minCoins = numCoins return minCoins print(recMC([1,5,10,25],63)
可是,對於每個大於25的數目,都有四個最優子結構,而後對於每一個最優子結構,還有大量相同重複的參數(具體細節看參考)。因此這個解法並不合適。
2.2.二、解法2,動態規劃
首先要有自底向上的思想,從change等於1時,開始往上迭代,參考最優子結構,記錄下來最少硬幣數。一直迭代到63。
1==>1
2==>min(2-1) + 1 = 2
3==>min(3-1) + 1 = 3
4==>min(4-1) + 1 = 4
5==>min(min(5-1) + 1 = 5, min(5-5) + 1 = 1)= 1
6==>min(min(6-1) + 1 = 2, min(6-5) + 1 = 2)= 2
7==>min(min(7-1) + 1 = 3, min(7-5) + 1 = 3)= 3
由此能夠推下去,每個change對應的最少硬幣數,均可以由前面的若干個最優子結構(有幾個最優子結構,由change是多少決定,change大於5就有兩個子結構,大於10就有三個。。)獲得。這樣一直迭代到63,那麼就能夠獲得63的最少硬幣數。
所以,須要一個循環來從頭至尾遍歷。
須要必定須要一個map來記錄部分結果。
每個change,咱們能夠根據上面的式子遍歷最優子結構,並將每一個子結構的結果都添加到一個list中,在遍歷完最有子結構之後,選擇最小的那一個,添加到map中去。
求解一個新的 i 的最優解的過程是很方便的,從最優子結構中挑選最小的值而後加1便可。
最優子結構的值,能夠用minCoin[i-j]獲得。其中j爲有效硬幣面額。
實現代碼:
def dpMakeChange(coinValueList,change): minCoins = { } for cents in range(change+1): #cents小於等於1時,coinCount會爲空,無法執行min。 #所以這裏先填上 if cents <= 1: minCoins[cents] = cents continue #遍歷cents的每一個最優子結構而且添加到list中,等待篩選 coinCount = [ ] for j in coinValueList: if cents >= j: coinCount.append(minCoins[cents - j] + 1) minCoins[cents] = min(coinCount) return minCoins[change] result = dpMakeChange([1,5,10,25],63) print(result)
固然這個函數是有瑕疵的,由於這個函數只告訴咱們最少的硬幣數,並不能告訴咱們應該找零的面額。因此咱們能夠擴展一下函數,跟蹤記錄咱們使用的硬幣便可。具體細節能夠看參考。
只講一下大體的思路。
問題中須要注意的地方:
這裏着重講解一下最後一點,也就是動態規劃最重要的地方。
最優子結構:對於5個金礦,10個工人的狀況,日後退一步存在兩種狀況。(第五個金礦的金礦數量爲350,所需工人爲3人)
最優子結構與最終問題之間的關係:5個金礦10個工人的最優選擇,就是上述兩個最優子結構的最大值。
因而咱們能夠獲得狀態轉移方程:
最重要的狀態轉移方程已經獲得,至於剩下的邊界條件,現實中會遇到的各類特殊狀況,這裏就不贅述了。細節參考漫畫。
3.2.1 解法一、遞歸
程序 :把狀態轉移方程翻譯成遞歸程序,遞歸的結束條件就是方程式中的邊界便可。
複雜度:由於每一個狀態有兩個最優子結構,因此遞歸的執行流程相似於一個高度爲N的二叉樹。因此方法的時間複雜度是O(2N)。
3.2.2 解法二、備忘錄算法
程序:在簡單遞歸的基礎上,增長一個HashMap備忘錄,用來存儲中間的結果,HashMap的Key是一個包含金礦數N和工人數W的對象,Value是最優選擇得到的黃金數。
複雜度:時間複雜度和空間複雜度相同,都等於被網絡中不一樣Key的數量。
3.2.3 解法三、動態規劃
爲了實現自底向上的迭代,對於參數有兩個的問題,咱們能夠先畫要一個表格來作分析。根據狀態轉移方程,咱們能夠方便的畫出表格。注意,必定是要根據狀態轉移方程來求的。
因爲咱們在求解每一個格子的數值時,結合狀態轉移方程,發現除了第一行之外,每個格子均可以由前一行的格子中的一個或者兩個格子推導而來。
從總體上來講,每一行的值均可以由前一行來求得。
因而,咱們在寫代碼的時候,也能夠像畫表格同樣,從左至右,從上到下一個一個的推出最終結果。反映到程序上就是:
for i in range(金礦數): for j in range(工人數目): 狀態轉移方程
另外,由上可知,咱們並不須要存儲整個表格,只須要存儲前一行的結果便可推出新的一行。
代碼這裏就不寫了。
注意:
因此說,每一種算法沒有絕對的好與壞,關鍵要看應用場景。
我的以爲, 動態規劃算法最重要的有兩點
至於,模型中的邊界問題,特殊狀況等,就是須要多敲代碼來慢慢考慮的了。