背包问题总结

Section I 01背包&完全背包

这两种背包非常有意思,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

Section II 多重背包

比起上述两种背包,多重背包就要麻烦的多了。因为它可以取超过一个,却又有数量上限,这就要求状态中要储存当前物品已经用了多少个的信息。

一种方法是把多重背包拆成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

Section III 分组背包

所有的背包问题都可以用一个模型来求解,那就是分组背包问题。

分组背包是指在空间限制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

Section IV 泛化背包

其实这个概念本身意义不大,但是为了讨论接下来的依赖背包问题,所以我这里引入了这个在《背包九讲》中被一笔带过的概念。

假设一个背包的容量是V,那么对于某个物品,如果我们给它在背包中分配0…V的空间,都存在一个确定的w(v),也就是其对应的价值,那么我们称其为泛化的。而每一个泛化的物品,我们都可以将其转化为上述的分组背包中的一个组。同样地,分组背包中的任何一个组,我们都可以在如下操作之后将其转变为一件泛化的物品:

①由于最优性质,显然一个组中的v相同的物品,选择w更大的是更优的,所以可以将v重复的物品删去,剩下一个w最大的。此时所以出现过的v的位置上都已经有了确定的w(v)。
②对于剩下的所有位置(包括0),我们都将其置为0。显然,在背包中加入一个有体积而无价值的东西是更劣的,所以这些状态(0除外)都是永远不会被直接选到的,所以不会影响到最后结果。这样,在所有0…V的所有位置上都有了确定的w(v)。

由此,我们可以将一个组变成一件泛化的物品。方便起见,我们称其为一件泛化物品。类似地,我们还有01泛化物品完全泛化物品等。

Section V 依赖背包

依赖背包是指,选取每样东西的时候,都可能会存在至多一个依赖物品,要选取了它的前置物品才能选取当前物品,且每样物品至多被选中一次。这样的话,我们可以将这个依赖关系用一条有向边表示,从而我们得到了一个森林。为了方便起见,我们虚构一个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

你可能感兴趣的:(DP)