5年前寫過一篇關於動態規劃的博客。最近重新學習基本算法,對動態規劃有了更深入的體會。
對於“揹包問題”,現在可以按照自己的思路寫出來代碼了,雖然運行一下發現有問題,經過修改調試,最終可以完成。這樣的過程,我認爲比記住一些代碼,然後一遍把代碼寫對,是要好很多了。
要把動態規劃的代碼寫出來要經過幾個步驟。
遞歸算法的思考方式是自頂向下的,而動態規劃是自底向上的。
但是有了自頂向下的“記憶化搜索”解法之後,再來思考動態規劃的解法,會好很多。
下面用代碼來展示這個思路
0.問題描述
有n個物體,每個物體有自己的重量和價值。現在有一個容量爲 C 的揹包,要從n個物體中選一些來放到這個揹包裏,要使得這個揹包裏的物體價值最大。
例如有 3個 物體
編號爲 0, 1, 2。
重量爲 1, 2,3。
價值爲 6, 10,12.
揹包的容量爲 5.
那麼價值最大的解法爲 揹包裏放入 編號爲1,2的物體,總價值爲 10+12=22
1. 遞歸解法
要能寫出遞歸解法,就是考慮:大的問題,能不能通過規模小一點的子問題來解決,並且要有遞歸終止的條件,這就是自頂向下的思考方式。
對於這個問題,我們要從[0,…n-1]個物體中 選出能裝入 容量爲 C的揹包裏。
那麼我們考慮 揹包裏是否需要 放入 第 n-1 個物體 來作爲破題的入口。
(1)假設最終的解法裏, 揹包裏不需要第n-1個物體,那麼問題就變成了:
我們要從[0,…,n-2]個物體中 選出能裝入 容量爲 C的揹包裏。
(2)假設最終的解法裏,揹包裏需要放入第n-1個物體,那麼問題就轉變爲:
我們要從[0,…,n-2]個物體中 選出能裝入 容量爲:(C 減去 第n-1個物體的重量)的揹包裏。
比較(1)和(2)兩種情況下,揹包裏物體的價值誰最大就是問題的解法。
其實函數的定義, 就是 所謂的 “狀態定義”。函數定義裏的參數,就是問題的約束條件。
遞歸函數的實現體,就是 所謂的 “狀態轉移方程”。
class Solution:
# get the max value in [0,...,n-1] items
def max_value(self, weight_list, value_list, n, capacity):
if n <= 0 or capacity <= 0:
return 0
# 第(1)種情況的解法
res1 = self.max_value(weight_list, value_list, n-1, capacity)
# 第(2)中情況的解法
res2 = 0
if capacity >= weight_list[n-1]:
res2 = value_list[n-1] + self.max_value(weight_list, value_list, n-1, capacity-weight_list[n-1])
# 比較(1)和(2)誰最優
res = max(res1, res2)
return res
def backpack(self, weight_list, value_list, capacity):
n = len(weight_list)
return self.max_value(weight_list, value_list, n, capacity)
# main
s = Solution()
weight = [1, 2, 3]
value = [6, 10, 12]
print(s.backpack(weight, value, 5))
2. 記憶化搜索
記憶化搜索,其實就是使用變量把中間結果暫存起來,避免重複計算。
class Solution:
def __init__(self):
self.memo = list()
# get the max value in [0,...,n-1] items
def max_value(self, weight_list, value_list, n, capacity):
if n <= 0 or capacity <= 0:
return 0
# 記憶化搜索新加代碼
if self.memo[n][capacity] != 0:
return self.memo[n][capacity]
res1 = self.max_value(weight_list, value_list, n-1, capacity)
res2 = 0
if capacity >= weight_list[n-1]:
res2 = value_list[n-1] + self.max_value(weight_list, value_list, n-1, capacity-weight_list[n-1])
res = max(res1, res2)
# 記憶化搜索新加代碼
self.memo[n][capacity] = res
return res
def backpack(self, weight_list, value_list, capacity):
n = len(weight_list)
self.memo = [[0 for j in range(capacity+1)] for i in range(n+1)]
return self.max_value(weight_list, value_list, n, capacity)
# main
s = Solution()
weight = [1, 2, 3]
value = [6, 10, 12]
print(s.backpack(weight, value, 5))
3. 動態規劃解法
有了前面自頂向下的 記憶化搜索算法,反過來自底向上的思考動態規劃的解法。
def backpack(weight_list, value_list, capacity):
n = len(weight_list)
memo = [[0 for j in range(capacity + 1)] for i in range(n+1)]
for j in range(capacity + 1):
memo[0][j] = value_list[0] if weight_list[0] <= j else 0
for i in range(1, n):
for c in range(0, capacity+1):
remain = 0
if c >= weight_list[i]:
remain = memo[i-1][c-weight_list[i]] if (c-weight_list[i]) >= 0 else 0
remain = value_list[i] + remain
memo[i][c] = max(memo[i-1][c], remain)
return memo[n-1][capacity]
# main
weight = [1, 2, 3]
value = [6, 10, 12]
print(backpack(weight, value, 5))
致謝
感謝慕課網上《玩轉算法面試-- Leetcode真題分門別類講解》這門課程的老師liuyubobobo,本文的思路就是從這門課裏學到的。這門課不僅教會了我具體問題的解法,而且指出了思考的路徑,做到了授人以漁。
應用
在下一篇文章(算法-動態規劃3)將應用這個思路來解決一個組合問題。