问题一:有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