0-1背包:题意给出n种物品分别的体积v和价值w,背包的总容量vol,每种物品只能选0或1个,问背包里最多能装多少价值。
完全背包:题意给出n种物品分别的体积v和价值w,背包的总容量vol,每种物品能选无限个,问背包里最多能装多少价值。
多重背包:题意给出n种物品分别的体积v、价值w和个数c,背包的总容量vol,每种物品能选c[i]个,问背包里最多能装多少价值。
混合背包:一道题里给出的物品分别可能是上述三种的情况,混合讨论,这种比较简单,分类转移即可,不多讨论,但模板是按这个写。
分组背包:题意给出n'组'物品,每组里有c种物品,每组只能选或不选1个物品,问背包里最多能装多少价值。
二维费用背包:一般是0-1背包的变种,给出的物品多了个重量,背包也有负重上限。简言之就是多了个维度。
以上所有问题还可以变成问方案数。
二维费用背包
了,如果讨论会明确指出。01背包
是最简单的背包入门,正常状态定义是二维:f = [[0]*(vol+1) for _ in range(n+1)] ,其中vol是背包容积,n是物品数。
完全背包
只需在01背包的代码上修改j的转移顺序。多重背包
比较进阶,但没那么难,除了单调队列优化外几乎可以看做01背包+完全背包的合体。分类讨论即可:
分组背包
和前三个背包联系就不那么紧密,和01背包还是有关系的(所以01背包是所有背包的基础):
多重背包
是无法求方案数的,除非同种物品可以区分。这一般会是分组背包
。总结一下边界
恰好
为j:至多
为j时:至多
为j时的价值极值至少
为j时:for j in range(self.vol, -1, -1):f[j] = merge(f[j], f[max(j - v,0)] + w) # 01背包
for j in range(self.vol+1):f[j] = merge(f[j], f[max(j - v,0)] + w) # 完全背包
至少
为j时的价值极值恰好
为j:至多
为j时:至多
为j时的方案数。至少
为j时:for j in range(self.vol, -1, -1):f[j] += f[max(j - v,0)] # 01背包
for j in range(self.vol+1):f[j] += f[max(j - v,0)] # 完全背包
至多少
为j时的方案数例题: 7. 混合背包问题
# Problem: 混合背包问题
# Contest: AcWing
# URL: https://www.acwing.com/problem/content/7/
# Memory Limit: 64 MB
# Time Limit: 1000 ms
import sys
from collections import deque
from math import inf
RI = lambda: map(int, sys.stdin.buffer.readline().split())
RS = lambda: map(bytes.decode, sys.stdin.buffer.readline().strip().split())
RILST = lambda: list(RI())
DEBUG = lambda *x: sys.stderr.write(f'{str(x)}\n')
class PackMaxMin:
""" 背包问题求最大/最小值,复杂度O(体积*物品(种类)数),
如果是多重背包,可以优化成O(体积*物品种类数)
有两种使用方法:
1. 初始化只传入背包容量vol和极值方法merge(如:max)。外部遍历物品,手动调用p.zero_one_pack等方法。见solve1。
好处是不用思考怎么创建dp数组,怎么倒序遍历,遍历时的边界问题(如倒序:range(vol,v-1,-1))
2. 初始化同时传入对应类型的物品集合,然后调用run(),可以直接转移完成。但是需要在外部组合起需要的物品,比较麻烦。
好处是如果外部本身就组合过了,可以直接算,不用外部暴露逻辑。
无法处理的问题:
1. 求方案数(另写一个模板)
2. 求具体方案(还没想好)
3. 二维费用背包:一般是f定义成2维,然后倒序枚举j变成倒序枚举j、k即可,但对代码入侵性较强,变化也多,没想好怎么封装,有空可以尝试写一下试试。
"""
def __init__(self, vol, zero_one=None, multi=None, complete=None, merge=max, sit='just', ini=None):
"""关于ini:
如果求体积'恰好'j时,ini应该是-inf/inf
如果求体积'至少/至多'时,ini应该是0
考虑只有一个物品v=4,但枚举j==5的场景,
若初始化f[1]=0,f[5]会计算成f[1]+w,是有结果的,实际上认为5可以容纳4,即体积不超过/至多5的数据全部容纳。
若初始化f[1]=-inf,则f[5]计算也会是-inf,认为非法。则任何位置只能从0先转移一个合法数据。
"""
self.zero_one = zero_one # 用于01背包的物品 (v,w):体积,价值
self.multi = multi # 用于多重背包的物品 (v,w,c):体积,价值,数量
self.complete = complete # 用户完全背包的物品 (v,w):体积,价值
self.merge = merge # 取极值的方案数,一般是max
self.vol = vol # 背包容量,注意计算复杂度时要用到
if ini is None:
if sit == 'just':
ini = -inf if merge.__name__ == 'max' else inf
elif sit in ['at_least',
'at_most']: # 注意,至多的情况for循环需要修改为:for j in range(self.vol, -1, -1):f[j] = merge(f[j], f[max(j - v,0)] + w)
ini = 0
self.f = [0] + [ini] * vol # f[j]代表体积至多j时的最优值
def zero_one_pack(self, v, w):
""" 01背包,逆序处理即可滚动
v:体积, w:价值
"""
f, merge = self.f, self.merge
for j in range(self.vol, v - 1, -1):
f[j] = merge(f[j], f[j - v] + w)
def complete_pack(self, v, w):
""" 完全背包,正序处理即可滚动
v:体积, w:价值
"""
f, merge = self.f, self.merge
for j in range(v, self.vol + 1):
f[j] = merge(f[j], f[j - v] + w)
def multi_pack_by_zero_one(self, v, w, c):
""" 多重背包转化成01背包,效率最低,但思考方便,一些无脑dp的题可能可以参考到
注意和分组背包区分:外层枚举c个物品,内层倒序枚举j。因为每个物品都要尝试放,扫一遍j。
注意有时会和分组背包混淆:如题意描述成,第i种物品有c个,可以任选几个,但同种物品不区分。
这种情况用多重背包计算就会出现重复方案,实际上考虑分组背包:这组中有c种物品,只能选0/1个。这c种物品分别是1个i,2个i。。c个i。
v:体积, w:价值, c:本物品个数
"""
if v * c >= self.vol: # 如果数量足够到超过目标体积,那么可以当完全背包做,更快
return self.complete_pack(v, w)
for _ in range(c): # 直接展开,尝试c次即可
self.zero_one_pack(v, w)
def multi_pack_by_binary(self, v, w, c):
""" 多重背包的二进制优化,可以把复杂度里的*c变成*lg(c)。原理是:
所有数字都可以用1,2,4,8..等2的幂互相加起来,那么把c分解成这些数字分别尝试逆序转移即可,别忘记最后尝试剩余的部分
v:体积, w:价值, c:本物品个数
"""
if v * c >= self.vol:
return self.complete_pack(v, w)
f, merge = self.f, self.merge
k = 1
while k < c:
self.zero_one_pack(v * k, w * k)
c -= k
k <<= 1
self.zero_one_pack(v * c, w * c)
def multi_pack_by_mono_que(self, v, w, c):
""" 多重背包的单调队列优化,可以把复杂度里的*c变成*1。原理需要画图展开消项,比较复杂,还不是很懂。
v:体积, w:价值, c:本物品个数
"""
if v * c >= self.vol:
return self.complete_pack(v, w)
f, merge = self.f, self.merge
pre = f[:]
for k in range(v):
q = deque()
for j in range(k, self.vol + 1, v):
if q and q[0] < j - c * v:
q.popleft()
while q and pre[q[-1]] + (j - q[-1]) // v * w <= pre[j]:
q.pop()
q.append(j)
f[j] = pre[q[0]] + (j - q[0]) // v * w
def run(self):
"""直接计算这些背包的转移,除了很模板的题不建议使用"""
if self.zero_one:
for v, w in self.zero_one:
self.zero_one_pack(v, w)
if self.multi:
for v, w, c in self.multi:
self.multi_pack_by_mono_que(v, w, c)
if self.complete:
for v, w in self.complete:
self.complete_pack(v, w)
return self.f
# 1324 ms
def solve():
n, vol = RI()
pp = PackMaxMin(vol, merge=max)
for _ in range(n):
v, w, s = RI()
if s == 1 or s == -1:
pp.zero_one_pack(v, w)
elif s == 0:
pp.complete_pack(v, w)
else:
pp.multi_pack_by_mono_que(v, w, s) # 1324 ms
# pp.multi_pack_by_binary(v, w, s) # 1349 ms
# pp.multi_pack_by_zero_one(v, w, s) # 1353 ms
print(max(pp.f))
# 1342 ms
def solve1():
n, vol = RI()
zero_one, multi, complete = [], [], []
for _ in range(n):
v, w, s = RI()
if s == 1 or s == -1:
zero_one.append((v, w))
elif s == 0:
complete.append((v, w))
else:
multi.append((v, w, s))
pp = PackMaxMin(vol, zero_one=zero_one, multi=multi, complete=complete, merge=max)
print(max(pp.run()))
if __name__ == '__main__':
solve()
例题: 9. 分组背包问题
import sys
from math import inf
RI = lambda: map(int, sys.stdin.buffer.readline().split())
RS = lambda: map(bytes.decode, sys.stdin.buffer.readline().strip().split())
RILST = lambda: list(RI())
DEBUG = lambda *x: sys.stderr.write(f'{str(x)}\n')
class GroupedPackMaxMin:
"""分组背包求最大/最小值
传入多组物品,每组中只能选一个,求极值。转化成01背包考虑,外层枚举体积j,内层尝试这个j选不同物品(注意和多重背包区分)。
注意有时会和多重背包混淆:如题意描述成,第i种物品有c个,可以任选几个,但同种物品不区分。
这种情况用多重背包计算就会出现重复方案,实际上考虑分组背包:这组中有c种物品,只能选0/1个。这c种物品分别是1个i,2个i。。c个i。
"""
def __init__(self, vol, grouped_items=None, merge=max, sit='just', ini=None):
"""关于ini,可以不填,指定sit即可,其中sit用的最多的是at_most,这时一般可以直接返回p.f[-1]:
如果求体积'恰好'j时,ini应该是-inf/inf
如果求体积'至少/至多'时,ini应该是0
考虑只有一个物品v=4,但枚举j==5的场景,
若初始化f[1]=0,f[5]会计算成f[1]+w,是有结果的,实际上认为5可以容纳4,即体积不超过/至多5的数据全部容纳。
若初始化f[1]=-inf,则f[5]计算也会是-inf,认为非法。则任何位置只能从0先转移一个合法数据。
"""
self.grouped_items = grouped_items # 形如[[(1,2),(2,3)],[(1,2),(2,3)],],注意是多组数组,每组中只能选一个
self.merge = merge
self.vol = vol
if ini is None:
if sit == 'just':
ini = -inf if merge.__name__ == 'max' else inf
elif sit in ['at_least',
'at_most']: # 注意,至多的情况for循环需要修改为:for j in range(self.vol, -1, -1):f[j] = merge(f[j], f[max(j - v,0)] + w)
ini = 0
self.f = [0] + [ini] * vol # f[j]代表体积j时的最优值,
def grouped_pack(self, items):
f, merge = self.f, self.merge
for j in range(self.vol, 0, -1):
for v, w in items:
if j >= v:
f[j] = merge(f[j], f[j - v] + w)
def run(self):
if self.grouped_items:
for items in self.grouped_items:
self.grouped_pack(items)
return self.f
# 1065 ms
def solve():
n, vol = RI()
gp = GroupedPackMaxMin(vol)
for _ in range(n):
s, = RI()
a = []
for _ in range(s):
a.append(RILST())
gp.grouped_pack(a)
print(max(gp.f))
# 1071 ms
def solve1():
n, vol = RI()
items = []
for _ in range(n):
s, = RI()
a = []
for _ in range(s):
a.append(RILST())
items.append(a)
gp = GroupedPackMaxMin(vol, items)
print(max(gp.run()))
if __name__ == '__main__':
solve()
class PackCountPlan:
""" 背包问题求方案数,复杂度O(体积*物品(种类)数),一般求方案数很大,都要取模,这里为了防止忘记取模/模写错,直接调用全局变量,如果忘记定义会报错。
注意和求极值不同:
1. dp[0][0] = 1代表 一个不取的方案数是1
2. 数组里只需要一列体积参数,不需要价值。
一般是01背包,完全背包。可能无法处理多重背包,因为无法区分同种类物品。这时尝试考虑分组背包,把每种物品看成一组,这组里的物品分别是1个i,2个i..c个i。
"""
def __init__(self, vol, zero_one=None, multi=None, complete=None,sit='just',ini=0):
"""关于ini
如果是求体积'恰好'为j时的方案数,ini=0;
如果是求体积'至多'为j时的方案数,ini=1;
如果是求体积'至少'为j时的方案数,ini=1; 同时遍历体积需要改动为 for j in range(self.vol, v - 1, -1):f[j] = (f[j] + f[max(j - v,0)]) % MOD
"""
self.zero_one = zero_one # 用于01背包的物品 (v):体积
self.multi = multi # 用于多重背包的物品 (v,c):体积,数量--- 注意用不了,我只是没删,可能后续想办法补。
self.complete = complete # 用户完全背包的物品 (v):体积
self.vol = vol # 背包容量,注意计算复杂度时要用到
if ini is None:
if sit == 'just':
ini = 0
elif sit in ['at_least',
'at_most']: # 注意,至多的情况for循环需要修改为:for j in range(self.vol, -1, -1):f[j] = merge(f[j], f[max(j - v,0)] + w)
ini = 1
self.f = [1] + [ini] * vol # f[j]代表体积j时的方案,
def zero_one_pack(self, v):
""" 01背包,逆序处理即可滚动
v:体积
"""
f = self.f
for j in range(self.vol, v - 1, -1):
f[j] = (f[j] + f[j - v]) % MOD
def complete_pack(self, v, w):
""" 完全背包,正序处理即可滚动
v:体积, w:价值
"""
f = self.f
for j in range(v, self.vol + 1):
f[j] = (f[j] + f[j - v]) % MOD
def run(self):
"""直接计算这些背包的转移,除了很模板的题不建议使用"""
if self.zero_one:
for v in self.zero_one:
self.zero_one_pack(v)
if self.complete:
for v in self.complete:
self.complete_pack(v)
return self.f
例题: 6310. 获得分数的方法数
MOD = 10**9+7
class GroupedPackCountPlan:
""" 分组背包问题求方案数,复杂度O(体积*物品(种类)数),一般求方案数很大,都要取模,这里为了防止忘记取模/模写错,直接调用全局变量,如果忘记定义会报错。
注意和求极值不同:
1. dp[0][0] = 1代表 一个不取的方案数是1
2. 数组里只需要一列体积参数,不需要价值。
一般是01背包,完全背包。可能无法处理多重背包,因为无法区分同种类物品。这时尝试考虑分组背包,把每种物品看成一组,这组里的物品分别是1个i,2个i..c个i。
"""
def __init__(self, vol, grouped_items=None):
self.grouped_items = grouped_items # 用于01背包的物品 (v):体积
self.vol = vol # 背包容量,注意计算复杂度时要用到
self.f = [1] + [0] * vol # f[j]代表体积j时的方案,如果是求min这里初始值要改成inf
def grouped_pack(self, items): # 注意传进来的是本组的物品的体积们:[1,6,2,3,4,5..],最好是排序的可以优化一下break
f = self.f
for j in range(self.vol, 0, -1): # 注意外层循环遍历体积j,内层尝试放组内每个物品。
for v in items:
if j >= v: # 这里可以尝试sort break,但是最好能在外层预处理或者天然是排序的。
f[j] = (f[j]+f[j - v] )%MOD
def run(self):
"""直接计算这些背包的转移,除了很模板的题不建议使用"""
if self.grouped_items:
for v in self.grouped_items:
self.grouped_pack(items)
return self.f
class Solution:
def waysToReachTarget(self, target: int, types: List[List[int]]) -> int:
gp = GroupedPackCountPlan(target)
for count,marks in types:
items = [] # 本组物品的体积相当于尝试用k个marks,其中k<=count
for i in range(1,count+1):
items.append(marks*i)
gp.grouped_pack(items) # 整理好本组物品再一起传进去
return gp.f[-1] % MOD
例题: 11. 背包问题求方案数
import sys
RI = lambda: map(int, sys.stdin.buffer.readline().split())
RS = lambda: map(bytes.decode, sys.stdin.buffer.readline().strip().split())
RILST = lambda: list(RI())
DEBUG = lambda *x: sys.stderr.write(f'{str(x)}\n')
MOD = 10 ** 9 + 7
# ms
def solve1():
n, vol = RI()
f = [0] + [0] * vol
g = [1] + [0] * vol # g[j]是体积恰好j时最优价值的方案数,这样需要计算最优方案数的话,需要找到每个最优的j相加。
for _ in range(n):
v, w = RI()
for j in range(vol, v - 1, -1):
if f[j] < f[j - v] + w:
f[j] = f[j - v] + w
g[j] = g[j - v]
elif f[j] == f[j - v] + w:
g[j] += g[j - v]
g[j] %= MOD
mx = max(f)
ans = 0
for i, v in enumerate(f):
if mx == v:
ans += g[i]
print(g[-1] % MOD)
def solve():
n, vol = RI()
f = [0] + [0] * vol
g = [1] + [1] * vol # g[j]是体积不超过j时最优解的方案数,最优方案数的话,就可以直接返回cnt[-1]
for _ in range(n):
v, w = RI()
for j in range(vol, v - 1, -1):
if f[j] < f[j - v] + w:
f[j] = f[j - v] + w
g[j] = g[j - v]
elif f[j] == f[j - v] + w:
g[j] += g[j - v]
g[j] %= MOD
print(g[-1] % MOD)
if __name__ == '__main__':
solve()
例题: 10. 有依赖的背包问题
# Problem: 有依赖的背包问题
# Contest: AcWing
# URL: https://www.acwing.com/problem/content/10/
# Memory Limit: 64 MB
# Time Limit: 1000 ms
import sys
from types import GeneratorType
from math import inf
RI = lambda: map(int, sys.stdin.buffer.readline().split())
def bootstrap(f, stack=[]):
def wrappedfunc(*args, **kwargs):
if stack:
return f(*args, **kwargs)
else:
to = f(*args, **kwargs)
while True:
if type(to) is GeneratorType:
stack.append(to)
to = next(to)
else:
stack.pop()
if not stack:
break
to = stack[-1].send(to)
return to
return wrappedfunc
# ms
def solve():
n, vol = RI()
g = [[] for _ in range(n)] # 建图
a = []
root = 0
for i in range(n):
v, w, p = RI()
if p == -1:
root = i
else:
g[p - 1].append(i)
a.append((v, w)) # 体积和价值
p = [] # 一个全局dp数组,记录每棵子树dfs后,子树产生的背包dp数组长啥样。(所有组合的可能性),后序遍历时他就是分组背包的物品。
@bootstrap
def dfs(u, s): # 尝试放u节点,s代表还能用多少空间(因为之前选择依赖项必须占用一定空间
v, w = a[u] # 当前节点体积和价值
f = [-inf] * (s + 1) # 当前的空间数组
if v <= s: # 如果自己能放上,那就放上,不放不让选子树。
f[v] = w
vv = v # 记录一下体积,后边要使用v变量当邻居(我的习惯
for v in g[u]:
yield dfs(v, s - vv) # 后序遍历,先计算子树的背包数组,用子树状态更新当前节点;由于放了本节点,剩余体积要-vv
for j in range(s, 0, -1): # 倒序更新f,子树能产生的状态是个分组背包,因此先遍历体积,选择子树状态其中的一个。
for v, w in enumerate(p): # 遍历子树状态
if v <= j: # 能放上
f[j] = max(f[j], f[j - v] + w)
p[:] = f[:]
yield
dfs(root, vol)
print(max(p))
if __name__ == '__main__':
solve()
例题: LCP 47. 入场安检
class Solution:
def securityCheck(self, capacities: List[int], k: int) -> int:
MOD = 1000000007
n = len(capacities)
# m = len(capacities[-1])
# 房间是物品,k是容量
f = [0]*(k+1)
f[0] = 1
for v in capacities:
v -= 1
for j in range(k,v-1,-1):
f[j] = (f[j]+f[j-v])%MOD
return f[-1]