探讨与研究——动态规划算法、回溯法、分支限界法解0-1背包问题

一个人终归是要成长的,是要不断历练的,没有人可以安安稳稳一辈子。就算是最有地位最有钱的人也要不断追求、不断历练、不断提升自己。
人的学问少时在不断学习,青年时期不断实践。随着时间推移,到了老年终有所成,就是人口中的前辈、艺术家等等。这也是社会的普遍定律。还是一样,年少时学问再高,也高不过你年老时的学问。
不断历练,才能百炼成钢;必要的弯路绕也绕不过去,正视自我,正视现实,不断历练。必要的路该走就得走。不走起码你会后悔!
不断体验生活,不断历练自己;不断求得学问,不断获得收获;不断经历,不断实践。

本篇是讲一个NP-hard问题:0-1背包问题。为什么说它是一个NP-hard,首先你要知道什么是NP-hard。NP-hard,指所有NP问题都能在多项式时间复杂度内归约到的问题。也就是说0-1背包问题的时间复杂度是O(nb)虽然是一个多项式时间算法。然而b的规模是一个指数级的规模。所以0-1问题实际上是一个指数时间级的问题。现在还没有人去证明0-1背包问题存在多项式时间算法,自然就成为了本世纪最难的7个数学问题之一。所以算法的探讨与研究是非常重要的。

本篇不是要去证明0-1背包问题为什么是NP难或者是NPC问题。也不是要讲P、NP问题,更不是讲我在这一领域有了新发现,我还只是算法小白!

本篇是介绍三大算法,即动态规划算法、回溯法、分支限界法解0-1背包问题的内容。

0-1背包问题

什么是0-1背包问题

前几篇有一个是完全背包问题,那个是每种物品放多个的。而0-1背包问题是,有n种物品,每种物品只有1个,第i种物品价值为vi,重量为wi,i=1,2,…,n,问如何选择放入背包的物品,使得总重量不超过B,而价值达到最大

也就是说,完全背包问题在n个物品,限重为b下,只要不超重,可以拿多个同种装入背包;而0-1背包问题中,所有物品个数只有1个。即使在不超重也只能拿1个同种物品。也就是说对于每个物品要么装要么不装。这就是和完全背包问题的区别。

下面我们对0-1背包问题举个例子:
这里有4种物品,其物品重量分别是8,6,4,3,而对应它们的价值是:12,11,9,8。令背包限重是13
我们经过计算得到最优解是<0,1,1,1>,此时的价值是28,重量是13

下面我们分别用三种算法来解这个问题,首先来看动态规划算法

动态规划算法解0-1背包问题

和完全背包问题也是需要去创建一个二维表记录数据
令Fk(y):装前k种物品,总重不超过y,背包达到的最大重量

我们首先思考递推方程:
我们首先将第一种物品添加进背包里,只需要让第一个物品重量大于背包重量就可以了。则F1(y)=v1,y>=w1

下面考虑第k(k>1)个物品的情况
如果在y下物品不放进背包,那么这个背包的价值是不是第k-1个物品的价值啊。因为前k个物品的最优子结构是已经计算出来了,而且都存在了备忘录里面了。
即Fk(y)=Fk-1(y)

如果第k(k>1)个物品可以装入背包,那说明背包此时的重量是>=第k个物品的重量了。那么怎么计算此时的最大价值呢?
仿照完全背包问题一样,我们可以把此时的第k个物品拿出去,因为一个物品是能放一个,那么你拿出去了,是不是至多里面含k-1个物品了。即Fk-1(y),在这之后加上第k个物品的价值不就是此时的Fk(y)。两者进行比较取最大值就可以了。
即Fk(y)=max{Fk-1(y),Fk-1(y-wk)+vk}

而完全背包是怎么写的?
完全背包是Fk(y)=max{Fk-1(y),Fk(y-wk)+vk},为什么不一样,那就是因为完全背包的同种物品是可以装多个的,而0-1背包物品的同种物品只能装1个。如果你按照完全背包那样写,最后的结果就是第k个物品你装了多个。

因此我们得到了递推方程是:
Fk(y)=max{Fk-1(y),Fk-1(y-wk)+vk};
初值:
F1(y)=v1,y>=w1;
Fk(y)=-∞,y<0;
F0(y)=0,0<=y<=b;
Fk(0)=0,0<=k<=n

接下来进行代码分析:
我们得出了递归方程,那么对于代码动态规划的代码编写就很简单了。下面介绍一下,如何追踪解。

追踪解应该是自底向上追踪,我们令ik(y):在重量y的时候,第k个物品有没有装(这里装了的话就是1;没有装就是0)。
大家再想,如果在b重量(最大限重)下装了第k个物品,那么价值是不是就会更新,是不是就会比装k-1个物品的价值要大;如果这个价值还是和第k-1号物品的价值一样,那么是不是就没有装入第k个物品啊。就依照这样的想法来追踪解。

首先确定in(b)与in-1(b)是否相等,假设不相等的话,那就in(b)=1,再判断in-1(b-wk)与
in-2(b-wk),如果不相等,就使in-2(b-wk)=0,再判断in-3(b-wk)和in-4(b-wk)进行比较。就这样以此类推下去就行了。
最后怎么处理i1(y)呢。如果此时有重量,那么i1(y)=1;重量为0,那么i1(y)=0。这个很容易就能想出来。

最后再将ik(y)里面含1的输出出来就行了。

下面我们根据这样的思路来编写代码

public class Materia {
	public int weight;
	public int value;
	
	public Materia() {
		// TODO Auto-generated constructor stub
	}

	public Materia(int weight, int value) {
		super();
		this.weight = weight;
		this.value = value;
	}
	
	
}

public static void main(String[] args) {
		// TODO Auto-generated method stub
		Materia materia[]=new Materia[6];
		materia[0]=new Materia(4, 8);
		materia[1]=new Materia(6, 10);
		materia[2]=new Materia(2, 6);
		materia[3]=new Materia(2, 3);
		materia[4]=new Materia(5, 7);
		materia[5]=new Materia(1, 2);
		
		int b=12;  //背包最大重量限制
		int m[][]=new int[materia.length][b+1];  //背包现有价值
		int d[]=new int[materia.length];  //追踪解
		
		int maxValue=MaxloadingValue(materia, b, m, d);
		
		System.out.println("背包的最大价值是:"+maxValue);
		System.out.print("选取物品的最优解是:");
		for(int i=0;i<d.length;i++)
			if(d[i]==1)
				System.out.print(i+1+"  ");
	}

	//动态规划算法解0-1背包问题
	public static int MaxloadingValue(Materia materia[],int b,int m[][],int d[]) {
		int maxWeight=b;
		//第一个物品装包情况
		for(int i=1;i<=b;i++) {
			if(i>=materia[0].weight) 
				m[0][i]=materia[0].value;
		}
		
		for(int i=1;i<materia.length;i++)
			for(int j=1;j<=b;j++) {
				if(j>=materia[i].weight)  
					m[i][j]=Math.max(m[i-1][j], m[i-1][j-materia[i].weight]+materia[i].value);
				else
					m[i][j]=m[i-1][j];
			}
		
		//至于解的设置,如果m[i][b]=m[i-1][b],则说明第i个物品没有装进去。否则就装进去,令d[i]=1
		for(int i=materia.length-1;i>0;i--) 
			if(m[i][b]==m[i-1][b])
				d[i]=0;
			else
			{
				d[i]=1;
				b-=materia[i].weight;
			}
		d[0]=m[0][b]>0?1:0;
		return m[materia.length-1][maxWeight];
	}

下面进行回溯法解0-1背包问题

回溯法解0-1背包问题

首先这个问题,它是一个要么装要么不装的问题,即搜索空间是一棵子集树。

约束条件就是:装第k个物品时候是否<=背包最大限重b。

我们将当前背包的最大价值当作界

下面思考代价函数。那必须得让背包装的价值越多越好,质量越少越好。那么我们可以用单位价值重量来进行评判。也就是让背包装的单位价值重量越大越好

如何判断装入第k个物品的最大价值是什么?我们令单位价值重量:vk/wk>vk+1/wk+1>vk+2/wk+2
我们按照装入单位价值重量越大越好的标准来装。首先背包已经是有了一部分价值了,那么第k个物品装进去了,第k+1个物品也装进去了。再装第k+2个物品的时候,超重了。但是背包还留有一定的重量y。我们把这部分重量用第k+2个物品填一下。(vk+2/wk+2)·y,这个公式求出来是不是就是剩余空间装入第k+2个物品的时候的价值了。然后让前面的装好的,就是装到了第k+1的时候的那个价值+(vk+2/wk+2)·y,是不是就是此时背包在k结点的最大价值了。

也许你看到这里,你就明白了。我们首先得进行预处理,将这几个物品按照单位价值重量进行降序排序,排好序后,再进行搜索就可以了。这就是代价函数的求解

代码的编写,因为这个0-1背包问题是一棵子集树,那么我们就按照子集树的模板来写,
判断是否满足约束条件——计算、x[i]=1——递归左子树——归还——x[i]=0、递归右子树(注意限界思想)
判断是否到达叶子结点、是否没到达叶子结点。
详情可以参考《回溯法与分支限界法的总结》、《回溯法的一个应用:最优装载问题》对子集树代码的分析。

下面直接上代码

public class Materia {
	public int index;  //物品编号
	public int weight;
	public int value;
	
	public Materia() {
		// TODO Auto-generated constructor stub
	}

	public Materia(int index,int weight, int value) {
		super();
		this.index=index;
		this.weight = weight;
		this.value = value;
	}
	
	
}

public class Element implements Comparable{

	int id;  //物品编号
	double p;  //单位重量价值(vi/wi)
	
	public Element() {
		// TODO Auto-generated constructor stub
	}
	
	
	public Element(int id, double p) {
		super();
		this.id = id;
		this.p = p;
	}

	//将单位物品价值升序排序
	@Override
	public int compareTo(Object o) {
		// TODO Auto-generated method stub
		double d=((Element)o).p;
		if(p<d) return -1;
		else if(p==d) return 0;
		return 1;
	}
	
	@Override
	public boolean equals(Object obj) {
		// TODO Auto-generated method stub
		return p==((Element)obj).p;
	}
	
}

import java.util.Arrays;

public class KnapSack {
	Materia materia[];  //物品类
	Element q[];  //存储单位重量价值
	Materia tempMateria[];  //暂存排序后的物品类
	
	public KnapSack() {
		// TODO Auto-generated constructor stub
	}

	public KnapSack(Materia[] materia) {
		super();
		this.materia = materia;
		this.q = new Element[materia.length];
	}
	
	
	public Materia[] Sort() {
		for(int i=0;i<q.length;i++)
			q[i]=new Element(materia[i].index, ((double)materia[i].value/materia[i].weight));
		Arrays.sort(q);
		
		tempMateria=new Materia[q.length];
		//重新装入物品属性
		for(int i=0;i<q.length;i++) 
			tempMateria[i]=materia[q[q.length-i-1].id-1];
		
		return tempMateria;
	}
	
}

public static int nowWeight;  //当前重量
	public static int nowValue;  //当前价值
	public static int maxValue=-0x3f3f3f3f;  //最优价值
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Materia materia[]=new Materia[6];
		materia[0]=new Materia(1, 4, 8);
		materia[1]=new Materia(2, 6, 10);
		materia[2]=new Materia(3, 2, 6);
		materia[3]=new Materia(4, 2, 3);
		materia[4]=new Materia(5, 5, 7);
		materia[5]=new Materia(6, 1, 2);
		
		int b=12;  //背包最大重量限制
		int x[]=new int[materia.length];  //当前存储解
		int bestx[]=new int[materia.length];  //记录最优解
		
		//初始化
		KnapSack knapSack=new KnapSack(materia);
		Materia[] tempMateria = knapSack.Sort();
		MaxloadingValue(tempMateria, b, x, bestx, 0);
		
		System.out.println("背包的最大价值是:"+maxValue);
		
		System.out.print("选取物品的最优解是:");
		for(int i=0;i<bestx.length;i++)
			if(bestx[i]==1)
				System.out.print(tempMateria[i].index+"  ");
			   
	}

	//回溯法解0-1背包问题
	public static void MaxloadingValue(Materia tempMateria[],int b,int x[],int bestx[],int t) {
		//到达叶子结点
		if(t==tempMateria.length) {
			if(nowValue>maxValue) {
				for(int i=0;i<x.length;i++)
					bestx[i]=x[i];
				maxValue=nowValue;
			}
			return;
		}
		
		//未到达叶子结点
		if(nowWeight+tempMateria[t].weight<=b) {
			x[t]=1;
			nowWeight+=tempMateria[t].weight;
			nowValue+=tempMateria[t].value;
			MaxloadingValue(tempMateria, b, x, bestx, t+1);  //进入左子树
			nowWeight-=tempMateria[t].weight;
			nowValue-=tempMateria[t].value;
		}
		
		//剪枝操作
		if(Bound(b, t+1, tempMateria)>maxValue) {
			x[t]=0;
			MaxloadingValue(tempMateria, b, x, bestx, t+1);  //进入右子树
		}
	}
	
	//设置代价函数,进行剪枝操作
	public static double Bound(int b,int t,Materia tempMateria[]) {
		//计算剩余重量和当前价值
		int surplusWeight=b-nowWeight;
		double value=nowValue;
		//以物品单位重量价值递减顺序装入物品
		while(t<tempMateria.length&&tempMateria[t].weight<=surplusWeight) {
			surplusWeight-=tempMateria[t].weight;
			value+=tempMateria[t].value;
			t++;
		}
		
		//用剩余空间补满下一个物品
		//剩余空间*下一个物品的单位重量价值,让背包最大限度的填满整个物品,获得最大价值
		if(t<tempMateria.length)
			value+=(double)tempMateria[t].value*surplusWeight/tempMateria[t].weight;
		return value;
	}

最后我们用分支限界法解0-1背包问题

分支限界法解0-1背包问题

这里我们用的基于优先队列解0-1背包问题
首先是要做分支限界法的准备,创建活结点类(父结点、左子树结点)、活结点属性类(活结点类、价值上界、当前价值、当前重量、层数)、入队类

准备这些之后,别忘了还有一个要求代价函数的准备,即根据单位价值重量进行排序。

这些类建完之后,就可以编写代码了。

这里我想介绍如何确定结点的价值上界,和这个算法的问题。

比如说对于第1个物品(初始单位价值重量最大的物品)的价值上界怎么求呢?
首先先说清楚这个价值上界和限界思想的界的含义是不一样的,前者是说的结点的,后者说的是原问题的界,而且后者的界是你要求解的原问题的最大价值量

我们仿照最大团那样的代码逻辑思考,实际上这个结点的价值上界,就是纳入这个结点的背包此时可装载的最大价值量。
令Bound(i):考虑装第i个物品时候的背包可以装载的最大价值量。
例如如果是第一个物品可以装,那么价值上界就是Bound(1)。什么意思,这个不就是代价函数。即使要纳入第二个结点,它的价值上界不还是Bound(1)嘛,
如果第一个物品不可以装,那么此时价值上界就是Bound(2)。
为什么要这么算?
你从第1个点开始搜索,要准备搜索第2个点的时候,是要生成第2个点的数据,换句话说就是要把第2个点的信息要添加到队列里面去。所以说我算的界算的是在第2个点可能使背包产生的最大价值量
换句话说就是纳入还是不纳入这个点,对于下一个点可能使背包产生的最大价值量

当然优先队列排序是基于结点的价值上界的降序排序

下面说说这个算法的问题,这个算法可能不是一个很好的广度搜索,它更像是一个深度搜索。是盲目搜索(不可预测本结点以下的结点进行的如何)。所以要F>=B,不然就会出现队列为空的现象。队列为空就不知道结点搜索到哪里了。

接下来直接上代码

public class Materia {
	public int index;  //物品编号
	public int weight;
	public int value;
	
	public Materia() {
		// TODO Auto-generated constructor stub
	}

	public Materia(int index,int weight, int value) {
		super();
		this.index=index;
		this.weight = weight;
		this.value = value;
	}
	
	
}


public class Element implements Comparable{

	int id;  //物品编号
	double p;  //单位重量价值(vi/wi)
	
	public Element() {
		// TODO Auto-generated constructor stub
	}
	
	
	public Element(int id, double p) {
		super();
		this.id = id;
		this.p = p;
	}

	//将单位物品价值升序排序
	@Override
	public int compareTo(Object o) {
		// TODO Auto-generated method stub
		double d=((Element)o).p;
		if(p<d) return -1;
		else if(p==d) return 0;
		return 1;
	}
	
	@Override
	public boolean equals(Object obj) {
		// TODO Auto-generated method stub
		return p==((Element)obj).p;
	}
	
}


import java.util.Arrays;

public class KnapSack {
	Materia materia[];  //物品类
	Element q[];  //存储单位重量价值
	Materia tempMateria[];  //暂存排序后的物品类
	
	public KnapSack() {
		// TODO Auto-generated constructor stub
	}

	public KnapSack(Materia[] materia) {
		super();
		this.materia = materia;
		this.q = new Element[materia.length];
	}
	
	
	public Materia[] Sort() {
		for(int i=0;i<q.length;i++)
			q[i]=new Element(materia[i].index, ((double)materia[i].value/materia[i].weight));
		Arrays.sort(q);
		
		tempMateria=new Materia[q.length];
		//重新装入物品属性
		for(int i=0;i<q.length;i++) 
			tempMateria[i]=materia[q[q.length-i-1].id-1];
		
		return tempMateria;
	}
	
}


public class KsNodes {
	KsNodes parents;  //父结点
	boolean leftchild;  //左子树
	
	public KsNodes() {
		// TODO Auto-generated constructor stub
	}

	public KsNodes(KsNodes parents, boolean leftchild) {
		super();
		this.parents = parents;
		this.leftchild = leftchild;
	}
}


public class HeapKsNodes implements Comparable{
	KsNodes node;
	double upBound;  //价值上界
	int value;  //当前价值
	int weight;  //当前重量
	int depth;  //当前深度
	
	public HeapKsNodes() {
		// TODO Auto-generated constructor stub
	}
	

	public HeapKsNodes(KsNodes node, double upBound, int value, int weight, int depth) {
		super();
		this.node = node;
		this.upBound = upBound;
		this.value = value;
		this.weight = weight;
		this.depth = depth;
	}



    //将上界值降序排序
	@Override
	public int compareTo(Object o) {
		// TODO Auto-generated method stub
		double upper=((HeapKsNodes)o).upBound;
		if(upBound<upper) return 1;
		else if(upBound==upper) return 0;
		return -1;
	}
	
}


import java.util.Collections;
import java.util.LinkedList;

public class CreateKsNode {
	Materia tempMateria[];
	static LinkedList<HeapKsNodes> list;  //创建优先队列
	
	public CreateKsNode() {
		// TODO Auto-generated constructor stub
	}

	public CreateKsNode(Materia tempMateria[]) {
		super();
		this.tempMateria = tempMateria;
		this.list=new LinkedList<HeapKsNodes>();
	}
	
	public static void addNode(KsNodes nodes, double upBound, int value, int weight, int depth,boolean leftchild) {
		KsNodes node=new KsNodes(nodes, leftchild);
		HeapKsNodes heapKsNodes=new HeapKsNodes(node, upBound, value, weight, depth);
		list.add(heapKsNodes);
		Collections.sort(list);
	}
}


public class ZYBaggage {
	public static int nowWeight;  //当前重量
	public static int nowValue;  //当前价值
	public static int maxValue=-0x3f3f3f3f;  //最优价值
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Materia materia[]=new Materia[6];
		materia[0]=new Materia(1, 4, 8);
		materia[1]=new Materia(2, 6, 10);
		materia[2]=new Materia(3, 2, 6);
		materia[3]=new Materia(4, 2, 3);
		materia[4]=new Materia(5, 5, 7);
		materia[5]=new Materia(6, 1, 2);
		
		int b=12;  //背包最大重量限制
		int bestx[]=new int[materia.length];  //记录最优解
		
		//初始化
		KnapSack knapSack=new KnapSack(materia);
		Materia[] tempMateria = knapSack.Sort();
		CreateKsNode createKsNode=new CreateKsNode(tempMateria);
		
		MaxloadingValue(tempMateria, b, bestx, 0);
		
        System.out.println("背包的最大价值是:"+maxValue);
		
		System.out.print("选取物品的最优解是:");
		for(int i=0;i<bestx.length;i++)
			if(bestx[i]==1)
				System.out.print(tempMateria[i].index+"  ");
	}

	//分支限界法解0-1背包问题
	public static void MaxloadingValue(Materia tempMateria[],int b,int bestx[],int t) {
		//初始化结点
		KsNodes nodes=null;
		double up=Bound(b, 0, tempMateria);
		
		//搜索子集空间树
		while(t!=tempMateria.length) {
			//左孩子结点是否满足约束条件
			if(nowWeight+tempMateria[t].weight<=b) {
				//如果装上物品的价值超过最大价值,则更新最大价值。
				if(nowValue+tempMateria[t].value>maxValue)
					maxValue=nowValue+tempMateria[t].value;
				//添加左子树结点
				CreateKsNode.addNode(nodes, up, nowValue+tempMateria[t].value, nowWeight+tempMateria[t].weight, t+1, true);
			}
			//否则进入右子树
			//首先上界更新(上界不包括t=0)
			up=Bound(b, t+1, tempMateria);
			//剪枝操作
			if(up>=maxValue)
				//添加右子树结点
				CreateKsNode.addNode(nodes, up, nowValue, nowWeight, t+1, false);
			HeapKsNodes heapKsNodes=CreateKsNode.list.poll();
			nodes=heapKsNodes.node;  //取下一个结点
			up=heapKsNodes.upBound;
			nowWeight=heapKsNodes.weight;
			nowValue=heapKsNodes.value;
			t=heapKsNodes.depth;
		}
		//构造最优解
		for(int j=bestx.length-1;j>=0;j--) {
			//如果结点的leftchild为true,说明结点可以纳入最优解中
			bestx[j]=nodes.leftchild?1:0;
			//寻求父结点
			nodes=nodes.parents;
		}
	}
	
	//设置代价函数,进行剪枝操作
		public static double Bound(int b,int t,Materia tempMateria[]) {
			//计算剩余重量和当前价值
			int surplusWeight=b-nowWeight;
			double value=nowValue;
			//以物品单位重量价值递减顺序装入物品
			while(t<tempMateria.length&&tempMateria[t].weight<=surplusWeight) {
				surplusWeight-=tempMateria[t].weight;
				value+=tempMateria[t].value;
				t++;
			}
			
			//用剩余空间补满下一个物品
			//剩余空间*下一个物品的单位重量价值,让背包最大限度的填满整个物品,获得最大价值
			if(t<tempMateria.length)
				value+=(double)tempMateria[t].value*surplusWeight/tempMateria[t].weight;
			return value;
		}
0-1背包问题分析

你无论用什么算法,它的时间复杂度就是指数级的时间。差不多就是O(2^n)
而且现在不存在一个多项式时间的算法,它是一个NP-hard问题

以上就是0-1背包问题的内容

         日既暮而犹烟霞绚烂,岁将晚而更橙橘芳馨。故末路晚年,君子更宜精神百倍。《菜根谭》
  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值