动态规划算法-02矿工挖矿问题
礦工挖礦問題
礦工挖礦
問題描述
該問題為了解決在給定金礦和礦工數量的前提下,能夠獲得最多黃金的挖礦策略。很多算法題其實就是這個問題換了一個情境。
有5個金礦,每個金礦黃金儲量不同,需要參與挖掘的工人數目也不同,假定有10個工人,每個金礦要么全挖,要么不挖,不可以拆分,試問,想得到最多的黃金,應該選擇挖取哪幾個金礦。金礦信息如下表。
| 1 | 400 | 5 |
| 2 | 500 | 5 |
| 3 | 200 | 3 |
| 4 | 300 | 4 |
| 5 | 350 | 3 |
題解思路
如果要使用動態規劃解題,就要確定動態規劃的三要素:最優子結構、邊界和狀態轉移函數。
最優子結構
解題目標是確定10個工人挖5個金礦能夠獲得最多的黃金量,該問題可以從10個工人挖4個金礦的子問題中遞歸求解。而我們在解決10個工人挖4個金礦時,存在兩種選擇,一種是放棄第5個礦,將10個工人投入到挖四個礦中;另一種選擇是決定對第5個礦進行挖掘,因此需要從這10個人中抽取3個人(第5個礦需要人數)加入第5個礦開采,剩余的人處理前4個礦。因此,最優解應該是這兩種選擇中獲得黃金數量較多的那一種。
為了描述方便,設金礦數量為nnn,工人個數為www,第n個礦的黃金數量為G[n?1]G[n-1]G[n?1],第n個礦所需工人為P(n)P(n)P(n),要想獲得10個礦工挖掘5個金礦的最優解為max(F(4,10),F(4,10?P[4])+G[4])max(F(4,10),F(4,10-P[4])+G[4])max(F(4,10),F(4,10?P[4])+G[4])。這就是最優子結構。
邊界
對于第一個金礦而言,若當前的礦工數量不能達到金礦的挖掘需要則只能放棄這個礦,獲得的黃金數量為0;若能滿足礦工數量要求,獲得的黃金數量為G[0]G[0]G[0]。
因此,邊界可以描述為
- n=1,w>=P[0]n=1,w>=P[0]n=1,w>=P[0]時,F(n,w)=G[0]F(n,w)=G[0]F(n,w)=G[0]
- n=1,w<P[0]n=1,w<P[0]n=1,w<P[0]時,F(n,w)=0F(n,w)=0F(n,w)=0
狀態轉移函數
F(n,w)={0(n<=1,w<P[0])G[0](n==1,w>=P[0])F(n?1,w)(n>1,w<P[n?1])max(F(n?1,w),F(n?1,w?P[n?1])+G[n?1])(n>1,w>=P[n?1])F(n,w) = \left\{ \begin{aligned} & 0 &(n<=1,w<P[0]) \\ & G[0] &(n==1, w>=P[0])\\ & F(n-1,w) & (n>1,w<P[n-1])\\ & max(F(n-1,w), F(n-1, w-P[n-1])+G[n-1]) &(n>1,w>=P[n-1]) \end{aligned} \right. F(n,w)=?????????????0G[0]F(n?1,w)max(F(n?1,w),F(n?1,w?P[n?1])+G[n?1])?(n<=1,w<P[0])(n==1,w>=P[0])(n>1,w<P[n?1])(n>1,w>=P[n?1])?
到這里,三要素找到了,經過這個過程也明白了求解過程,由底向上計算,一步步推導可以得到結果,換句話說,n步的解其實就是第一步的結果推來的。
首先,我們可以使用遞歸的方法來寫,它是自頂而下的。
def gold_mining(n ,w, G, P):if n <= 1 and w < P[0]:return 0if n == 1 and w >= P[0]:return G[0]if n > 1 and w < P[n-1]:return gold_mining(n-1, w, G, P)if n > 1 and w >= P[n-1]:return max(gold_mining(n-1, w, G, P), gold_mining(n-1, w-P[n-1], G, P) + G[n-1])if __name__ == '__main__':G = [400, 500, 200, 300, 350]P = [5, 5, 3, 4, 3]n = 5w = 10print(gold_mining(n, w, G, P))這個解法可以求得最優解,會以樹形結構求解子問題,如下圖所示,不難發現,若金礦有nnn和,工人充足,復雜度即為O(2n)O(2^n)O(2n)。
優化思路
當金礦只有5個的時候,問題不是很復雜,但是當金礦數目增加的時候,這樣的復雜度是無法接受的,直觀上來看,這種遞歸之所以慢其實是因為進行了如下圖所示的重復計算。
事實上,我們可以通過維護DP Table的方式來存儲最優解從而以自下而上的方式求解本題,表格中的dp[i][j]就表示F(n,w)的值,因此我們可以得到如下的代碼。
上述代碼的時間復雜度和空間復雜度都是O(nw)O(nw)O(nw),這比遞歸的性能好了很多。
進一步優化
上面這段代碼其實時間上已經沒什么優化的空間了,空間上還可以進一步優化。我們不妨回顧DP表,可以發現,其實每一行的結果值依賴于上一行的10個結果。舉個例子,我們已4個金礦9個工人為例,F(4,9)F(4, 9)F(4,9)來源于F(3,5)F(3,5)F(3,5)和F(3,9)F(3,9)F(3,9),這兩個結果顯然在上一行已經計算了。
因此,其實無論金礦多少個,我們也只需要保存一行的數據,在下一行計算時,從右向左統計并更新數據即可。
優化后的代碼如下,可以發現,空間復雜度降低到了O(n)O(n)O(n),這就是本題的最優方法了。
def gold_mining(n ,w, G, P):dp = [0] * (w+1)for i in range(1, len(G)+1):for j in range(w, 0, -1):if j >= P[i-1]:dp[j] = max(dp[j], dp[j-P[i-1]] + G[i-1])return dp[w]if __name__ == '__main__':G = [400, 500, 200, 300, 350]P = [5, 5, 3, 4, 3]n = 5w = 10print(gold_mining(n, w, G, P))補充說明
具體代碼可以查看我的Github,歡迎Star或者Fork,參考書《你也能看得懂的Python算法書》,書中略微有一點不合理之處,做了修改。到這里,其實你已經體會到了動態規劃的簡約之美,當然,要注意Python是有遞歸深度限制的,如不是必要,建議使用循環控制。
總結
以上是生活随笔為你收集整理的动态规划算法-02矿工挖矿问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 动态规划算法-01爬楼梯问题
- 下一篇: 动态规划算法-03背包问题