1. 程式人生 > >dp基礎之揹包問題

dp基礎之揹包問題

問題一:有N個物品,重量為A[0]...A[N-1],有一個容量為M(M是一個正整數)。
問:最多能帶走多重的物品。
例:A = [2,3,5,7]
    M = 10
輸出:10(2,3,5)

問題分析:
要求N個物品能否拼出重量W(W = 0,1...M),需要知道前N-1個物品能否拼出重量W(W = 0,1...M)

考慮最後一個物品(A[N-1])放不放入進入揹包
    情況1:如果前N-1個物品能拼出重量W,則前N個物品必然也能拼出重量W
    情況2:如果前N-1個物品能拼出重量W-A[N-1],則前N個物品就能拼出重量W,加上物品A[N-1]即可

子問題:
設:f[i][w]表示物品前i個能拼出重量w(True/False)
        f[i][w] = f[i-1][w] or f[i-1][w-A[i-1]]
                  不放入A[i-1]    or 放入A[i-1]
初始條件:
         0個物品可以拼出重量0
         f[0][0] = True
         0個物品不能拼出大於0的任何重量
         f[0][1...M] = False


邊界情況:
         f[i][w-A[i-1]] w>=A[i-1]時使用

計算順序:
         初始化
         f[0][0..M]
         前1個物品能拼出:f[1][0]...f[1][M]
                             .
                             .
         前N個物品能拼出:f[N][0]...f[N][M]

時間複雜度:O(MN),空間複雜度O(MN),優化後可以達到O(M)

 

程式碼及註釋如下:

def backpack(A,M):
    N = len(A)
    if N == 0:
        return 0
    f = [[False for i in range(M+1)] for j in range(N+1)]
    #初始化,f[0][0] = True ;f[0][1...M] = False
    f[0][0] = True
    for i in range(1,N+1):
        
        for j in range(0,M+1):
            #f[i][w]表示物品前i個能拼出重量w(True/False)
            #f[i][w] = f[i-1][w] or f[i-1][w-A[i-1]](w>=A[i-1])
            f[i][j] = f[i-1][j]
            if j >= A[i-1] :
                f[i][j] = f[i-1][j] or f[i-1][j-A[i-1]]
    #返回前N個物品能拼出最大的重量,肯定不會超過M,因為最大就是M
    for j in range(M+1)[::-1]:
        if f[N][j]:
            return j
            
A = [2,3,7,5]
M = 10
print(backpack(A,M))
#結果:10

 

問題二:假設每個物品只有一個(每個物品只能用一次),問一共有多少種方式正好湊成重量Target?
例:
A = [1,2,3,3,7],Target = 7
輸出:2(1,3,3;7)

問題分析:
如果知道這N個物品有多少種方式拼出0...Target,也就得到了答案
確定狀態:需要N個物品有多少種方式拼出重量W(W = 0...Target)
最後一步:考慮第N個物品A[N-1](最後一個物品)是否進入揹包

    case1: 不進入,用前N-1個物品拼出W
    case2: 進入,前N-1個物品能拼出W-A[N-1],加上最後一個A[N-1],正好拼出W
    現在要求的是方式數,
    case1的方式數+case2的方式數 = 用前N個物品拼出W的方式數
轉移方程:設f[i][w]表示用前i個物品能拼出w的方式數
    f[i][w] = f[i-1][w]+f[i-1][w-A[i-1]]
初始條件:
    #0個物品有一種方式拼出0

    #0個物品不能拼出大於0的重量
    f[0][0] = 1
    f[0][1]...f[0][Target] = 0
 
邊界情況:
    f[i-1][w-A[i-1]]中,w>=A[i-1]
計算順序:
         初始化
         f[0][0..Target]
         前1個物品能拼出:f[1][0]...f[1][Target]
                             .
                             .
         前N個物品能拼出:f[N][0]...f[N][Target]
答案是f[N][Target]

時間複雜度:O(N*Target),空間複雜度O(N*Target),優化後可以達到O(Target)

程式碼及註釋如下:

 

def backpackII(A,Target):
    #優化空間
    N = len(A)
    if N == 0:
        return 0
    f = [0 for i in range(Target+1)]
    #初始化f[0] = 1,f[1...Target] = 0
    for i in range(Target+1):
        f[i] = 1 if i == 0 else 0
    
    for i in range(N+1):
        #優化空間門衛了不覆蓋掉有用的值,我們從後往前算j從Target到0
        #把原來的f[j]覆蓋掉,
         for j in range(Target+1)[::-1]:
                #f'[j] = [j]+f[j-A[i-1]]
                #其實除了j>=A[i-1]為了減少計算,還有A[i-1] <= j <= sum(A[0]+...A[i-1])
                if j >= A[i-1] and j <= sum(A[0:i]):
                    f[j] += f[j-A[i-1]]
    return f[Target]
A = [1,2,3,3,7]
Target = 7
print(backpackII(A,Target))
#結果:2

 

問題三:假設每個物品只有一個(每個物品可以用人任意次),問一共有多少種方式正好湊成重量Target?
例:
A = [1,2,4],Target = 4
輸出:6(1,1,1,1; 2,2,2; 4; 1,1,2;1,2,1;2,1,1)(順序不同也算不同)

問題分析:
所有正確組合中,總重量都是Target
在總重量是Target的時,最後一個物品重是k,則前面物品重為Target-k
k的取值無非是A[0]..A[N-1]中的一個
如果最後一個物品是A[i],則要求有多種組合拼成Target-A[i]

轉移方程:設f[i]表示有多少種方式拼出重量i
    f[i] = f[i-A[0]] + f[i-A[1] + ... + f[i-A[N-1]]
初始條件:
    有1種方式拼出重量0
    f[0] = 1
    若i<A[j],則對應的f[i-A[j]]不加入f[i]
 
計算順序:
         f[0],...,f[Target]
                             .
答案是f[Target]

時間複雜度:O(N*Target),空間複雜度O(Target)

 

程式碼及註釋如下:

 

def backpackIII(A,Target):
    f = [0 for i in range(Target+1)]
    f[0] = 1
    for i in range(1,Target+1):
        f[i] = 0
        for j in range(len(A)):
            if i >= A[j]:
                f[i] += f[i-A[j]]
    return f[Target]
A = [1,2,4]
Target = 4 
print(backpackIII(A,Target))
#答案:6

問題IV:如果要打印出問題III裡拼出arget重量的一種方式,如何解?

 

程式碼及註釋如下:

def printbackpackIV(A,Target):
    #打印出拼成Target的方式\
    f = [0 for i in range(Target+1)]
    f[0] = 1
    #pai[i]表示至少有一種方式拼成重量i,且最後一個物品是pai[i]
    pai = [-1 for i in range(Target+1)]
    for i in range(1,Target+1):
        f[i] = 0
        for j in range(len(A)):
            if i >= A[j]:
                f[i] += f[i-A[j]]
                #至少有一種方式拼出重量i
                if f[i-A[j]] >=1 :
                    #pai[i]表示pai[i]表示至少有一種方式拼成重量i,且最後一個物品是pai[i]
                    #如果至少有一種方式能夠拼出重量i且最後一個物品是A[j],就是pai[i] = A[j]
                    pai[i] = A[j]
    if f[Target] >= 1:
        #要拼出重量Target
        i = Target
        print(i)
        while i != 0 and pai[i] != -1:
            #表示至少有一種方式拼成重量i,且最後一個物品是pai[i]
            print(pai[i])
            #現在的重量是i,且最後一個物品是物品pai[i],
            #去掉最後一個物品pai[i],之前的重量是i-pai[i],即減去最後一個物品的重量
            i = i-pai[i]
    return f[Target]
B = [5,7,13,17]
Target = 32
printbackpackIV(B,Target)
#
32
17
5
5
5
Out[30]:
22

 

問題五:N個物品重量為A[0]...A[N-1],價值分別V[0]...V[N-1],有一個容量為M的揹包,問最多能帶走多大價值的物品?
例:
A = [2,3,5,7],V = [1,5,2,4],M = 11
輸出:9(物品1和物品3,5+4 = 9,重量 為2+7=10 < 11)

問題分析:和前面的題目類似,需要知道N個物品:
    能否拼出重量W(W = 0...M)
    對於每個重量W,最大總價值是多少
考慮最後一步:最後一個物品(物品A[N-1],價值V[N-1])能否進入揹包:
    情況1:不進入,前N-1個物品能拼出重量W,最大總價值V,則前N個物品必然也能拼出重量W且最大總價值為V
    情況2:進入,前N-1個物品能拼出重量W-A[N-1],最大總價值V,則前N個物品(即加入物品A[N-1])拼出重量W且最大總價值為V+V[N-1]
子問題:要求前N個物品能不能拼出重量W(W = 0...M),以及拼出重W時的最大總價值
        則需要知道前N-1個物品能不能拼出重量W(W = 0...M),以及重量W此時的最大總價值
        
設:f[i][w]表示前i個物品拼出重量w時的最大總價值(我們用-1表示不能拼出)(跟問題一有點類似)

    f[i][w] = max{f[i-1][w] ,  f[i-1][w-A[i-1]] + V[i-1] ( w >= A[i-1] && f[i-1][w-A[i-1]] !=- 1 ) }
              max{不進入,前N-1個物品能拼出重量W,最大總價值V ;進入,前N-1個物品能拼出重量W-A[N-1],最大總價值V,則前N個物品(即加入物品A[N-1])拼出重量W且最大總價值為V+V[N-1] }(w> =A[i-1](能夠拼出)且 f[i-1][w-A[i-1]] !=- 1(所拼出的重量w>=A[i-1],))

初始條件:
    f[0][0]...f[N][0] = 0,前0...N個物品能拼出重量0,且最大價值是0。
    f[0][1]...f[0][M] = -1,前1個物品不能拼出重量大於0,我們用-1表示。

計算順序:
    f[0][0]...f[0][M]
    .
    .
    .
    f[N][0]...f[N][M]

答案:max(f[N][w] and f[N][w] != -1(w = 0...M))

時間複雜度O(N*W),空間複雜度O(N*W),優化後達到O(M)

 

詳細解釋一下空間優化的過程,轉移方程如下:
     f[i][w] = max{f[i-1][w] ,  f[i-1][w-A[i-1]] + V[i-1]}
     
本來計算
    f[0][0]...f[0][M]
    f[1][0]...f[0][M]
    .
    .
    .
    f[i-1][0]...f[i-1][M]
    f[i][0]...f[i][M]
    .
    .
    .
    f[N][0]...f[N][M]
計算
    f[i][w] = max{f[i-1][w] ,  f[i-1][w-A[i-1]] + V[i-1]}時,我們讓w按照M...0的順序,
    即先算f[i][M],再算f[i][M-1]...f[i][0]發現算完f[i][M]時,上一行的f[i-1][M]之後就不用了,
    所以,我們直接把f[i][M]存放在f[i-1][M],即覆蓋上一行(i-1行)的同一列(M列)的位置f[i-1][M],
    程式中可以這樣:新的f[w]覆蓋舊(上一行)的f[w]
        f[w] = max(f[w],f[w-A[i-1]] + V[i-1])
    因為計算f[i][M-1]時不會用到f[i-1][M],只會用到f[i-1][M]前面的數。
    所以只要用一個一維陣列即可,空間複雜度優化到O(M),關鍵點在於我們計算f[w] = max(f[w],f[w-A[i-1]] + V[i-1])時,讓w從後往前(從M...0)計算即可。

程式碼及註釋如下:

 

def backpackV(A,V,M):
    #優化空間後的程式碼,用一維陣列f[w]代替二維陣列f[i][w]
    N = len(A)
    if N == 0:
        return 0
    #初始化,f[0] = 0,f[1]...f[M] = -1
    f = [-1 for i in range(M+1)]
    f[0] = 0
    
    for i in range(1,N+1):
        for  w in range(M+1)[::-1]:
            #f[i][w] = max{f[i-1][w] , f[i-1][w-A[i-1]] + V[i-1] ( w >= A[i-1] && f[i-1][w-A[i-1]] != -1 ) }
            if w >= A[i-1] and f[w-A[i-1]] != -1:
                f[w] = max(f[w],f[w-A[i-1]] + V[i-1])
    #return max(f if max(f) != -1
    #其實沒必要,因為至少會存在f[0] = 0。故直接返回max(f)
    return max(f)
A = [2,3,5,7]
V = [1,5,2,4]
M = 11
print(backpackV(A,V,M))
#答案:9

 

問題六:N種物品,每種物品的重量是A[0]...A[N-1],價值分別V[0]...V[N-1],有一個容量為M的揹包,
這裡每個物品可以有任意個,問最多能帶走多大價值的物品?
例:
A = [2,3,5,7],V = [1,5,2,4],M = 11
輸出:15(3個物品1,價值3*5 = 15,重量3*3=9 < 11)


問題分析:和問題四不同點在於,A[i]可以有任意個:
因此設:f[i][w]表示前i種物品拼出重量w時的最大總價值(我們用-1表示不能拼出重量w)

    f[i][w] = max{f[i-1][w] , f[i-1][w-kA[i-1]] + kV[i-1]}(k>=0)
    #如果上面 k = 1 就和問題四一樣
f[i][w] = max{f[i-1][w] , f[i-1][w-1*A[i-1]]+1*V[i-1] , f[i-1][w-2*A[i-1]]+2*V[i-1], ...}

但仔細觀察,發現max{f[i-1][w-1*A[i-1]]+1*V[i-1] , f[i-1][w-2*A[i-1]]+2*V[i-1], ...} = f[i][w-A[i-1]]
因此:    
    f[i][w] = max{f[i-1][w],f[i][w-A[i-1]]+V[i-1]}
    
初始條件:

    f[0][0]...f[N][0] = 0,前0...N種物品能拼出重量0,且最大價值是0。
    f[0][1]...f[0][M] = -1,前1種物品不能拼出重量大於0,我們用-1表示。

計算順序:
    f[0][0]...f[0][M]
    .
    .
    .
    f[N][0]...f[N][M]

答案:max(f[N][w] and f[N][w] != -1(w = 0...M))

時間複雜度O(N*W),空間複雜度O(N*W),優化後達到O(M)


這裡也詳細解釋一下空間優化:我們的轉移方程是
    f[i][w] = max{f[i-1][w],f[i][w-A[i-1]]+V[i-1]}
    同樣,跟問題四里的空間優化一樣,考慮:
本來計算
    f[0][0]...f[0][M]
    f[1][0]...f[0][M]
    .
    .
    .
    f[i-1][0]...f[i-1][M]
    f[i][0]...f[i][M]
    .
    .
    .
    f[N][0]...f[N][M]

   f[i][w] = max{f[i-1][w],f[i][w-A[i-1]]+V[i-1]}
 對於第i行和第i-1行,當計算f[i][M]時,只跟前一行(i-1行)同一列(M列)f[i-1][M]和同一行(i行)前面的w-A[i-1]列f[i][w-A[i-1]]有關。
如果我們還是跟問題四一樣從後往前計算,則需要兩行來記錄i-1行和i行,不能壓縮到1行。因為計算f[i][M]跟i行和i-1行都有關係,當從後往前計算時,不能直接覆蓋。那如果我們從前往後計算呢?
例如:我們計算f[i][0],f[i][1]...f[i][M],隨便幾個例子,我們計算f[i][6]
    f[i][6] = max(f[i-1][6],f[i][6-A[i-1]]+V[i-1])
    也就是說,計算完f[i][6]時,i-1行的6列位置f[i-1][6]這個數後面不會再用到了,
    故我們可以用f[i][6]直接覆蓋f[i-1][6],因此也可以只要開一個以為陣列,只不過計算順序從0...到M,即可,程式碼只要在問題四的程式碼上把w的迴圈順序(M...0)改成(0...M)即可。

程式碼及註釋如下:

def backpackV(A,V,M):
    #優化空間後的程式碼,用一維陣列f[w]代替二維陣列f[i][w]
    N = len(A)
    if N == 0:
        return 0
    #初始化,f[0] = 0,f[1]...f[M] = -1
    f = [-1 for i in range(M+1)]
    f[0] = 0
    
    for i in range(1,N+1):
        ############修改w迴圈順序即可###########
        for  w in range(M+1):
            #f[i][w] = max{f[i-1][w] , f[i-1][w-A[i-1]] + V[i-1] ( w >= A[i-1] && f[i-1][w-A[i-1]] != -1 ) }
            if w >= A[i-1] and f[w-A[i-1]] != -1:
                f[w] = max(f[w],f[w-A[i-1]] + V[i-1])
    #return max(f if max(f) != -1
    #其實沒必要,因為至少會存在f[0] = 0。故直接返回max(f)
    return max(f)
A = [2,3,5,7]
V = [1,5,2,4]
M = 10
print(backpackV(A,V,M))
#答案:15