首先介绍一下:什么是多重背包问题?背包问题?
给定 N = [ N 1 , N 2 , . . . , N N ] N=[N_1, N_2, ..., N_N] N=[N1,N2,...,NN]件物品以及每件物品对应的重量 W = [ W 1 , W 2 , . . . , W N ] W=[W_1, W_2, ..., W_N] W=[W1,W2,...,WN]和价值 V = [ V 1 , V 2 , . . . , V N ] V=[V_1, V_2, ..., V_N] V=[V1,V2,...,VN],求解将这些物品放入容量为 C C C的背包得到的最大价值为多少?
注意:每种物品仅有一件,多重背包问题此处定义不一致。
采用动态规划的思想求解该问题,定义子问题及其状态转移。
特点:每种物品仅有一件,可以选择放或者不放入背包。
用子问题定义状态: F ( i , c ) F(i,c) F(i,c)表示前 i i i件物品放入容量为 c c c的背包可以获得的最大价值。
选择将第 i i i件物品放入背包得到的价值:
F = F ( i − 1 , c − W i ) + V i F = F(i-1, c-W_i) + V_i F=F(i−1,c−Wi)+Vi
选择不将第 i i i件物品放入背包得到的价值:
F = F ( i − 1 , c ) F = F(i-1, c) F=F(i−1,c)
最后对两者求最大值即可:
F ( i , c ) = m a x ( F ( i − 1 , c ) , F ( i − 1 , c − W i ) + V i ) F(i, c) = max(F(i-1, c), F(i-1, c-W_i) + V_i) F(i,c)=max(F(i−1,c),F(i−1,c−Wi)+Vi)
时间复杂度: O ( N ∗ C ) O(N*C) O(N∗C)
空间复杂度: O ( N ∗ C ) O(N*C) O(N∗C)
优化空间复杂度
假设给定物品详情如下:
输入:物品件数:3,背包容量:5;
每件物品的重量: 1, 2, 3;
每件物品的价值:6, 10, 12.
(1)将 O ( N ∗ C ) O(N*C) O(N∗C)的空间复杂度优化到 O ( 2 ∗ C ) O(2*C) O(2∗C)的空间复杂度:
根据递推表达式,知道每次递推第 i i i的时候都只用到了第 i − 1 i-1 i−1项的值,那么能否只用两行来表示整个全部的递推过程呢?
针对上述例子,采用 O ( N ∗ C ) O(N*C) O(N∗C)的空间复杂度的递推状态结果如下:
物品件数/容量 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
i = 0 i=0 i=0 | 0 | 6 | 6 | 6 | 6 | 6 |
i = 1 i=1 i=1 | 0 | 6 | 10 | 16 | 16 | 16 |
i = 2 i=2 i=2 | 0 | 6 | 10 | 16 | 18 | 22 |
def zero_one_pack(weight, value, N, C):
assert len(weight) == len(value)
assert N >= 0
memo = []
for i in range(N):
memo.append([-1] * (C+1))
for i in range(C + 1):
memo[0][i] = value[0] if i >= weight[0] else 0
for i in range(1, N):
for j in range(C+1):
memo[i][j] = memo[i - 1][j]
if j >= weight[i]:
memo[i][j] = max(memo[i][j], memo[i-1][j - weight[i]] + value[i])
return memo[N-1][C]
采取两行的递推结果如下:
第一步:
物品件数/容量 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
i = 0 i=0 i=0 | 0 | 6 | 6 | 6 | 6 | 6 |
i = 1 i=1 i=1 | 0 | 6 | 10 | 16 | 16 | 16 |
第二步:
物品件数/容量 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
i = 2 i=2 i=2 | 0 | 6 | 10 | 16 | 18 | 22 |
i = 1 i=1 i=1 | 0 | 6 | 10 | 16 | 16 | 16 |
当物品数量较多的时候,依次类推,可以发现当 i i i为偶数的时候,更新值都位于第一行,当 i i i为奇数的时候,更新值都位于第二行。
def one_zero_pack_second(weight, value, N, C):
assert len(weight) == len(value)
assert N >= 0
memo = []
for i in range(2):
memo.append([-1] * (C + 1))
for i in range(C+1):
memo[0][i] = value[0] if i >= weight[0] else 0
n_index = 1
while n_index < N:
for j in range(C+1):
# n_index & 1 == 0 表示n_index为偶数,n_index & 1 == 1表示n_index为奇数
memo[n_index & 1][j] = memo[(n_index-1) & 1][j]
if j >= weight[n_index]:
memo[n_index & 1][j] = max(memo[n_index & 1][j], value[n_index] + memo[(n_index-1) & 1][j - weight[n_index]])
n_index += 1
return memo[(N-1) & 1][C]
(2)将 O ( 2 ∗ C ) O(2 *C) O(2∗C)的空间复杂度优化到 O ( C ) O(C) O(C)的空间复杂度
在递推 F ( i , c ) F(i, c) F(i,c)的时候只用到了 i − 1 i-1 i−1行的数据,然而真正用到的并不是 i − 1 i-1 i−1行的全部数据,而是 i − 1 i-1 i−1行的某一个值 F ( i − 1 , c ) F(i-1, c) F(i−1,c)或者 F ( i − 1 , c − W i ) F(i-1, c-W_i) F(i−1,c−Wi),能否在更新的时候,只对一行数据进行逆向更新呢,先更新 F ( i , C ) F(i, C) F(i,C),再更新 F ( i , C − 1 ) F(i, C-1) F(i,C−1),依次往前递推,前边值的更新并不需要用到后边的数据,而前向更新的时候,更新后边的值可能用到前边的数据.
构建递推表达式:
F ( c ) = m a x ( F ( c ) , F ( c − W i ) + V i ) F(c) = max(F(c), F(c-W_i)+V_i) F(c)=max(F(c),F(c−Wi)+Vi)
接着上边的例子:
第一步初始化:
物品件数/容量 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
i = 0 i=0 i=0 | 0 | 6 | 6 | 6 | 6 | 6 |
第二步更新:
物品件数/容量 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
i = 1 i=1 i=1 | 0 | 6 | 10 | 16 | 16 | 16 |
第三步更新:
物品件数/容量 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
i = 2 i=2 i=2 | 0 | 6 | 10 | 16 | 18 | 22 |
def one_zero_pack_third(weight, value, N, C):
assert len(weight) == len(value)
assert N >= 0
memo = []
for i in range(C+1):
memo.append(value[0] if i >= weight[0] else 0)
for i in range(1, N):
# 注意: 这个地方j从 C开始递推
for j in range(C, weight[i] - 1, -1):
memo[j] = max(memo[j], value[i] + memo[j - weight[i]])
return memo[C]
多重背包问题是在0-1背包问题的基础上进行升华的一个问题,0-1背包问题只限定每个物品只有一个,多重背包问题对于每个物品具有指定数目个,引入numbers变量, n u m b e r s = [ M 1 , M 2 , M 3 , . . . , M N ] numbers=[M_1, M_2, M_3, ..., M_N] numbers=[M1,M2,M3,...,MN]。
多重背包问题的求解方案:
将每个物品的多件,拆成多个单件物品,该问题变成了原来的0-1背包问题,时间复杂度为 O ( C ∗ ∑ i M i ) O(C *\sum_i M_i) O(C∗∑iMi);
将第 i i i件物品转换成若干件原始物品的和,使得原问题中的第 i i i种物品的可取策略为 [ 0 , 2 0 , 2 1 , . . . , 2 k − 1 , M i − 2 k + 1 ] [0, 2^0, 2^1, ..., 2^{k-1},M_i - 2^k + 1] [0,20,21,...,2k−1,Mi−2k+1], k k k是满足 M i − 2 k + 1 > 0 M_i - 2^k + 1>0 Mi−2k+1>0的最大的整数,例如:当 M i = 13 M_i=13 Mi=13时, k = 3 k=3 k=3,此时时间复杂度为: O ( C ∗ ∑ i l o g ( M i ) ) O(C * \sum_i log(M_i)) O(C∗∑ilog(Mi))
def multi_pack(weight, value, nums, N, C):
c_i = 0
len_capa = N
while c_i < len_capa:
# 将费用大于最大容量的物品剔除掉
if weight[c_i] > C:
weight.pop(c_i)
value.pop(c_i)
len_capa -= 1
continue
c_i += 1
new_capacity = []
new_value = []
for i in range(len(weight)):
k = int(math.log(nums[i] + 1, 2))
if round(math.log(nums[i] + 1, 2) - k, 1) == 0.0:
k = k - 1
for j in range(k):
new_capacity.append(weight[i] * pow(2, j))
new_value.append(value[i] * pow(2, j))
if (nums[i] - pow(2, k) + 1) != 0:
new_capacity.append((nums[i] - pow(2, k) + 1) * weight[i])
new_value.append((nums[i] - pow(2, k) + 1) * value[i])
res = one_zero_pack_third(new_capacity, new_value, len(new_capacity), C)
return res
多重背包可行性问题指的是:每种有若干件的物品能否填满给定容量的背包,此时不考虑价值最大问题
实际案例:
小明身边有2张1元现金,3张5元现金,4张20元现金,1张50元现金,问小明身边能够凑足19元整钱吗?
即用:numbers = [2, 3, 4, 1], weight = [1, 5, 20,50]的物品去填充19元的背包,求解是否可行?
使用动态规划求解该问题,定义子问题的状态转移:
F ( i , j ) F(i, j) F(i,j)表示使用前 i i i个物品,填充容量为 j j j的背包,第 i i i个物品最多能够剩余多少个,如果无法填充容量为 j j j的背包,则值为-1.
根据该定义,可以得到如下递推表达式:
F ( i , j ) = { M i , F ( i − 1 , j ) ≥ 0 − 1 , j < W i o r F ( i , j − W i ) ≤ 0 F ( i , j − W i ) − 1 , 其 他 F(i,j)=\left\{ \begin{aligned} M_i, \qquad \qquad \qquad \qquad \quad F(i-1, j) \geq 0\\ -1, \qquad j<W_i \quad or \quad F(i, j-W_i) \leq 0 \\ F(i, j-W_i) - 1, \qquad \qquad \qquad \qquad 其他 \end{aligned} \right. F(i,j)=⎩⎪⎨⎪⎧Mi,F(i−1,j)≥0−1,j<WiorF(i,j−Wi)≤0F(i,j−Wi)−1,其他
# 背包问题---可行性
def can_fill_pack(capacity, nums, C):
len_capa = len(capacity)
F = []
for i in range(len_capa):
F.append([])
for j in range(C + 1):
F[i].append(-1)
k = min(int(C / capacity[0]), nums[i])
for i in range(k + 1):
F[0][k * capacity[0]] = nums[i] - k
for i in range(1, len_capa):
for j in range(C + 1):
if F[i - 1][j] >= 0:
F[i][j] = nums[i]
elif j < capacity[i] or F[i][j - capacity[i]] <= 0:
F[i][j] = -1
else:
F[i][j] = F[i][j - capacity[i]] - 1
if F[len_capa - 1][C] != -1:
return True
else:
return False
由于每一次都只用到了一行的数据,故而可以对空间复杂度进行缩减.
def can_fill_pack_second(weight, nums, C):
len_capa = len(weight)
F = []
for i in range(C + 1):
F.append(-1)
k = min(int(C / weight[0]), nums[0])
for i in range(k + 1):
F[k * weight[0]] = nums[i] - k
for i in range(1, len_capa):
for j in range(C + 1):
if F[j] >= 0:
F[j] = nums[i]
elif j < weight[i] or F[j - weight[i]] <= 0:
F[j] = -1
else:
F[j] = F[j - weight[i]] - 1
if F[C] != -1:
return True
else:
return False
上述代码均通过小样本测试用例,如若描述有误之处,还望指正!
参考资料:
[1]. https://github.com/tianyicui/pack
[2].https://blog.csdn.net/ctsas/article/details/53708712