目录
背包
背包的核心思想是用某种方式填满一个空间获取最大收益
根据填充方式不同就有了下面这几种基本类型:
类型 | 性质 |
---|---|
01背包 | 物品不重复,一种仅可以取一个 |
完全背包 | 物品任意重复拿取 |
多重背包 | 01变式,每种物品有n个 |
混合背包 | 以上三种情况均有出现 |
在以上基本类型之上又有一些变体:
变体 | 性质 |
---|---|
二维费用背包 | 选一个物品此时消耗两种价,例如时间与金钱 |
分组背包 | 将物品分组,组内互相冲突,放入背包时一组只能选其一 |
有依赖背包 | 如果选i就必须选j,选了主件就必须选配件 |
泛化背包 | 物品的价值不是定值,取决于你给它多大空间 |
所要求的答案也不尽相同:
答案类型 |
---|
输出最大值 |
输出最大装包方案 |
输出(最优/装满)方案数 |
下面分类分析之
Basic
~玄牝之门,谓天地之根~
01背包
基础的做法是使用二维的状态
dp[item][volume] 当前状态是:已经对item之前的物品做了决断(即已经选好了编号1~item - 1) 并且背包里还剩volume的体积 易得状态转移方程 dp[curi][curv] = max(dp[curi - 1][curv],IfEnougthSpace(dp[curi - 1][curv - curw])+gain) 遍历方式: for curi 0->n for curv 0->v
观察上面的遍历方式,发现 for curi 0->n 对每一行仅取用一次,想到可以把for curi 0->n压入一维
dp[volume] 当前状态是:已经对curi之前的物品做了决断(即已经选好了编号1~item - 1) 并且背包里还剩volume的体积 状态转移方程变为: dp[curv] = max(dp[curv],IfEnougthSpace(dp[curv - curw]))
完全背包
朴素的做法是对每一个物品枚举选了多少个
dp[item][volume] 当前状态是item之前的物品做了决断,剩余体积volume 状态转移方程是 dp[curi][curv] = max(dp[curi - 1][curv],dp[curi - 1][curv - curw*k]+gain) 其中gain是选了k个当前物品得到的总价值 遍历方式: for curi 0->n for curv 0->v while EnougthSpace k->∞
类似01背包,也可以将数组压缩到一维
dp[volume] 当前状态是:已经对curi之前的物品做了决断,剩余体积volume 采用从前向后填表的方式进行状态转移 dp[volume - k*wi] = max(dp[volume] + gain, self) 遍历方式同上
多重背包
它就是01背包,over
但直接套用01背包进行了重复计算:对于同样的物品0,1,2, 选0,1与选1,2没有差别,然而使用01背包的话这两种情况会被分别计算,浪费了时间
可以采用 二进制分组优化
具体来说,对于每一种k个物品,试图2^i个这物品捆绑起来作为一个大物品,用这些大物品的组合表示任意num∈[0,k]
拆分成大物品形如
6 = 1+2+3 8 = 1+2+4+1 31 = 1+2+4+8+16
这种拆分方式有一个好处,拆分后的每组数量相加可以得到任意数量,原因就在于2^i对应一个二进制数某一位
如果选这个2^i捆绑的区间,就相当于将这个二进制位置1,反之置0。我们可以把数量二进制码任意一位置0或1, 自然就可以表示任意的数量。
对于num - ∑2^i 得到的多余部分,可以视作一个偏移量,给二进制可表示的区间加上偏移量就使得可表示的范围覆盖了所有可能取值
这样一来,原来每组k个物品就被优化成了ceil(log(k))个物品,效率大大提升
Varietas
~万变不离其宗~
混合背包
01、多重、完全的物品都有
尽管三者在物品的限制上有所区别,但dp数组中记录的元素含义相同,三种背包问题实际上可以共用一个dp数组
只需要curi的种类作出判断,选择正确的状态转移方程进行转移即可
二维费用背包
有时候问题的开销不止一种,比如背包同时有容积与载重上限
在这里我们试图泛化背包中剩余空间的概念:它不仅仅可以是一个数,也可以是空间中的向量; 状态转移也不一定是一个数加一个数,也可以是一个向量加一个向量
因此只需把上面的状态转移方程扩成所需的维数即可
dp[curx][cury] = max(self,ifEnougthSpace(dp[curx][cury] - costi)) 遍历方式: for curi 0->itemNum for curx 0->xmax for cury 0->ymax ……
分组背包
有时候背包里的物品会产生冲突,如果放了锂电池就不能往里面灌水
给出多组物品,每一组只能选其中一个物品放进背包
此时采用从前往后填表的方式就比从后往前刷表自然
for volume for group for item dp[volume + ifEnougthSpace(w[item])] = max(self,dp[volume] + gain)
有依赖的背包
如果背包里装一个手机,那必然要一块带上充电器
这样我们把多个物品捆绑为一个,即可转换成上面的基本背包问题
泛化背包
仅需要根据所分配的空间改写状态转移方程即可
Output Request
~背包问题只知道最多能装多少并不足以解决实际问题,还须给出怎么装最多、有几种方案等变形输出~
给出方案
对于每一个状态,都记录当前状态所进行的操作(状态转移时是否选择了某个物品)
用数组 chosen[volume]记录每一个剩余容积是选哪个物品得到的,根据这个数组可以回复选择最优方案的路径:只要根据记录一件一件将东西拿走即可 如果有多种可能,让chosen数组后面挂一个链表或一个一维数组即可
(最优)方案数
单单求方案数二不考虑最优,只需记录总数,状态转移方程把max改为count++即可
对于最优方案数,只需将正常背包问题与上面求方案数的方法组合,找到最优解,看最优解的方案数即可
Summary
背包的本质是什么?
给定一个N^n空间,给定这和个空间中的一些向量,这个向量有一定价值
试图选择这些向量,最大化所得价值,同时不超过空间的边界,这就是背包问题的最终抽象
动态规划的运算过程实际上就是对这个空间里的每一个点尝试每一个可能的向量
最后还有一些优化办法,例如排序、贪心