算法-動態規劃2

    5年前寫過一篇關於動態規劃的博客。最近重新學習基本算法,對動態規劃有了更深入的體會。
對於“揹包問題”,現在可以按照自己的思路寫出來代碼了,雖然運行一下發現有問題,經過修改調試,最終可以完成。這樣的過程,我認爲比記住一些代碼,然後一遍把代碼寫對,是要好很多了。

要把動態規劃的代碼寫出來要經過幾個步驟。

Created with Raphaël 2.2.0遞歸算法記憶化搜索優化動態規劃

遞歸算法的思考方式是自頂向下的,而動態規劃是自底向上的。
但是有了自頂向下的“記憶化搜索”解法之後,再來思考動態規劃的解法,會好很多。

下面用代碼來展示這個思路

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)將應用這個思路來解決一個組合問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章