
数据结构与算法教程之动态规划(2)——背包 dp
动态规划
背包 dp
背包问题是动态规划中入门级别的题目,其应用还算广泛。
一般背包就是指一个容器,然后装载物品,使得期望的某些价值最大等等。
01 背包
问题模型
有 n 个价值和体积分别为 v[i],w[i] 的物品(每个物品都只有一件),有一个大小为 W 的背包,求问最多能在背包中装多少价值的物品。
分析
根据上讲的分析,我们先来思考状态的表示。
题目相当于算体积和不超过 W 的物品的最大价值和。
不超过看上去不太好写,我们可以考虑计算体积和恰为 W 的物品的最大价值和。
之后只需要对体积和为 0∼W 求得其中的最大值即可。
设 dp[i][j] 表示从前 i 件物品选出体积和为 j 的物品集合的最大价值。
转移十分自然:dp[i][j]=max(dp[i−1][j],dp[i−1][j−w[i]]+v[i])。
所以核心代码就是:
时间复杂度与空间复杂度均为 O(nW)。
优化
这样子用一个二维数组来表示状态显得冗余,我们考虑用一维数组 dp[i] 表示到当前体积和为 j 的物品集合的最大价值。
那么转移本身还是差不多:dp[j]=max(dp[j],dp[j−w[i]]+v[i])。
但我们需要保证每件物品最多用了一次。
所以有一个巧妙的方法,就是倒着枚举 j,这样小的 j 就不会更新给大的。
代码会变为:
甚至变得更为简单。
这样时间复杂度不变,空间复杂度降为了 O(W)。
完全背包
问题模型
有 n 类价值和体积分别为 v[i],w[i] 的物品(每个物品有无限件),有一个大小为 W 的背包,求问最多能在背包中装多少价值的物品。
分析
跟上一个的区别就是每个物品可以取多次。
所以我们只需要将上面的优化算法的 j 的枚举顺序重新变为正序即可。
我写的代码:
复杂度跟上面完全相同。
多重背包
问题模型
有 n 类价值和体积分别为 v[i],w[i] 的物品(每个物品 m[i] 件),有一个大小为 W 的背包,求问最多能在背包中装多少价值的物品。
分析
有点像上述两个的中间体,既不是不能多次,又不是无限次使用。
看起来十分难以直接表示状态。所以需要更进一步的状态。
考虑到每个非负整数都能用二进制表示,我们可以将 m[i] 件物品分为若干部分。
取 ti=⌊log2(m[i]+1)⌋,可以将其分为 20,21,⋯,2ti−1,m[i]–2ti+1 这么几个部分。
这样每一个 0∼m[i] 的整数,一定能被分成这些部分的和。
也就是说,用这些每个部分作为整体(价值和体积都乘上若干倍数),其选择方案肯定覆盖了原有的所有可能。
所以这之后就重新变为了 01 背包问题。
具体代码不再演示。
时件复杂度为 O(nWlogM),空间复杂度为 O(W)。(M=nmaxi=1mi)
优化
这种方法不是一定要掌握。
如果用 dp[i][j] 表示同样的含义,容易写出转移方程:
dp[i][j]=max{dp[i−1][j−k×w[i]]+k×v[i]},0≤k≤m[i]。
这个算法的复杂度大概高的可怕。
设 d=w[i],a=⌊jd⌋,b=j–a×d。
则转移方程可改写为:
dp[i][j]=max{dp[i−1][b+k×d]−k×v[i]}+a×v[i],a–m[i]≤k≤a。
(相信大家可以自己看出来是怎么化简的)
dp[i][j] 就是求 j 的前面 m[i]+1 个数 dp[i−1][b+k×d]−k×v[i] 的最大值,然后加上 a×v[i]。
如果将 dp[i][j] 前面所有的 dp[i−1][b+k×d]−k×v[i] 放入一个队列,那么 dp[i][j] 就是求这个队列长度不超过 m[i]+1 时,元素的最大值,再加上一个常数。所以我们希望用 O(1) 的时间算出一个队列的最大值。
而这也正是单调队列所能做到的。
具体关于单调队列的内容先不讲,留待之后数据结构部分细说。
这里给一下代码:
学过单调队列再来看这题,我们可以发现,这样做的时间复杂度应当是 O(nW),空间复杂度是 O(W),成功将时间复杂度优化掉了一个 log。
混合背包
问题模型
每个物品都有前三个物品的三种可能数量要求,其他不变。
分析
显然可以使用多重背包直接做。
当然,如果想常数优秀,你可能还可以对于每个 i,特判掉 m[i]=1(01 背包)和 m[i]×w[i]≥W(完全背包)的情况。
多重背包的两种方法往往都已经足够优秀。
二维费用背包
问题模型
与前面的背包问题不同的是,每个物品的体积限制变为了两维。
也就是有两个属性 w[i] 和 g[i],其总和分别不超过 W 和 T。
分析
二维版 01 背包显然可以通过加一维状态解决。
设 dp[i][j][k] 表示从前 i 件物品选出 w[i] 之和为 j,g[i] 之和为 k 的物品集合的最大价值和。
则转移显然是:
dp[i][j][k]=max(dp[i−1][j][k],dp[i−1][j−w[i]][k−g[i]]+v[i])。
当然也可以倒序使得维数下降。
完全背包同理使用顺序即可,多重背包使用二进制拆分即可。
当然这里原来的优先队列方法就较难类比了。
时间复杂度应该是 O(nWT) 和 O(nWTlogM)。
空间复杂度应该是 O(WT) 或者 O(nWT)。
分组背包
问题模型
在 01 背包的问题基础上,对于物品有一个分组,对于同一组中的物品,至多选择一件。
分析
其实只有把原来的前 i 件,换为前 i 组,并且在最后枚举第 i 组中的每个物品即可。
大致核心代码:
时空复杂度还是 O(nW) 和 O(W)。
有依赖背包
简化问题模型
在 01 背包的问题基础上,物品间存在一些依赖关系,比如选择物品 i 就必须选择物品 j。
这里我们先假设没有一个物品既被依赖,又依赖别的物品,并且一个物品至多依赖一个物品。
分析
对于不依赖其他物品的物品,称之为”主件“,依赖其他物品的物品,称之为”附件“。
则对于每一个主件及依赖其的 m 个附件,构成了一个组合。
对于这个组合,我们有 2m 种选择方案,也就是如果转化为分组背包,这个分组中将有 2m 个物品。
但是实际上,在体积相同的情况下,我们只需要知道最大的价值即可。
那么我们可以对每个主件 i 的所有附件进行一次体积上限为 W–w[i] 的 01 背包得到 dp[0∼W–w[i]],那么转化后的分组就是 W–w[i]+1 件物品,体积分别为 k+w[i],价值分别为 dp[k]+v[i]。
这之后再进行分组背包即可。
时间复杂度大概是 O(nW2),空间复杂度大概是 O(nW)。
一般问题模型
一个物品可以既被依赖又依赖别的物品,也就是这些依赖关系以森林形式给出。
可以考虑从最底层开始不断向上合并,方法与简化问题模型类似,然后用这一层的 dp 值和 v[i],w[i] 的和作为下一层的新的一些物品。
因为这里涉及到了树形 dp 的思想,而且每个子树的物品组都类似于一个泛化物品,所以不细讲,可以在讲完树形 dp 后自行探究。
泛化物品
问题模型
每个物品的价值变为与其体积也就是费用相关的一个函数。
每次选择物品时还要选择它的体积。
也就是说一个泛化物品是一个数组 v[i][0∼W],给出费用(体积)c,可得到其价值 v[i][c]。
可以发现,上面所有的背包问题,都可以转化成这里的泛化物品。
具体转化可以自行探究一下。
分析
对于两个泛化物品,如何选择分别的大小,使其总费用为 j 时总价值最大呢?
设两个泛化物品的函数关系 f[i] 和 g[i],设最终合并两个泛化物品的结果为 h[i]。
可以轻松得到转移:
h[j]=jmaxi=0{f[i]+h[j−i]}。
而事实上,原问题其实就相当于求解 n 个泛化物品的和。
所以自然只需要重复这个过程就能得出泛化物品下的背包问题做法。
一般对于较为复杂的类背包问题,就可以尝试转化为泛化物品解决。
背包问题的变化
求方案
总体思想是记录是从哪一个状态转移而来转移,如通过 g[i] 表示 dp[i] 的状态通过 dp[i−w[g[i]]]+v[i] 转移而来。
这样只要对最终状态倒序循环,就能找出每一步的转移,即得方案。
字典序最小方案
也就是需要在转移时,对于价值相同的情况优先选择序号小的。
对于选择了物品 1,则问题转化为体积上限为 W–w[1] 的物品 2∼n 的背包,否则就是体积不变的物品 2∼n 的背包。
可以发现问题总是变为 i∼n 的子问题,所以我们可以倒序加入物品。
特别注意,当 dp[i][j]=dp[i−1][j] 和 dp[i][j]=dp[i−1][j−w[i]]+v[i] 同时成立,优先选择后者,也就是选择物品 i。
求方案总数
其实就是把 max 换成 ∑ 即可。
初始条件 dp[0][0]=1。
求最优方案数
直接加一个数组,如果更新了最大值,同步更新方案数。
如果与原来最大值相同,则加上新的方案数。
第 k 优解
无非就是用一个大小不超过 k 的单调下降数组来代替原来的 dp[i][j] 或者 dp[j]。
合并时可以类比归并排序。
时间复杂度也就是乘了一个 k。
No Comments