# 背包九讲1
def Hello():
print("01背包问题\n完全背包问题\n多重背包问题\n多重背包问题(二进制优化)\n多重背包问题(单调队列优化)\n混合背包问题\n二维背包问题\n分组背包问题")
Hello()
'''
01背包问题:
有N件物品,一个容量为V的背包,第i件物品的价值为value[i-1],重量为weight[i-1];
在体积不超过背包容量的情况下,返回价值最高的方法;
每种物体只有一件,可以选择放还是不放。
f[i][j] 表示只看前i件物品,总体积是j的情况下,最大价值是多少。
result = max(f[0~N][0~V])
1. 不选第i件物品:
f[i][j] == f[i-1][j]
2. 选第i件物品:(因为选择了第i件物品,所以要处理上下标为i-1的物品信息)
f[i][j] == f[i-1][j-weight[i-1]]+value[i-1]
f[i][j] == max(1、2)
初始化:f[0][0] == 0,i==0和j==0 时都为0
'''
def process1(value, weight, N, V):
# dp[0~N][0~V]
dp = [[0] * (V + 1) for _ in range(N + 1)]
for i in range(1, N + 1):
for j in range(V + 1):
ans1 = dp[i - 1][j]
ans2 = 0
if j - weight[i - 1] >= 0:
ans2 = dp[i - 1][j - weight[i - 1]] + value[i - 1]
dp[i][j] = max(ans1, ans2)
return dp[N][V]
'''
01背包问题优化:
f[i] 的数据只和 f[i-1] 有关,所以我们没有必要将所有层的数据都表示出来;
而且 f[i][j] 有可能依赖的 f[i-1][j] 和 f[i-1][j-weight[i-1]] 都是在 f[i][j]的同一侧;
所以我们就能将这个二维数组压缩成从一维数组,并且这个一维数组是从依赖方向的反方向开始填写的;
例如,f[i][j]需要 f[i-1][j] 和 f[i-1][j-weight[i-1]]的数据;
我们在第i层循环的时候自然不能从j为0开始填补这个一维数组;
因为如果在i层循环时填上了这个数据就会将原本 f[i-1][j-weight[i-1]]的数据覆盖成 f[i][j-weight[i-1]]
'''
def process2(value, weight, N, V):
# dp[0~V]
dp = [0] * (V + 1)
for i in range(1, N + 1): # 仍然需要N层循环,只能节省空间,不能节省时间
for j in range(V, -1, -1): # 需求方向从0开始,所以反方向填补
ans1 = dp[j]
ans2 = 0
if j - weight[i - 1] >= 0:
# 在这里可以把判断放到j循环里面, 写为for j in range(V,weigth[i-1]-1,-1), 可以节省一点时间,但是为了方便理解暂时牺牲一点点时间
ans2 = dp[j - weight[i - 1]] + value[i - 1]
dp[j] = max(ans1, ans2)
return dp[V]
'''
完全背包问题 (直接讲优化版本):
有N种物品,一个容量为V的背包,第i件物品的价值为value[i-1],重量为weight[i-1];
在体积不超过背包容量的情况下,返回价值最高的方法;
每种物体有无限件。
f[i][j] 表示只看前i件物品,总体积是j的情况下,最大价值是多少。
1. 不选第i件物品:
f[i][j] == f[i-1][j]
2.1 选1件第i件物品:
f[i][j] == f[i-1][j-weight[i-1]] + value[i-1]
2.2 选2件第i件物品:
f[i][j] == f[i-1][j-2*weight[i-1]] + 2*value[i-1]
....
f[i][j] == max(1、2.1、2.2、2.3)
现在我们来分析完全背包问题的依赖关系:
f[i-1][j-weight[i-1]] 表示 不考虑第i种的物品时,重量为j-weight[i-1]的最优解
f[i][j-weight[i-1]] 表示 考虑第i种物品时。重量为j-weight[i-1]的最优解
所以我们可以把 f[i][j] (前i种物品,体重为j的最优解) == max
f[i-1][j] (考虑第i种物品但是不要)
f[i][j-weight[i]]+value[i] (考虑第i种物品,再要一个,要完刚好装满)
这里说一下和01背包问题的区别:
f[i][j] (前i个物品,体重为j的最优解) == max
f[i-1][j] (考虑第i个物品但是不要)
f[i-1][j-weight[i]]+value[i] (考虑第i种物品,再要一个,要完刚好装满)
这俩个的区别就是:f[i][j-weight[i]]+value[i] 只在乎它是最后一个,而不是它到底要了多少个;
01背包问题不能不在乎要了多少个,必须只能要一个
结合之前对01背包问题的优化:将j从大到小遍历,避免将 f[i-1][j-weight[i-1]]的数据覆盖成 f[i][j-weight[i-1]];
所以完全背包问题只要从低到高遍历就行。
'''
def process3(value, weight, N, V):
# dp[0~V]
dp = [0] * (V + 1)
for i in range(1, N + 1): # 仍然需要N层循环,只能节省空间,不能节省时间
for j in range(V + 1):
ans1 = dp[j]
ans2 = 0
if j - weight[i - 1] >= 0:
# 在这里可以把判断放到j循环里面, 写为for j in range(weigth[i-1],V+1), 可以节省一点时间,但是为了方便理解暂时牺牲一点点时间
ans2 = dp[j - weight[i - 1]] + value[i - 1]
dp[j] = max(ans1, ans2)
return dp[V]
'''
01背包和完全背包的总结:
1. 01背包每件物品最多只能用一次;完全背包每种物品能用无数次;
2. 01背包: 必须要考虑物品被使用的次数,所以只能从考虑i物品之前的情况 f[i-1][j-weigth[i-1]],通过加上一个 value[i-1]来获取依赖;
3. 完全背包: 可以不用考虑物品被使用的次数,所以可以从考虑i物品之后的情况 f[i][j-weight[i]],通过加上最后一个 value[i-1]来获取依赖;
4. 在代码层面: 第一层循环是一样的,
01背包为了保证每一次获取的 f[j-weigth[i-1]]都是i-1层遍历计算出来的,所以从大到小遍历;
完全背包为了保证每一次获取的 f[j-weigth[i-1]]都是i层遍历计算出来的,所以从小到的大遍历。
( [0,1,2,3,4],j-weigth[i-1]是向左边获取值;
在新的一轮中,如果想要获取的值是上一轮的旧值,就从左往右遍历,也就是j从大到小;
在新的一轮中,如果想要获取的值是本一轮的新值,就从左往右遍历,也就是j从小到大)
'''
'''
多重背包问题:
有N种物品,一个容量为V的背包,每种物品最多有S件;(限制了有多少件,所以需要关注物品被使用的次数)
第i件物品的费用为value[i],价值为weight[i];
在体积不超过背包容量的情况下,返回价值最高的方法;
每种物体有无限件。
f[i][j] == max(f[i-1][j], f[i-1][j-1*weight[i-1]]+1*value[i-1], f[i-1][j-2*weight[i-1]]+2*value[i-1]....)
'''
def process4(value, weight, N, V, S):
# dp[0~V]
dp = [0] * (V + 1)
for i in range(1, N + 1): # 仍然需要N层循环,只能节省空间,不能节省时间
for j in range(V, -1, -1): # 需求方向从0开始,所以反方向填补
for s in range(S + 1):
# 增加一个循环就行,因为需要掌控数量,所以从j从大到小循环
if j - s * weight[i - 1] < 0:
break
dp[j] = max(dp[j], dp[j - s * weight[i - 1]] + s * value[
i - 1]) # dp[j - 0 * weight[i - 1]] + 0 * value[i - 1] == dp[j]
return dp[V]
'''
多重背包问题优化:
方法一:假设 S==2,value=[2,4,4,5],weight=[1,2,3,4];
将物品拆成S份,转化为N=S*N规模的01背包问题:value=[2,2,4,4,4,4,5,5],weight=[1,1,2,2,3,3,4,4];
可以减少一轮循环。
但是方法一面临的问题是:当S很小时,value和weight扩容的代价不算高;但是当S比较高时,扩容S倍的代价就不容小觑了;
方法二 (二进制优化):
例如,S==10,value=[...2...],weight=[...1...];
方法一:value=[...2,2,2,2,2,2,2,2,2,2...],weight=[...1,1,1,1,1,1,1,1,1,1...],这样扩容可以组成0~10的每一种情况;
可以发现扩容的代价很容易失控;
方法二:只要保证拆的方法能组成0~S的每一种情况,就代表方法是可行的;
所以,将物品拆成价值为value[i]*2^k,重量为weight[i]*2^k的若干份物品;
如:value=[...2,4,8,6...],weight=[...1,2,4,3...]
可以发现这样也能组成0~10的情况。
'''
def process5(value, weight, N, V, S):
newvalue = []
newweight = []
k = 0
while True:
S = S - 2 ** k
k += 1
if S - 2 ** k < 0:
break
for i in range(N):
for j in range(k):
newvalue.append(value[i] * (2 ** j))
newweight.append(weight[i] * (2 ** j))
newvalue.append(value[i] * S)
newweight.append(weight[i] * S)
N = len(newweight)
dp = [0] * (V + 1)
for i in range(N + 1):
for j in range(V, newweight[i - 1] - 1, -1):
dp[j] = max(dp[j], dp[j - newweight[i - 1]] + newvalue[i - 1])
return dp[V]
'''
多重背包问题 (单调队列优化)
二进制优化后的时间复杂度为 N*log(S)*V ,那么还可以更优化吗?
有没有办法可以优化为 N*V的时间复杂度;
来看看未优化的多重背包问题:
def process(value, weight, N, V, S):
dp = [0] * (V + 1)
for i in range(N + 1):
for j in range(V, weight[i - 1] - 1, -1):
for k in range(S):
if j - k * weight[i - 1] < 0:
break
dp[j] = max(dp[j], dp[j - k * weight[i - 1]] + k * value[i-1])
return dp[V]
可以发现多重背包问题的第一层循环还是哪一个物品,第二层是容量,第三层是决策;
物品和容量是无法优化的,那么能否使用某种方法优化决策呢?
令w=weight[i-1],v=value[i-1]
调度策略依赖关系就变成了 dp[j-w]+v、dp[j-2*w]+2*v.....dp[j-k*w]+k*v
可以发现 f[j]的问题就变成了在 f[j-0*w]+0*v,f[j-1*w]+1*v,f[j-2*w]+2*v,f[j-3*w]+3*v....之中找到一个最大值;
f[j]:max(f[j-0*w]+0*v,f[j-1*w]+1*v,f[j-2*w]+2*v,f[j-3*w]+3*v....f[j-S*w]+S*v)
f[j-w]:max(f[j-1*w],f[j-2*w]+1*v,f[j-3*w]+2*v,f[j-4*w]+3*v....f[j-(S+1)*w]+S*v)
....
即问题:
for k in range(S):
if j - k * weight[i - 1] < 0:
break
dp[j] = max(dp[j], dp[j - k * weight[i - 1]] + k * value[i-1])
可以从x (x<=S)次求最大值的问题,优化为一个滑动窗口求最大值的问题
从余数 r开始: r、r+w、r+2w....j-2w、j-w、j每次跨度为w
即例如,f[9]可能只通过f[9]、f[7]、f[5]...即一定跨度的信息,这个类的区别就是通过 r的不同来区分的;
现在我们知道了相同r的重量所依赖都是同一组,那么如果能在一次遍历的时候就获得所有weight的最大价值,就能实现加速;
在这之前我们先引入一个副本,用于保存i-1层的数据:
def process(value, weight, N, V, S):
dp = [0] * (V + 1)
for i in range(N + 1):
copy_file = dp.copy()
for j in range(1, V + 1):
for k in range(S):
if j - k * weight[i - 1] < 0:
break
dp[j] = max(copy_file[j], copy_file[j - k * weight[i - 1]] + k * value[i - 1])
return dp[V]
'''
def process6(value, weight, N, V, S):
dp = [0] * (V + 1)
for i in range(1, N + 1):
copy_file = dp.copy()
for j in range(weight[i - 1]):
queue = []
for k in range(j, V + 1, weight[i - 1]):
if queue != [] and queue[0] < k - S * weight[i - 1]:
# 同类型的物品不能使用超过S次,因为每一次下标跳跃weight[i-1]格,所以可以用这个方法检测判断队头是否不在窗口内
queue.pop(0)
if queue != []:
# 判断不选的情况和选了的情况中最好的情况
# (k - queue[0]) / weight[i - 1] * value[i - 1]:因为k一次跳跃weight[i-1]格,所以表示 跳跃长度/单位跳跃长度==跳跃次数*value[i-1]
dp[k] = int(max(copy_file[k], copy_file[queue[0]] + (k - queue[0]) / weight[i - 1] * value[i - 1]))
while queue != [] and copy_file[k] >= copy_file[queue[len(queue) - 1]] + (k - queue[len(queue) - 1]) / \
weight[i - 1] * value[i - 1]:
# 始终保持queue头是能选择第i物品时,最好的情况
queue.pop()
queue.append(k)
return dp[V]
'''
混合背包问题:
有N件物品,一个容量为V的背包,
物品一共有三类:
1. 只能使用1次,s==-1 (01背包)
2. 可以使用无限次,s==0 (完全背包)
3. 只能使用S次,s>0 (多重背包)
第i件物品的价值为value[i-1],重量为weight[i-1];
在体积不超过背包容量的情况下,返回价值最高的方法;
因为01背包问题和完全背包问题都能方便计算,所以将多重背包问题转化为01背包问题解决
'''
def process7(value, weight, limit, N, V):
transaction = []
for s in range(N):
if limit[s] == -1:
# 如果是01背包问题,直接放入
transaction.append([value[s], weight[s], -1])
elif limit[s] == 0:
# 如果是完全背包问题,直接放入
transaction.append([value[s], weight[s], 0])
else:
# 如果是多重背包问题,二进制优化拆分为01背包问题
S = limit[s]
k = 0
while True:
S = S - 2 ** k
k += 1
if S - 2 ** (k) < 0:
break
for i in range(k):
transaction.append([value[s] * (2 ** i), weight[s] * (2 ** i), -1])
if S != 0:
transaction.append([value[s] * S, weight[s] * S, -1])
# 再遍历所有的物品
dp = [0] * (V + 1)
for i in transaction:
print(i)
if i[2] == -1:
# 01背包问题,从高到低遍历
for j in range(V, i[1] - 1, -1):
dp[j] = max(dp[j], dp[j - i[1]] + i[0])
else:
# 完全背包问题,从低到高遍历
for j in range(i[1], V + 1):
dp[j] = max(dp[j], dp[j - i[1]] + i[0])
return dp[V]
'''
二维费用背包问题:
有N件物品和一个容量为V的背包,背包的最大承重为M;
第i件物品的价值为value[i-1],重量为weight[i-1],体积为volume[i-1];
在两个条件都不超过的情况下,返回最高的价值;
'''
def process8(value, weight, volume, N, V, M):
dp = [[0] * (V + 1) for _ in range(M + 1)]
for i in range(1, N + 1):
for j in range(M, weight[i - 1] - 1, -1):
for k in range(V, volume[i - 1] - 1, -1):
dp[j][k] = max(dp[j][k], dp[j - weight[i - 1]][k - volume[i - 1]] + value[i - 1])
return dp[M][V]
'''
分组背包问题:
有N组物品和一个容量为V的背包;
每组物品中的物品最多只能选择一个;
求将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大,返回最大价值;
value=[[2,4],[2,6],[1]] 表示三个组
'''
def process9():
getone = input().split(" ")
N = int(getone[0])
V = int(getone[1])
dp = [0] * (V + 1)
for i in range(N): # 分组层
S = int(input())
weight = []
value = []
for j in range(S): # 输入数据
getone = input().split(" ")
weight.append(int(getone[0]))
value.append(int(getone[1]))
for j in range(V, -1, -1): # 从高往低
for k in range(S): # 选择A或者B,因为每一轮都是修改的dp的V方向,所以不会对后续的其他物品选择造成影响
if j >= weight[k]:
dp[j] = max(dp[j], dp[j - weight[k]] + value[k])
print(dp[V])
python算法----背包九讲
最新推荐文章于 2024-05-21 19:16:35 发布