背包DP学习笔记
01背包
由于01背包太过经典,所以一定要把每一个细节理解透彻!
有 个物品和一个容量为 的背包,每个物品有体积 和价值 ,求用这个背包所能装下的最大价值。
设 表示只考虑前 个物品,体积不超过 的最大价值。如果我们算完了前 个物品的所有结果,那么第 个物品有选和不选两种情况。如果不选,则结果为 ;如果买,则:由于选了 后体积不超过 ,那么选 之前体积就不能超过 ,而选了 之后获得的价值就多了 ,所以结果为 。这样,我们就得出了经典的状态转移方程:。
接下来,我们再考虑一些细节。对于 数组的初始化,我们可以将所有的方案全部赋值为 ,因为无论考虑多少物品,无论体积不超过多少,都一定有一种符合要求的方案:一个也不选。这时的价值就是 。
然后就到了经典的空间优化:我们可以发现 这一行只与 这一行有关,所以我们可以将 这一维省略。这样,当我们准备求 时,我们要求 和 都还没有被更新过。因为我们正准备更新 ,所以第一个要求可以保证,那么怎么保证 没有被更新过呢?答案就是倒序更新(这样就在更新 之后才会更新到 了)。
接下来还有一个经典的常数优化:因为当 时,转移方程中 不存在,不需要考虑更新,所以 必须大于等于 ,也就是倒序枚举时 只需要从 枚举到 。
所以,我们就得到了经典的01背包代码:
1 | for(int i=1;i<=n;i++) |
例题:采药
正好填满的01背包
是一个01背包经典的变形,题意基本与01背包相同,但要求背包必须填满。
这时, 数组的意义发生了一点变化: 表示只考虑前 个物品,体积恰好为 的最大价值。
但是,仔细推理一下,就会发现,状态转移方程和01背包一模一样,空间优化和常数优化也都通用。那不一样的地方在哪里呢?答案是初始化。由于要求体积恰好为 ,所以当 时,不允许一个也不选,所以初始化为负无穷(表示目前没有任何方案满足条件),当 时,才存在一个也不选的方案,这时才能初始化为 。
代码:
1 | memset(f,-0x3f,sizeof(f)); |
二维费用背包
有两维费用(如:一个事件既要消耗时间也要消耗金钱,获得一定价值)的01背包。
将01背包多开两维费用,其他完全相同。
代码:
1 | //背包第一维容量为m,背包第二维容量为t |
例题:榨取kkksc03
完全背包
又是一个经典模型,也必须要理解透彻。题意与01背包基本相同,但每个物品能选无数遍。
同样设 表示只考虑前 种物品,体积不超过 的最大价值。这时如何更新呢?如果我们不选这个物品,那么结果为 ;如果选,那么结果为 。这是为什么呢?在01背包中,我们当前要选 ,那么选这个 之前,只能考虑前 个物品,所以要从 转移,但是在完全背包中,每个物品可以选无数次,所以选这个 之前, 也是可以选的,所以要从 转移而来。这样,我们就得到了最终的转移方程:。
同01背包一样,我们也可以省略掉 这一维。这时,当我们求 时,要求变成 还没有更新,而 已经更新过了(因为我们要用的是 而不是 )。同样,第一个要求能直接满足,对于第二个要求,我们只需要正序枚举 即可。所以,最终转移方程与01背包一样,但 的枚举顺序变成了正序。
代码:
1 | for(int i=1;i<=n;i++) |
例题:疯狂的采药
多重背包
题意与01背包基本相同,但每种物品能选 次。
一个很容易想到的思路为将一种物品选 次转换成 个完全相同的物品,再做01背包。
这样的复杂度显然不够优秀,所以我们考虑优化。我们希望将每个物品选 次转换成若干个物品,使得无论想选多少次都能用这若干个物品凑出来。一个较为明显的做法就是二进制分解。例如,我们有一个物品能选20次,我们就将它分解成一个 倍物品、一个 倍物品、一个 倍物品、一个 倍物品和一个 倍物品(几倍物品指的是体积和价值都为原物品的几倍)。易证,这一定满足我们的条件。这样,我们就将一个物品选 次分解成了 个物品,然后再跑一遍01背包即可。
代码:
1 | for(int i=1;i<=n;i++) |
例题:宝物筛选
混合背包
将01背包、完全背包和多重背包缝合在一起的问题。
思路很简单,无需多讲解,分别考虑即可。形式为:
1 | for(枚举物品) { |
实际上,01背包和多重背包可以共用多重背包的代码,因为01背包可以当成只能取一次的多重背包。
例题:樱花
核心代码:
1 | for(int i=1;i<=n;i++) { |
至此,基本模型已经讲完,其他变种模型以后有空再更新。