背包DP

0-1背包

问题分析:

0-1背包问题,即每个物体只有2种状态(取与不取),正如二进制中的0和1,且一个物品只能取一次

一般例题中,已知条件有第$i$个物品的重量 $w_i$ ,价值$v_i$,以及背包的总容量$W$,在最大容量内取最高价值的物品。

解决dp类型的问题,我们需要先写出其状态转移方程

设状态$f_{i,j}$为只能放前$i$个物品时,容量为$j$的背包所能达到的最大总价值。
假设当前已处理好前$i-1$个物品时,那么对于第$i$个物品,我们有两种选择:将其放入背包,背包的剩余容量减少$w_i$,总价值增加$v_i$,即最大价值为$f_{i-1,j-w_i}+v_i$;不将其放入背包,背包的剩余容量和总价值皆不改变,即最大价值为$f_{i-1,j}$。
由此,可以得出状态转移方程:
$$f_{i,j} = max(f_{i,j},f_{i-1,j-w_i}+v_i)$$
我们发现,对于第$i$个物品,其只受到第$i-1$个物品影响, 所以我们可以降一个维度,直接用$f_j$表示处理当前物品$i$时背包容量为$j$的最大价值。
$$f_j = max(f_j,f_{j-w_i}+v_i)$$

注意:我们只处理物品重量和背包容量为整数时的问题!

代码实现

首先,创建一个长度为 $W+1$ 的列表 f[j] ,存储内容是背包容量为 $j$ 时的最大价值。 默认初始为0
dp板子一般都是双层循环,外层循环判断物品数,内层循环判断背包状态

遍历规则:每次取第 $i$ 个物品,然后更新背包容量为 $j$ 的最大价值,即 f[j] 的值。

n,m = map(int,input().split())   # n为物品数量,m为背包最大容量
inf = []

for i in range(n):
    inf.append(list(map(int,input().split())))  # 存储每个物品的总量和价值

f = [0 for i in range(m+1)]

for i in range(n):
    for j in range(m,inf[i][0]-1,-1):      # 倒序枚举,一个物品只能放入一次
        if f[j] < f[j-inf[i][0]] + inf[i][1]:
            f[j] = f[j-inf[i][0]] + inf[i][1]
print(f[m])

最后,f[m]存储的就是背包容量为$m$时物品价值的最大值。


完全背包

问题分析

完全背包与0-1背包问题基本相同,不同点在于完全背包问题的一个物品物品可以取多次

设$f_{i,j}$为在只能取前 $i$ 个物品时,容量为 $j$ 的背包所能取的最大价值,即 $f[j]$ 的值。
我们先分析状态转移方程:
最暴力的做法:对于第 $i$ 个物品时,我们疯狂取直到背包上限,时间复杂度为 $O(n^3)$
$$ f_{i,j} = max(f_{i-1,j-w_i×k}+v_i×k) k∈(0,+∞)$$
优化算法:
$$ f_{i,j} = max(f_{i-1,j},f_{i,j-w_i}+v_i)$$
大家仔细想想,不难发现,$f_{i,j-w_i}$已经由$f_{i,j-{2×w_i}}$更新过,而$f_{i,j-{2×w_i}}$又被$f_{i,j-{3×w_i}}$更新过,即$f_{i,j-w_i}$是充分考虑了第 $i$ 个物品的所选次数。

代码实现

m,n = map(int,input().split())
inf = []
for i in range(n):
    inf.append(list(map(int,input().split())))
f = [0 for i in range(m+1)]
for i in range(n):
    for j in range(inf[i][0],m+1):                  #正序枚举,一个物品可以取多次
        if f[j] < f[j-inf[i][0]] + inf[i][1]:
            f[j] = f[j-inf[i][0]] + inf[i][1]
print(f[m])

代码思路基本和0-1背包相同,唯一不同的地方是双层循环时,内层循环0-1背包是倒序,而完全背包是正序

小洪助力:
学习完0-1背包和完全背包,相信大家对双层循环部分还是一知半解,我们用文字模拟一下遍历过程,有助于大家理解。
外层循环 $i$ 控制的是每次只能取前 $i$ 个物品所能达到不同容量 $j$ 时的最大价值,很明显,对于遍历容量 $j$ 时,我们只有当容量 $j$ 大于或等于第 $i$ 个物品的重量时,才有判断的意义。
因此,我们是从背包容量最大值 $W$ 遍历到第 $i$ 个物品的重量,还是从第 $i$ 个物品的重量遍历到背包容量最大值 $W$,就是0-1背包和完全背包的区别。
想一想,如果我们从小到大遍历,先更新了 $f[j]$,那么 $f[j]$ 中已经包含了一个物品 $i$,如果 $f[j+w_i]$ 满足判断要求,再更新$f[j+w_i]$,那么这个背包里就会出现两个物品 $i$,很明显就不符合0-1背包问题。
而如果我们从大到小遍历,$f[j]$ 会依据旧的 $f[j-w_i]$ 更新,而不会受到新的 $f[j-w_i]$ 影响,就不会出现物品 $i$ 选取多次的情况。


多重背包

问题分析

多重背包与0-1背包相似,区别在于多重背包的某些物品有$k_i$个,而并非只有一个。

我们可以将多重背包里的物品 $i$ 有 $k_i$个等价为有$k_i$个相同物品 $i$ ,这样就能转化为0-1背包问题。

状态转移方程:
$$f_{i,j} = max(f_{i-1,j-k×w_i}+v_i×k) k∈(0,k_i)$$
时间复杂度为$O(nW\sum{k_i})$

代码实现

强行将多重背包问题转化为0-1背包问题,当数据量过大容易被系统卡掉,不建议这么写!

n,m = map(int,input().split())
inf = []
num = 0
for i in range(n):
    v,w,p = map(int,input().split())
    num += p
    for j in range(p):
        inf.append([v,w])
f = [0 for i in range(m+1)]
for i in range(num):
    for j in range(m,inf[i][1]-1,-1):
        if f[j] < f[j-inf[i][1]] + inf[i][0]:
            f[j] = f[j-inf[i][1]] + inf[i][0]
print(f[m])

二进制分组优化

强行转化成0-1背包时,假设第 $i$ 个物品有 10 个,最优解时我们需要取 2 个,在遍历时我们就会有 $C^2_{10}$ 个组合,浪费了很多时间。对此,我们使用二进制分组优化

简单来说,二进制分组就是把 $k_i$ 个物品拆成1 2 4 8 ...的组合,对于不是2的整数次幂的数,我们只需在最后一位补齐就好。
例:13 可拆分为 1 2 4 6
7 可拆分为 1 2 4

说一下原理,因为对于任意大小的数值,我们总能在这些组合中找到有且只有一个组合符合这个数,即减少不必要的遍历。

n,m = map(int,input().split())
inf = []
f = [0 for i in range(m+1)]
for i in range(n):
    v,w,p = map(int,input().split())
    idx = 1
    while p - idx > 0:
        inf.append([idx*v,idx*w])
        p -= idx
        idx = idx << 1
    inf.append([p*v,p*w])
for i in range(len(inf)):
    for j in range(m,inf[i][1]-1,-1):
        if f[j] < f[j-inf[i][1]] + inf[i][0]:
            f[j] = f[j-inf[i][1]] + inf[i][0]
print(f[m])

单调队列优化

单调队列:单调递增或单调递减的队列

简单来说,单调队列的作用是在一个连续的 $k$ 长度区间内找到其最值
例:在 1 3 -1 -3 5 3 6 7 中找到 $k$ 长度为3的最值。
不难发现,从左到右的最大值为:3 3 5 5 6 7 ;最小值为:-1 -3 -3 -3 3 3

先讲一下用单调减队列求最大值的原理:
$·$ 从左到右依次遍历 $i$ ,若 $i$ 小于队尾的值,将 $i$ 加入队列。
$·$ 若 $i$ 大于队尾的值,将队尾的数剔除,并继续判断,若还是大于队尾的值,继续剔除并判断,直到 $i$ 小于队尾的值或队空,将 $i$ 加入队列。
$·$ 判断队尾元素的序号和对头的序号是否超过长度 $k$ 的限制,若超过则剔除队头。
$·$ 当前队列的队头即是区间 $[i-k,i]$ 的最大值

注意:单调增队列求最小值,单调减队列求最大值
单调增队列求最小值原理相同,自己举一反三

我们来模拟一遍单调增队列求区间最小值的实现过程:

STEP维护队列队首元素(区间最小值)
1 入队{1}
3 比 1 大, 3 入队{1, 3}
-1 比 1 3 小,1 3 出队,-1 入队{-1}-1
-3 比 -1 小,-1 出队,-3 入队{-3}-3
5 比 -3 大,5 入队{-3, 5}-3
3 比 5 小,5 出队,3 入队{-3, 3}-3
-3 不在所查询的区间范围内,-3 出队, 6 比 3 大,6 入队{3, 6}3
7 比6大,7 入队{3, 6, 7}3

很明显,单调队列是一个双端队列,不仅需要在队尾执行插入删除操作,同时也需要在对头执行插入删除操作。
在 $python$ 中,我们可以使用 $deque$ 队列完成上述操作。

from collections import deque
dq = deque()  # 创建一个空的双端队列
dq.appendleft() # 在队头插入元素
dq.append() # 在队尾插入元素
dq.popleft() # 删除队头元素
dq.pop() # 删除队尾元素

具体代码实现: (单调减队列求区间最大值)

from collections import deque
dq = deque()
n,k = map(int,input().split())  # 区间边界k
num = list(map(int,input().split()))
ans = [0 for i in range(n)]
for i in range(n):
    while dq and num[i] > dq[-1][0]:  # 与队尾进行比较
        dq.pop()
    dq.append((num[i],i))  # 加入队尾
    if i - dq[0][1] == k: # 判断队头是否符合区间要求
        dq.popleft()
    ans[i] = dq[0][0] # 记录区间最大值
print(ans[2:])

学习完单调队列的基本原理,我们来看看单调队列在多重背包上的优化。

先简单描述一个场景,我们只取一种物品 $i$,知道它的价值 $v_i$,重量 $w_i$ 和数量 $p_i$ 以及我们背包容量的最大值$W$。对于这个物品 $i$,如果 $w_i×p_i>W$,即我们的背包无法装完所有的物品,也就相当于该物品有无数多,我们可以直接转化为完全背包问题去解决。
单调队列优化是针对当 $w_i×p_i<=W$ 时,物品数量为有限多个的优化问题。

对于单个物品,其状态转移方程:$f_{j} = max(f_j,f_{j-k×w_i}+v_i×k) k∈(0,k_i)$
在考虑不同背包容量 $j$ 的最大值时,我们能发现 $f[j]$ 和 $f[j-k×w_i]$相关。
举个例子,一个物品重量为 $3$,价值为 $4$,那么背包容量 $j$ 为 $6$ 时, $f[6]$ 的值就等于 $max(f[0]+2×4,f[3]+1×4,f[6])$

我们定义变量 $ind$ 为背包放入最多数量物品 $i$ 后剩余的空间,即 $ind = j\%w_i,ind∈[0,w_i)$
再定义变量 $jnd$ 为选取物品 $i$ 的数量,其大小由背包剩余空间 $ind$ 和物品重量 $w_i$ 决定。$jnd∈[0,(W-ind)/w_i)$

通过遍历 $ind$ 和 $jnd$ 即可更新不同大小容量 $j$ 的最大值。 $j = ind+jnd×w_i$
例:在上面例子的条件下,当 $ind=0$ 时,依次遍历 $jnd$ 即可得到f[0],f[3]和f[6]的值。

现在,我们开始引入单调队列,前面讲到 $f[j]$ 会受到 $f[j-w_i×k]$ 的影响,也就是当 $f[j-w_i×k]+k×v_i$ 大时会更新 $f[j]$,我们只需不断找出在有限数量 $p$ 下的最大值即可。

from collections import deque
n,m = map(int,input().split())
f = [0 for i in range(m+1)]
while n:
    v,w,p = map(int,input().split())
    if w * p > m:            # 转化为完全背包问题解决
        for i in range(w,m+1):
            if f[i] < f[i-w] + v:
                f[i] = f[i-w] + v
    else:
        for ind in range(w):   # ind控制背包剩余容量
            dq = deque()
            num = (m-ind)//w   # num表示放入最大的数量
            for jnd in range(num+1):      # jnd控制物品选取数量
                temp = f[ind+jnd*w] - jnd*v  # temp为不取jnd个物品时最大价值
                while dq and temp > dq[-1][0]: 
                    dq.pop()              
                dq.append((temp,jnd))
                if jnd - dq[0][1] > p:  # 判断是否超过有限数量
                    dq.popleft()
                f[ind+jnd*w] = dq[0][0] + jnd*v
    n-= 1
print(f[m])

最后,祝大家能完全理解单调队列优化多重背包问题! OVO 超有趣的优化!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值