1. 程式人生 > >一文弄懂動態規劃(DP Dynamic Programming)下樓梯,國王和金礦,揹包問題,Dijkstra演算法

一文弄懂動態規劃(DP Dynamic Programming)下樓梯,國王和金礦,揹包問題,Dijkstra演算法

動態規劃

參考連結

漫畫演算法,什麼是動態規劃?

DP

動態規劃是一種分階段求解決策問題的數學思想

題目一

問:下樓梯問題,有一座高度是10級臺階的樓梯,從下往上走,每跨一步只能向上1級或者2級臺階,請問有多少中走法。

思路

剛才這個題目,你每走一步就有兩種走法,暫時不管0級到8級臺階的過程。想要走到10級,必然是從8級或者9級走的。那麼問題來了,如果我們以及0到9級臺階的走法有x種,0到8級臺階有Y種,那0到10級臺階就是X+Y。

公式就是:
F(10) = F(9)+ F(8)

當只有1級臺階的時候只有一種解法,2級臺階的時候有兩種方法。

遞推公式就是:
F(1) = 1
F(2) = 2
F(N) = F(N-1)+ F(N-2)

動態規劃的三個概念:

  1. 最優子結構
  2. 邊界
  3. 狀態轉移公式


當只有1級臺階或2級臺階,我們直接得出結果,無需建華,我們就成F(1)F(2)為邊界。
F(N) = F(N-1)+ F(N-2)是階段與階段之間的狀態轉移公式。

求解問題

方法一:遞迴法


但是複雜度很高,因為當中有很多大量的重複計算。複雜度 O ( 2 N

) O(2^N) 。具體分析見開頭的連結。
遇到這種情況,我們只需要建立一個雜湊表,在python種建立一個字典就好了。把每次不同引數的計算結果存入雜湊,當遇到相同引數時,再從雜湊表裡面取出,就不會重複計算了。

方法二


感覺紅色箭頭少了個引數。

在以上程式碼中,集合map是一個備忘錄。當每次需要計算F(N)的時候,會首先從map中尋找匹配元素。如果map中存在,就直接返回結果,如果map中不存在,就計算出結果,存入備忘錄中


其實不用對F(N)自頂向下做遞迴運算,可以從下往上算。已知1,2是不是就能求3了。以此類推。

題目二

國王和金礦
問:有一個國家發現了5座金礦,每座金礦的黃金儲量不同,需要參與挖掘的工人數也不同。參與挖礦工人的總數是10人。每座金礦要麼全挖,要麼不挖,不能派出一半人挖取一半金礦。要求用程式求解出,要想得到儘可能多的黃金,應該選擇挖取哪幾座金礦?

解答


最優子結構:
10個人4金礦(第五金礦不挖的時候),10減去挖第五金礦的人數要求然後剩下4金礦(第五金礦挖的時候)。
最終問題:
10個人5金礦的最優選擇。
最優子結構和最終問題的關係:
設幾個變數便於描述:
N:金礦數量
W:工人人數
G[]:金礦黃金含量
P[]:金礦的用工量
關係:
F(5,10) = Max(F(4,10),F(4,10-P[4])+ G [4])
==> F(N,W) = Max(F(N-1,W),F(N-1,W-P[N-1])+G[N-1])

邊界條件:

if N == 1:
	if W>= P[0]:
		return G[0]
	else:
		return 0

總結:

和之前一樣,有三種實現方式,簡單遞迴,備忘錄演算法,動態規劃。

方法二:簡單遞迴

把狀態轉移方程式翻譯成遞迴程式,遞迴的結束的條件就是方程式當中的邊界。因為每個狀態有兩個最優子結構,所以遞迴的執行流程類似於一顆高度為N的二叉樹。方法的時間複雜度是 O ( 2 N ) O(2^N)

方法三:備忘錄演算法

在簡單遞迴的基礎上增加一個HashMap備忘錄,用來儲存中間結果。HashMap的Key是一個包含金礦數N和工人數W的物件,Value是最優選擇獲得的黃金數。方法的時間複雜度和空間複雜度相同,都等同於備忘錄中不同Key的數量。

方法四:動態規劃

在這裡插入圖片描述



規律




在這裡插入圖片描述
However ,當總工人數變成1000人,每個金礦的用工數也相應增加,這時候如何實現最優選擇呢?

可能你覺得還是之前的動態規劃方法。其實是不對的,我們可以來計算一下,
1000工人5個金礦,需要計算1000 * 5 次,需要開闢 1000 單位的空間。
然而我們用之前的簡單遞迴,需要計算2^n次也就是32次,只需要開闢5單位(遞迴深度的空間)。
所以從上面計算可以知道,動態規劃方法的時間和空間都和W成正比,而簡單遞迴卻與W無關,當工人數量很多的時候,動態規劃反而不如遞迴。
(我又一個想法,思路來源於Glibc 中的 qsort() 的實現在這個連結的舉例分析排序函式板塊中,我的思路是這樣的,兩個演算法都可以寫在函式實現上,如果當N特別大的時候,可以選擇動態規劃的方法,而當N不大,而W特別大的時候,且空間有限制,此時就可以讓演算法退化成簡單遞迴。不知道對不對這個思路,如果哪裡考慮的不對的話,請告訴我,萬分感謝)

以上就是漫畫演算法的全部內容。以下是我的補充內容

揹包問題 和迪杰特斯拉(Dijkstra演算法–求圖最短路徑)

揹包問題


如果認真讀了上面的過程,看到這個題目是不是覺得和前面礦工和金礦很像是不是。揹包問題就是,錢和重量,而前面礦工和金礦問題是,錢和人數的限制。
接下來的思路是演算法圖解中關於動態規劃的講解,可以參考看一下。


























寫出正確的動態規劃



在這裡插入圖片描述








Dijkstra’s Algorithm(是求從一點出發的最短路徑)

虛擬碼

Data: G, s, dist[], pred[] and
vSet: set of vertices whose shortest path from s is unknown
Algorithm:
dist[] // array of cost of shortest path from s
pred[] // array of predecessor in shortest path from s
dijkstraSSSP(G,source):
| 	Input graph G, source node
|
|	 initialise dist[] to all ∞, except dist[source]=0
|	 initialise pred[] to all -1
| 	vSet=all vertices of G
| 	while vSet is not NULL do
|	 | 	find s in vSet with minimum dist[s]
|	 | 	for each (s,t,w) in edges(G) do
| 	 | 	relax along (s,t,w)
|	 | 	end for
| 	 | 	vSet=vSet \ {s}
|	 end while

以上虛擬碼僅供參考作用,喜歡的話,可以研究一下。下面通過一道題目來了解整個過程。

  1. 根據虛擬碼進行初始化:
  2. 根據題目的要求,從node 0開始,遍歷與0相連的每條邊的權重,更新列表,pred代表是從哪裡來的,比如,第二列的0,代表從0來到1。(圖中綠色的代表當前遍歷的點)
  3. 然後遍歷dist中除去0以外的,最小的值,以這個最小值作為起始點,遍歷它連的邊,更新列表。
  4. 其實就是重複3的動作,先找剩下的最小邊,遍歷它連的邊,更新列表,你可以動手寫一下剩下的內容。當dist發生變化時,pred也要相應的發生變化,畢竟你要記錄最短路徑,當然要記錄這個路徑是從哪裡來的。

演算法複雜度分析

每條邊都需要遍歷一邊,O(E)
外迴圈是遍歷所有點,則是O(V)
“find s in vSet with minimum dist[s]” 這段程式碼
嘗試找s in vSet,cost為O(V)
==>overall cost 為 O E + V 2 = O V 2 O(E+V^2) = O(V^2)
如果用優先佇列來實現找最小距離,那麼
Overall cost = O E + V l o g V O(E+VlogV)