这两种背包非常有意思,01背包的顺序是V…0,而完全背包的顺序是0…V。其背后的原理是,V…0所用的状态都是没有取过当前物品的状态,而0…V所用的状态是可能取过任意个当前物品的状态。所以他们dp的顺序完全相反。
01Pack
int dp[100005];
//01Pack
void pack01(int dp[],int V,int cost,int value){
/*
dp[] 背包空间(若不要求装满则dp[0..V]=0,否则dp[0]=0,dp[1..V]=-INF)
dp[i] 背包中已经装了i体积物品时的价值
V 背包容量上限
cost 塞入物品的[花费]
value 塞入物品的[价值]
*/
for(int v=V;v>=cost;--v){
dp[v]=MAX(dp[v],dp[v-cost]+value);
}
}
CompletePack
dp[100005];
//completePack
void packComplete(int dp[],int V,int cost,int value){
/*
dp[] 背包空间(若不要求装满则dp[0..V]=0,否则dp[0]=0,dp[1..V]=-INF)
dp[i] 背包中已经装了i体积物品时的价值
V 背包容量上限
cost 塞入物品的[花费]
value 塞入物品的[价值]
*/
for(int v=cost;v<=V;++v){
dp[v]=MAX(dp[v],dp[v-cost]+value);
}
}
01背包例题:https://www.luogu.org/problemnew/show/P1048
完全背包例题:https://www.luogu.org/problemnew/show/P1616
比起上述两种背包,多重背包就要麻烦的多了。因为它可以取超过一个,却又有数量上限,这就要求状态中要储存当前物品已经用了多少个的信息。
一种方法是把多重背包拆成n个物品独立求解,复杂度为O(VNM)。在此基础上有一个优化,就是将其二进制拆分成logN个数组,如13可以拆成(1,2,4,6),那么可以将复杂度降到O(VNlogM)。
但是还有一种更加稀奇的优化,可以优先队列把复杂度降到O(VN)。推导过程可以看他的博客:https://blog.csdn.net/flyinghearts/article/details/5898183
实现的时候,用一个单调队列,维护背包空间中编号对当前物品的体积V取模后余数相同的点,且队列的大小为当前物品的数量M。由于队列大小为M,也就保证了至多从M个位置前转移到当前状态,也就是最多选取M个当前物体。
int dp[100005];
int freePack;
struct singleQueue{
/*
单调队列
inititate() 初始化队列
getw() 获取队头元素的数值
pop(i) 删除在编号i之前进队的元素(老队员退役)
push(w,c) 加入一个编号为c,权值为w的元素(接收新队员)
*/
int head,tail;
int w[100005];
int c[100005];
void initiate(){
head=0;
tail=-1;
}
int getw(){
return w[head];
}
void pop(int ind){
while(ind>=c[head])++head;
}
void push(int W,int C){
while(head<=tail&&w[tail]<=W){
--tail;
}
w[++tail]=W;
c[tail]=C;
}
}Q;
void packMulti(int dp[],int V,int cost,int value,int num){
/*
多重背包之优先队列优化
dp[] 要塞入的背包空间
V 花费的上限
cost 物体的花费
value 物体的价值
num 某个物体最多有num个
外部调用:单调队列
*/
if(!cost){//若花费为0,为了防止除0直接弹出
freePack+=value*num;
return;
}
num=MIN(num,V/cost);
for(int i=0;i<cost;++i){//枚举每一个余数
Q.initiate();
int all_=(V-i)/cost;
for(int j=0;j<=all_;++j){//对这个余数集合中元素遍历
Q.push(dp[i+j*cost]-j*value,j);
Q.pop(j-num-1);
dp[i+j*cost]=MAX(dp[i+j*cost],Q.getw()+j*value);
}
}
}
多重背包例题:https://www.luogu.org/problemnew/show/P1776
所有的背包问题都可以用一个模型来求解,那就是分组背包问题。
分组背包是指在空间限制V的基础上,有k组物品,每组物品中你最多选择一个加入背包中,问此时的最优解如何,特别地,什么都不选可以视为一个V=0且W=0的物品。那么我们之前提到了01背包可以表示为k组,每组中都只有两种东西选择,1个当前物品或者0个当前物品。而完全背包则可以表示有k组,每组的方案为选取0…(V/vi)个当前物品物品。多重背包同理,都是其中的特例。《背包九讲》里面提到的“泛化背包”问题,差不多也是这个意思。
而之前提到的三种背包,都因为其数据的特殊性,即每组中的物品都有着一定联系,存在着重复子问题,所有存在一些稀奇的优化,使得复杂度可以降到O(VN)。但是当分组中的物品不再具有这样的联系时,上述优化都将失效,复杂度变为O(VNM)。
对于这样的问题,我们都有着一致的通解,伪代码如下:
for each k=0..K//对于每一组
for each v=V..0//对于背包中每一个容量状态
for each i=0..sizeof(k)//对当前组中每一个物品
dp[v]=max(dp[v],dp[v-cost[k][i]]+weight[k][i];
//选取第i个物品或选择之前的最优状态
值得注意的是,i的循环必定在v的里面,这样就可以保证当我们考虑一个确定的v时,它的状态会从选取接下的任意一个(或没有)物品后的状态得出。同时,v要保证从大到小,保证每个物品至多被选择一次。当然,如果有一个无限背包组,里面的东西可以随便取,那么v就应该从小到大取,这还需要根据具体问题去考虑。
例题:https://www.luogu.org/problemnew/show/P1757
其实这个概念本身意义不大,但是为了讨论接下来的依赖背包问题,所以我这里引入了这个在《背包九讲》中被一笔带过的概念。
假设一个背包的容量是V,那么对于某个物品,如果我们给它在背包中分配0…V的空间,都存在一个确定的w(v),也就是其对应的价值,那么我们称其为泛化的。而每一个泛化的物品,我们都可以将其转化为上述的分组背包中的一个组。同样地,分组背包中的任何一个组,我们都可以在如下操作之后将其转变为一件泛化的物品:
①由于最优性质,显然一个组中的v相同的物品,选择w更大的是更优的,所以可以将v重复的物品删去,剩下一个w最大的。此时所以出现过的v的位置上都已经有了确定的w(v)。
②对于剩下的所有位置(包括0),我们都将其置为0。显然,在背包中加入一个有体积而无价值的东西是更劣的,所以这些状态(0除外)都是永远不会被直接选到的,所以不会影响到最后结果。这样,在所有0…V的所有位置上都有了确定的w(v)。
由此,我们可以将一个组变成一件泛化的物品。方便起见,我们称其为一件泛化物品。类似地,我们还有01泛化物品,完全泛化物品等。
依赖背包是指,选取每样东西的时候,都可能会存在至多一个依赖物品,要选取了它的前置物品才能选取当前物品,且每样物品至多被选中一次。这样的话,我们可以将这个依赖关系用一条有向边表示,从而我们得到了一个森林。为了方便起见,我们虚构一个V=0,W=0的根节点,指向森林中每棵树的根节点。这样一来,这就成了一棵有根树上的背包问题。
我们不妨先找一道简单的例题:
https://www.luogu.org/problemnew/show/P1064
用上面的方法,我们可以将其画成一颗深度为3的树。其中第三层必定全是叶子节点,第二层中部分是叶子节点,部分是以叶子节点为孩子的节点,第一层是根节点。
对于任何一个节点,假设只考虑它和它的孩子,那么显然它自己是要被选中的。而对于它的每一个孩子,可以考虑投入0…V-1个体积,使得最后总体积为V。那么我们不妨把他的每一个孩子当成一个泛化背包,如此就可以递归求解。
接下来的问题就是,如何去表示这个泛化背包。
显然,对于所有的叶子节点,我们都可以将其看成是一个01泛化背包,这是非常容易理解的。
而对于非叶子节点,因为我们希望把它也变成一个泛化物品,所以我们可以穷举以该节点为根时在这里分配0…V的体积时可以得到的最大价值。怎么得到这个最大价值呢?我们可以用分组背包的思想,将它的泛化物品孩子们当成一个分组背包问题去求解。值得注意的是,因为依赖关系,所以当给这颗子树分配非0的体积时,一定会先把这个节点自己放进背包。
由此我们可以得到一个伪代码:
dfs(cur){
for v=V..0
dp[cur][v]=w[cur]//先把自己放进背包
for(cur的每一个孩子i)
dfs(i)
for v=V..0
for cost=v-1..0
dp[cur][v]=MAX(dp[cur][v],dp[cur][v-cost]+dp[son][cost])
//分组背包
}
实际操作的时候,可以用dfs序来实现,因为将某个点作为泛化物品更新他的父亲的时候,不要求它的兄弟也已经被做成了一个泛化背包。真实代码如下:
int n,V;
struct edge{
edge(){}
edge(int to,int next):to(to),next(next){}
int to,next;
}e[305];
int tail[305],cnt=0;
void add_edge(int from,int to){
e[++cnt]=edge(to,tail[from]);
tail[from]=cnt;
}
int dp[305][305];//dp[i][j]表示泛化背包i在v==j时所得到的w
int w[305];
void dfs(int cur){
for(int i=1;i<=V;++i)dp[cur][i]=w[cur];
for(int i=tail[cur];i;i=e[i].next){
int son=e[i].to;
dfs(son);
for(int v=V;v;--v){
for(int cost=v-1;cost;--cost){
dp[cur][v]=MAX(dp[cur][v],dp[cur][v-cost]+dp[son][cost]);
}
}
}
}
对于这个问题,还有这很多稀奇的拓展,比如每个节点都可以被取任意次,或者可以取有限次,对于这个问题,我们只要将代码中“将自己放进背包”一段改成这个节点的特征即可。这是一个非常容易处理的子问题,这里就不再多做赘述了。
例题:https://www.luogu.org/problemnew/show/P2014