一个人终归是要成长的,是要不断历练的,没有人可以安安稳稳一辈子。就算是最有地位最有钱的人也要不断追求、不断历练、不断提升自己。
人的学问少时在不断学习,青年时期不断实践。随着时间推移,到了老年终有所成,就是人口中的前辈、艺术家等等。这也是社会的普遍定律。还是一样,年少时学问再高,也高不过你年老时的学问。
不断历练,才能百炼成钢;必要的弯路绕也绕不过去,正视自我,正视现实,不断历练。必要的路该走就得走。不走起码你会后悔!
不断体验生活,不断历练自己;不断求得学问,不断获得收获;不断经历,不断实践。
本篇是讲一个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背包问题是,有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
下面我们分别用三种算法来解这个问题,首先来看动态规划算法
和完全背包问题也是需要去创建一个二维表记录数据
令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背包问题
首先这个问题,它是一个要么装要么不装的问题,即搜索空间是一棵子集树。
约束条件就是:装第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背包问题
首先是要做分支限界法的准备,创建活结点类(父结点、左子树结点)、活结点属性类(活结点类、价值上界、当前价值、当前重量、层数)、入队类
准备这些之后,别忘了还有一个要求代价函数的准备,即根据单位价值重量进行排序。
这些类建完之后,就可以编写代码了。
这里我想介绍如何确定结点的价值上界,和这个算法的问题。
比如说对于第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;
}
你无论用什么算法,它的时间复杂度就是指数级的时间。差不多就是O(2^n)
而且现在不存在一个多项式时间的算法,它是一个NP-hard问题
以上就是0-1背包问题的内容