逐步解決動態(tài)規(guī)劃之01背包問題

[站外圖片上傳中...(image-fe2902-1583294261806)]

什么是動態(tài)規(guī)劃?

動態(tài)規(guī)劃(Dynamic programming,簡稱DP),是一種通過“大而化小”的思路解決問題的算法。

動態(tài)規(guī)劃沒有明確的算法模板,準(zhǔn)確的說,它是一種思想。動態(tài)規(guī)劃是一種解決問題的思想。

什么樣的題目適用于動態(tài)規(guī)劃

  1. 求最大值 / 最小值
  2. 求可不可行
  3. 求方案總數(shù)

以上三種問題基本上都是用動態(tài)規(guī)劃來求解。

注意:如果問題是讓你求出“所有的”方案和結(jié)果,則肯定不是使用動態(tài)規(guī)劃。

動態(tài)規(guī)劃問題的解決過程

主要以3個子目標(biāo)來攻克此類問題:

  1. 建立狀態(tài)轉(zhuǎn)移方程。
  2. 緩存并復(fù)用以往結(jié)果。
  3. 按順序從小往大算。

舉個簡單的例子,1個人有2條腿,2個人有4條腿,...,100 個人有多少條腿?

首先要建立狀態(tài)轉(zhuǎn)移方程: f(n) = 2n

這個太簡單了不用緩存復(fù)用,直接計(jì)算即可。

嘴上說還是不夠的,我們用實(shí)際的問題從淺到深,逐步來深入了解動態(tài)規(guī)劃問題。

[Talk is cheap. Show me the code]

一、斐波那契數(shù)列

  • 斐波那契數(shù)列:0,1,1,2,3,5,8,13,21,34,55,89,144...

  • 圖形如下:

[圖片上傳失敗...(image-cea13e-1583294261806)]

  • 斐波那契數(shù)列規(guī)律計(jì)算公式如下:

[圖片上傳失敗...(image-208a66-1583294261806)]

  1. 先看看暴力遞歸方法:
def fib(n):
    if n < 2:
        return n
    else:
        return fib(n - 1) + fib(n - 2)


if __name__ == "__main__":
    result = fib(100)
    print(result) # 別等了 根本執(zhí)行不出來

簡單遞歸的時間復(fù)雜度達(dá)到了O(n2)。 因?yàn)槊看味家M(jìn)行重復(fù)計(jì)算。

f(3) = f(2) + f(1)

f(4) = f(3) + f(2)

可以看出,f(2) 計(jì)算了兩次,計(jì)算的n越大,循環(huán)的次數(shù)越多。

  1. 動態(tài)規(guī)劃

    In [20]: def fib(n):
        ...:     result = list(range(n+1)) # 用于緩存以往結(jié)果,以便復(fù)用(目標(biāo)2)
        ...:     for i in range(n + 1):   # 按順序從小往大算(目標(biāo)3)
        ...:         if i < 2:
        ...:             result[i] = i
        ...:         else:
                      # 使用狀態(tài)轉(zhuǎn)移方程(目標(biāo)1),同時復(fù)用以往結(jié)果(目標(biāo)2)
        ...:             result[i] = result[i - 1] + result[i - 2]
        ...:     return result[-1] # 輸出列表中最后一個數(shù)值,也就是fib(n)的值
        ...:
    
    In [21]: result = fib(100)
    
    In [22]: result
    Out[22]: 354224848179261915075L
    
  1. (目標(biāo)1)狀態(tài)轉(zhuǎn)移方程是什么: f(n) = f(n-1) + f(n-2)
  2. (目標(biāo)2)緩存并復(fù)用以往的結(jié)果。在 result[i] = result[i - 1] +result[i - 2]中進(jìn)行了復(fù)用。相當(dāng)于你在計(jì)算了 + 1 + 1 + 1 + 1 + 1 + 1 + 1= 7 之后,再 + 1 你可以很快的計(jì)算出是 8 ,不必再重新將 8 個 1 相加。
  3. (目標(biāo)3)從小到大計(jì)算。這樣以來,時間復(fù)雜度降為O(n)。我們只需要計(jì)算紅圈圈出的部分即可。節(jié)省了大量的時間。
1583152807162.png

二、不同路徑問題

參照LeetCode上的62. 不同路徑

一個機(jī)器人位于一個 m x n 網(wǎng)格的左上角 (起始點(diǎn)在下圖中標(biāo)記為“Start” )。

機(jī)器人每次只能向下或者向右移動一步。機(jī)器人試圖達(dá)到網(wǎng)格的右下角(在下圖中標(biāo)記為“Finish”)。

問總共有多少條不同的路徑?

上圖是一個7 x 3 的網(wǎng)格。有多少可能的路徑?

太多了,我簡化一個3 X 2 的。

解這道題的時候,我們也要從三個目標(biāo)開始

  1. (目標(biāo)1)狀態(tài)轉(zhuǎn)移方程是什么:

    這道題你必須要知道轉(zhuǎn)移方程是什么。不然的話下面都不用做了...

    f(i, j) = f(i-1, j) + f(i, j - 1)

  2. (目標(biāo)2)緩存并復(fù)用以往的結(jié)果。這里是一個二維的行列問題。我在這里用二維的數(shù)組解決這個問題。

  3. (目標(biāo)3)同樣也是從小到大計(jì)算。我們需要雙重循環(huán),逐行逐列進(jìn)行計(jì)算。

# encoding:utf8
class Solution(object):
    def uniquePaths(self, m, n):
        """
        :type m: int m為行數(shù)
        :type n: int n為列數(shù)
        :rtype: int
        """
        result = [[1] * n] * m
        for i in range(1, m):
            for j in range(1, n):
                result[i][j] = result[i][j - 1] + result[i - 1][j]
        return result[-1][-1]


if __name__ == '__main__':
    s = Solution()
    print s.uniquePaths(3, 7)

輸出結(jié)果為28,證明我們的計(jì)算方法是正確的。

三、01背包問題

有n個物品,它們有各自的體積和價值,現(xiàn)有給定容量的背包,如何讓背包里裝入的物品具有最大的價值總和?

為方便講解和理解,下面講述的例子均先用具體的數(shù)字代入,即:eg:number=4,capacity=6

i(物品編號) 1 2 3 4
w(體積) 2 3 4 5
v(價值) 3 4 5 6

背包問題(0—1背包)

有N件物品,背包的最大承重為M,每件物品的數(shù)量均為1,價值集合為V,重量集合為W,計(jì)算該背包可以承載的物品的最大價值。
動態(tài)規(guī)劃思想:

  • 動態(tài)規(guī)劃解題步驟:問題抽象化、建立模型、尋找約束條件、判斷是否滿足最優(yōu)性原理、找大問題與小問題的遞推關(guān)系式、填表、尋找解組成

  • 動態(tài)規(guī)劃的原理

    • 動態(tài)規(guī)劃與分治法類似,都是把大問題拆分成小問題,通過尋找大問題與小問題的遞推關(guān)系,解決一個個小問題,最終達(dá)到解決原問題的效果。
    • 但不同的是:
      • 分治法在子問題和子子問題等上被重復(fù)計(jì)算了很多次,
      • 而動態(tài)規(guī)劃則具有記憶性,通過填寫表把所有已經(jīng)解決的子問題答案紀(jì)錄下來,在新問題里需要用到的子問題可以直接提取,避免了重復(fù)計(jì)算,從而節(jié)約了時間,所以在問題滿足最優(yōu)性原理之后,用動態(tài)規(guī)劃解決問題的核心就在于填表,表填寫完畢,最優(yōu)解也就找到。
  • 背包問題的解決過程
    在解決問題之前,為描述方便,首先定義一些變量:Vi表示第i個物品的價值,Wi表示第i個物品的體積,定義V(i,j):當(dāng)前背包容量j,前i個物品最佳組合對應(yīng)的價值,同時背包問題抽象化(X1X2,…,Xn,其中Xi 取0或1,表示第i 個物品選或不選)。

    此處要注意各個變量代表的含義,尤其是V(i,j):當(dāng)前背包容量j,前i個物品最佳組合對應(yīng)的價值。

1、建立模型,即求max(V1X1+V2X2+…+VnXn)

2、尋找約束條件,W1X1+W2X2+…+WnXn < capacity

3、尋找遞推關(guān)系式,面對當(dāng)前商品有兩種可能性:

圖示:

capacity 0 1 2 3 4 5 6
0 weight value 0 0 0 0 0 0 0
1 2 3 0 0 3 3 3 3 3
2 3 4 0 0 3 4 4 7 7
3 4 5 0 0 3 4 5 7 8
4 5 6 0 0 3 4 5 7 8
# encoding:utf8
class Solution(object):
    def bag01(self, weights, values, capacity):
        # 動態(tài)規(guī)劃
        n = len(weights)
        result = [[0 for j in range(capacity + 1)] for i in range(n + 1)]
        for i in range(1, n + 1):
            for j in range(1, capacity + 1):
                result[i][j] = result[i - 1][j]
                # 背包總?cè)萘繅蚍女?dāng)前物體,遍歷前一個狀態(tài)考慮是否置換
                if j >= weights[i - 1] and result[i][j] < result[i - 1][j - weights[i - 1]] + values[i - 1]:
                    result[i][j] = result[i - 1][j - weights[i - 1]] + values[i - 1]
        for x in result:
            print(x)
        return result

    def show(self, weights, capacity, result):
        n = len(weights)
        print ('最大解為:{}'.format(result[n][capacity]))
        x = [False for i in range(n + 1)]
        j = capacity
        for i in range(n, 0, -1):
            if result[i][j] > result[i - 1][j]:
                # i代表第i個物品,如果放入第i個物品的價值大于同等重量放入i-1物品的重量,說明選擇了物品i
                x[i] = True
                j -= weights[i - 1]
        print('items:')
        for i in range(n + 1):
            if x[i]:
                print('no:{}'.format(i))


if __name__ == '__main__':
    s = Solution()
    weights = [2, 2, 3, 1, 5, 2]
    values = [2, 3, 1, 5, 4, 3]
    capacity = 10
    result = s.bag01(weights, values, capacity)
    s.show(weights, capacity, result)

輸出結(jié)果:

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2]
[0, 0, 3, 3, 5, 5, 5, 5, 5, 5, 5]
[0, 0, 3, 3, 5, 5, 5, 6, 6, 6, 6]
[0, 5, 5, 8, 8, 10, 10, 10, 11, 11, 11]
[0, 5, 5, 8, 8, 10, 10, 10, 12, 12, 14]
[0, 5, 5, 8, 8, 11, 11, 13, 13, 13, 15]
最大解為:15
items:
no:2
no:4
no:5
no:6

Process finished with exit code 0

以上。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容