回溯法 —— 求解0/1背包问题(剪枝)

0/1背包问题

题目描述:

有n个重量分别为w1,w2,…,wn的物品(物品编号为1~n),它们的价值分别为v1,v2,…,vn,给定一个容量为W的背包。设计从这些物品中选取一部分物品放入该背包的方案,每个物品要么选中要么选不中,要求选中的物品不仅能够放到背包中,而且具有最大价值,并对下表所示4个物品求出W=6时的所有解和最佳解。

在这里插入图片描述

分析思路:回溯法

设n件物品重量分别为w1,w2,…,wn的物品,用数组w[1 … n]存放,物品的价值分别为v1,v2,…,vn,用数组v[n]存放;限制重量用W表示。用x[1 … n]数组存放最优解,其中每个元素取1或0,x[i]=1表示第i个物品放入背包中,x[i]=0表示第i个物品不放入背包中。

最优解问题,显然其解空间是子集树(每个物品要么装入,要么不装入),每个结点表示背包的一种选择状态,记录当前放入背包的物品总重量和总价值,每个分枝结点下面有两条边表示对某物品是否放入背包的两种可能选择。

第i层上的某个分枝结点的对应状态为dfs(1,tw,tv,op),其中
tw:表示装入背包中的的物品总重量
tv:表示背包中物品总价值
op:记录一个解向量

时间复杂度:O(2^n)

该状态两种扩展如下:

(1)选择第i个物品放入背包中:op[i]=1,tw=tw+w[i],tv=tv+v[i],转向下一个状态dfs=(i+1,tw,tv,op).该对策对应左分枝

(2)不选择第i个物品放入背包:op[i]=0,tw=tw不变,tv不变,转向下一个状态dfs=(i+1,tw,tv,op).该对策对应右分枝

叶子结点表示已经对n个物品做了决策,对应一个解。对所有叶子结点进行比较求出满足tw=W的最大tv(用maxw表示),将对应的最优解op存放到x中

对于表4.2所示的4个物品,在限制背包总重量W=6时描述问题求解过程的解空间树如图所示,每个结点中的两个数值为(tw,tv)

对于层次为1的根结点为(0,0),考虑物品1

(1)选择物品1:op[1]=1,tw=0+5,tv=0+4,产生新结点(5,4)作为根的左孩子

(2)不选择物品1:op[1]=0,tw=0,tv=0,产生新结点(0,0)作为根的右孩子

在这里插入图片描述

对于层次2的(5,4)结点,考虑物品2

(1)选择物品2:op[2]=1,tw=5+3=8,tv=4+4=8,产生新结点(8,8)作为其左孩子。

(2)不选择物品2:op[2]=0,tw=5,tv=4,产生新结点(5,4)作为其右孩子。

这样可构造出整棵子集树31个结点

递归代码:

#include<stdio.h>
#define MAXN 20  //最多20个物品 

int  n=4;   //4种物品 
int W=6;   //最大重量6 
int w[]={
    
    0,5,3,2,1};  //存放4个物品重量,下标为0元素不用 
int v[]={
    
    0,4,4,3,1};  //存放4个物品价值,下标为0元素不用 

int x[MAXN];  //最终解 
int maxv;   //最优解 
void dfs(int i,int tw,int tv,int op[])
{
    
    
	if(i>n)   //找到一个叶子结点
	{
    
    
		if(tw==W&&tv>maxv)  //找到一个最优解
		{
    
    
			maxv=tv;
			for(int j=1;j<=n;j++)
			x[j]=op[j];
		 } 
	 } 
	 else   //未找完所有物品 
	 {
    
    
	 	op[i]=1;  //选取第i个物品
		 dfs(i+1,tw+w[i],tv+v[i],op);
		 op[i]=0;
		 dfs(i+1,tw,tv,op); 
	 }	 
}
void dispsolution()  //最优解
{
    
    
	int i;
	printf("最优解:\n");
	for(i=1;i<=n;i++)
	if(x[i]==1)
	printf(" 选取第%d个物品\n",i);
	printf("总重量=%d,总价值=%d\n",W,maxv);
 } 
 int main(){
    
    
 	int op[MAXN];  //存放临时解 
 	dfs(1,0,0,op);  //i从1开始 
 	dispsolution();
 	return 0;
 }
 

在这里插入图片描述

对于第i层的有些结点,tw+w[i]已经超过了W,显然再选择w[i]不合适。
例第2层的(5,4)结点,tw=5,w[2]=3.而tw+w[2]>W,选择物品2进行扩展是不必要的,可以增加一个限界条件进行剪枝,如果选择物品i会导致超重,即tw+w[i]>W,就不再扩展该结点,也就仅仅扩展tw+w[i]<=W的左孩子结点。
剪枝后解空间如图(21个结点)

在这里插入图片描述

左孩子剪枝DFS代码:

void dfs(int i,int tw,int tv,int op[])
{
    
    
	if(i>n)   //找到一个叶子结点
	{
    
    
		if(tw==W&&tv>maxv)  //找到一个最优解
		{
    
    
			maxv=tv;
			for(int j=1;j<=n;j++)
			x[j]=op[j];
		 } 
	 } 
	 else   //未找完所有物品 
	 {
    
    
	 	if(tw+w[i]<=W){
    
    
	 	op[i]=1;  //选取第i个物品
		 dfs(i+1,tw+w[i],tv+v[i],op);
	}
		 op[i]=0;
		 dfs(i+1,tw,tv,op); 
	 }	 
}

只对左子树进行剪枝,没有对右子树进行剪枝,实际也可以进行对右子树进行剪枝。用rw表示考虑第i个物品时剩余的物品重量,即rw=w[i]+ … +w[n] (初始时rw是所有物品的重量和),对于第i层上的某个分枝结点,将对应的状态改为dfs(i,tw,tv,rw,op),对应两种扩展如下。

(1)选择第i个物品放入背包中:op[i]=1,如果放入物品i不超重吗,则tw=tw+w[i],tv=tv+v[i],rw=rw-w[i],转向下一个状态dfs=(i+1,tw,tv,rw,op).该对策对应左分枝

(2)不选择第i个物品放入背包:op[i]=0,tw不变,tv不变,rw=rw-w[i] (无论是否选择物品i,rw都是剩余没有考虑的物品重量和)转向下一个状态dfs=(i+1,tw,tv,op).该对策对应右分枝。

当不选择物品i时,若tw+rw<W(tw中包含w[i]),也就是说即使选择后面的所有物品,重量也不会达到W,因此可以不考虑扩展这样的结点,从而产生进一步剪枝的解空间,如图,有9个结点,

在这里插入图片描述

对于图中第2层的(0,0)结点,此时tw=0,rw=6(物品2、物品3、物品4的重量和),tw+rw=6,不大于W(此时又不选择物品2)所以不必扩展其右孩子。

右孩子剪枝代码:

void dfs(int i,int tw,int tv,int op[])
{
    
    
	int j;   //初始调用时rw为所有物品的重量和 
	if(i>n)   //找到一个叶子结点
	{
    
    
		if(tw==W&&tv>maxv)  //找到一个最优解
		{
    
    
			maxv=tv;
			for(int j=1;j<=n;j++)  //复制最优解 
			x[j]=op[j];
		 } 
	 } 
	 else   //未找完所有物品 
	 {
    
    
	 	if(tw+w[i]<=W){
    
    
	 	op[i]=1;  //选取第i个物品
		 dfs(i+1,tw+w[i],tv+v[i],rw-w[i],op);
	}
		 op[i]=0;   //不选取物品i,回溯 
		 if(tw+rw>W)   //右孩子结点剪枝 
		 dfs(i+1,tw,tv,op); 
	 }	 
}

猜你喜欢

转载自blog.csdn.net/gl620321/article/details/108698065