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