動態規劃之二:揹包問題knapsack

揹包問題描述

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

類似的問題非常多:
比如每個任務都有時間和價值,我們有一定的時間,現在在有限的的時間裏完成最大價值的任務,如何安排?

任務 A B C D E F G
所得收益 7 9 5 12 14 6 12
需要時間 3 4 2 6 7 3 5

下面就以這個問題來進行分析。

分析

如果簡單的用 收益/時間來表示收益率,其實非常容易解決這個問題,然而任務是無法分解的。不能做三分之一就停止。
我們還是用揹包問題求解。

揹包問題的核心是定義目標和找到狀態轉移公式。

求解

定義目標:求最大收益。
v[i][j] 來表示最大收益,背景:j是揹包容量(時間),前i個物品的組合;
這個是關鍵定義。

然後我們找到相鄰兩步的關係(注意我們做任何事情都需要極度關注相鄰狀態,比如馬爾科夫鏈)
v[i][j] 和上一個i-1個物品組合有何關係?
i物品太大,j放不下,那麼 v[i][j]=v[i-1][j]
i物品可以放,那麼 max(v[i-1][j], v[i-1][j-s[i]] + v[i])就是最優的。

這一步是第二個關鍵,我們分解一下。

  1. 放得下,那麼放的話,則 v[i-1][j-s[i]]+v[i] 這就是放完的最優狀態
  2. 放得下,但是不放, 收益其實不變,還是上一個狀態的v[i-1][j]

那麼再反過來思考一下:
v[i][j]是最優的
v[i-1][j-si]是最優的,空間變小後,收益增加v[i], 那麼必須比較一下,到底是放進去帶來的總體更好,還是不放留給後面的更好?

方法一: 遞歸

def value(n, space):
    '''
    knapsack遞歸解法
    選擇第n個item的時候,到底是選擇做還是不做;選擇不做,空間不變,收益不變;
    如果選擇做,那麼space要減掉,收益增加v(n)
    :param n:
    :param space:
    :return:
    '''
    if n==0:
        return 0
    if item[n][1] >space:
        return value(n-1,space)
    return max(value(n-1,space), 
    value(n-1,space-item[n][1])+item[n][0])

方法二:遞歸記憶

方法三:直接數組

數組的方法也放在這裏,總體代碼如下,請忽略格式。

import numpy as np


item = {}
item[1] = [7,3]
item[2] = [9, 4]
item[3] = [5, 2]
item[4] = [12,6]
item[5] = [14, 7]
item[6] = [6, 3]
item[7] = [12, 5]

v = np.zeros([8,16])

def value(n, space):
    '''
    knapsack遞歸解法
    選擇第n個item的時候,到底是選擇做還是不做;選擇不做,空間不變,收益不變;
    如果選擇做,那麼space要減掉,收益增加v(n)
    :param n:
    :param space:
    :return:
    '''
    if n==0:
        return 0
    if item[n][1] >space:
        return value(n-1,space)
    return max(value(n-1,space), value(n-1,space-item[n][1])+item[n][0])

def value_dp_solution(n,s):
    '''
    從初始化到狀態轉移,到最終解。
    定義問題:value[i][j]代表面對任務i的時候,空間是j的最優收益結果。
    那麼狀態轉移則是:不選擇i,直接上一個狀態;
    選擇i,那麼value(i-1, space-w(i))+v(i), value(i-1,space)的最大值!
    仔細思考這個狀態轉移方法。
    :return:
    '''
    if n==0:
        return 0
    if v[n][s]!=0:
        return v[n][s]
    if item[n][1] > s:
        return value(n-1,s)
    result =  max(value_dp_solution(n-1,s), value_dp_solution(n-1,s-item[n][1])+item[n][0])
    v[n][s] = result

    return result
def find_solution(a,b):
    if a<1:
        return
    if b<=0:
        return
    if v[a][b]!=v[a-1][b]: #選擇了a元素
        print("choose:",a)
        find_solution(a-1,b-item[a][1])
    else:
        print("nochoose:",a)
        find_solution(a-1,b)

def dp_array():
    '''
    問題的核心在哪裏:每次求解目標值,都需要上一步的i的值。所以v[i][0:max]先求出即可,這是最關鍵的步驟。
    :return:
    '''
    v = np.zeros([8,16])
    for i in range(1,8):
        for j in range(1,16):
            if j<item[i][1]:
                v[i][j] = v[i-1][j]
            else:
                v[i][j] = max(v[i - 1][j], v[i - 1][j - item[i][1]] + item[i][0]);
        print(i, v)

    print(v)
# for i in range(0,8):
#     print(i,value(i,15))
value_dp_solution(7,15)
print(v)
find_solution(7,15)

dp_array()



上述問題的答案

總的收益:34
選擇ABEF

總結

  1. 核心是定義目標函數
  2. 找到遞歸和轉移公式
  3. 利用數組和記憶更快

參考文獻

https://www.cs.cmu.edu/~avrim/451f09/lectures/lect1001.pdf

發佈了59 篇原創文章 · 獲贊 77 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章