java 0-1背包问题 动态规划、回溯法、分支限界
1.什么是0-1背包问题
有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?
注:与普通背包问题不同,0-1背包问题中,物品以整体的形式出现,只能选择整体放入背包或整体不放入背包。
2.实验数据
3.解决思路
(1)动态规划
算法思想:
动态规划是解决0-1背包问题常用的解决办法,时间复杂度为o(c*n),相对于回溯法大大减少了时间复杂度,可以用于背包空间较大和物品数量较多的情况。
动态规划与分治法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。但不同的是,分治法在子问题和子子问题等上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案记录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。
最优性原理是动态规划的基础,最优性原理是指“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”。
背包问题的解决过程:
在解决问题之前,为描述方便,首先定义一些变量:Vi表示第 i 个物品的价值,Wi表示第 i 个物品的体积,定义V(i,j):当前背包容量 j,前 i 个物品最佳组合对应的价值,同时背包问题抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 个物品选或不选)。
(1)建立模型,即求max(V1X1+V2X2+…+VnXn);
(2)寻找约束条件,W1X1+W2X2+…+WnXn<capacity;
(3)寻找递推关系式,面对当前商品有两种可能性:
包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j);
还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}。
其中V(i-1,j)表示不装,V(i-1,j-w(i))+v(i) 表示装了第i个商品,背包容量减少w(i),但价值增加了v(i);
由此可以得出递推关系式:
1、j<w(i) V(i,j)=V(i-1,j)
2、j>=w(i) V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)
代码实现:
//动态规划
public static void method1(int[] w,int[] v,int m,int n) {
//最大价值数组为maxvalue[N+1][maxWeight+1],从0开始保存
int[][] maxvalue = new int[n+1][m + 1];
int[] result=new int[n+1];
//重量和物品为0时,价值为0
for (int i = 0; i < m + 1; i++) {
maxvalue[0][i] = 0;
}
for (int i = 0; i < n + 1; i++) {
maxvalue[i][0] = 0;
result[i]=0;
}
//i:只拿前i件物品(这里的i因为取了0,所以对应到weight和value里面都是i-1号位置)
//j:假设能取的总重量为j
//n是物品件数
for (int i = 1; i <= n ; i++) {
for (int j = 1; j <= m; j++) {
//当前最大价值等于放上一件的最大价值
maxvalue[i][j] = maxvalue[i-1][j];
//如果当前件的重量小于总重量,可以放进去或者拿出别的东西再放进去
if (w[i-1] <= j) {
//比较(不放这个物品的价值)和
//(这个物品的价值 加上 当前能放的总重量减去当前物品重量时取前i-1个物品时的对应重量时候的最高价值)
if(maxvalue[i-1][j - w[i-1]] + v[i-1]>maxvalue[i-1][j]) {
maxvalue[i][j] = maxvalue[i-1][j - w[i-1]] + v[i-1];
}
}
}
}
System.out.println("背包最大价值为:"+maxvalue[n][m]);
}
实验效果:
(2)回溯法
算法思想:回溯法解决0-1背包问题并不是一个比较好的办法,它的时间复杂度为O(2n),复杂度为指数级,这种方法在物品数量多,背包空间大的情况下,求解时间很长。这种方法采取的思想是一件件的选择放入还是不放入,然后放到不能再放为止,计算此时的总价值,之后再回退到之前的状态,更改刚才放入的操作。下面我写的是没有剪枝操作的回溯法,相当于全排列。
代码实现:
//回溯算法
static class back {
public int[] weight;
public int[] value;
public int[] take;
int curWeight = 0;
int curValue = 0;
int bestValue = 0;
int[] bestChoice;
int maxWeight = 0;
int count;
public back(int[] weight, int[] value, int maxWeight,int n) {
this.value = value;
this.weight = weight;
this.maxWeight = maxWeight;
this.count=n;
take = new int[n];
bestChoice = new int[n];
}
public int[] backtrack(int x) {
//终止条件
if (x >count - 1) {
//更新最优解
if (curValue > bestValue) {
bestValue = curValue;
for (int i = 0; i < take.length; i++) {
bestChoice[i] = take[i];
}
}
}
else { //遍历当前节点(物品)的子节点:0 不放入背包 1:放入背包
for (int i = 0; i < 2; i++) {
take[x] = i;
if (i == 0) {
//不放入背包,接着往下走
backtrack(x + 1);//递归下一个物品
} else {
//约束条件,如果小于背包容量
if (curWeight + weight[x] <= maxWeight) {
//更新当前重量和价值
curWeight += weight[x];
curValue += value[x];
//继续向下深入
backtrack(x + 1);
//回溯
curWeight -= weight[x];
curValue -= value[x];
}
}
}
}
return bestChoice;
}
}
public static void method2(int[] w,int[] v,int m,int n) {
back bt=new back(w,v,m,n);
int[] result = bt.backtrack(0);
System.out.print("最佳选择为:[");
for(int i=0;i<bt.bestChoice.length;i++) {
if(i==bt.bestChoice.length-1) {
System.out.print(bt.bestChoice[i]+"]");
}else {
System.out.print(bt.bestChoice[i]+",");
}
}
System.out.print("\n此时价值最大,即"+bt.bestValue+"\n");
}
实验效果:
(3)分支限界法
算法思路:
1.先求出每个物品的单位重量的价值,并按降序排序;按此顺序取物品,取或不取的时候都计算出上界:ub=cv+(bagw-cw)(value[i]/weight[i]),即(当前价值+最佳回报*剩余重量);
2. 比较两种情况下的上界ub,选择界限大的方式。前提是选取该物品后总重不会超过背包容量:bagw;
3. 一个物品要么选,要么不选,选了就往选了的方式下继续分支,重复2,3,没选就往没选的方式下分支。
代码实现:
//分支限界
static class FZXJProblem {
// 准备放入背包中的物品
public int[] weight;
public int[] value;
public float[] ave;
// 背包的总承重
private int totalWeight;
// 给定的物品数
private int n;
// 物品放入背包可以获得的最大价值
private int bestValue;
public FZXJProblem(int[] w,int[] v,int n, int totalWeight) {
super();
this.weight=w;
this.value=v;
this.totalWeight = totalWeight;
this.n = n;
this.ave=new float[n];
// 物品依据单位重量价值进行排序
float[] ave1=new float[n];
for(int i=0;i<n;i++) {
ave1[i]=v[i]/w[i];
}
Arrays.sort(ave1);
int j=n-1;
for(int i=0;i<n;i++) {
this.ave[i]=ave1[j--];
}
}
// 队列式分支限界法
public void solve() {
LinkedList<Node> nodeList = new LinkedList<Node>();
// 起始节点当前重量和当期价值均为0
nodeList.add(new Node(0, 0, 0));
while (!nodeList.isEmpty()) {
// 取出放入队列中的第一个节点
Node node = nodeList.pop();
if (node.upboundValue >= bestValue && node.index < n) {
// 左节点:该节点代表物品放入背包中,上个节点的价值+本次物品的价值为当前价值
int leftWeight = node.currWeight + weight[node.index];
int leftValue = node.currValue + value[node.index];
Node left = new Node(leftWeight, leftValue, node.index + 1);
// 放入当前物品后可以获得的价值上限
left.upboundValue = getUpboundValue(left);
// 当物品放入背包中左节点的判断条件为保证不超过背包的总承重
if (left.currWeight <= totalWeight
&& left.upboundValue > bestValue) {
// 将左节点添加到队列中
nodeList.add(left);
if (left.currValue > bestValue) {
// 物品放入背包不超重,且当前价值更大,则当前价值为最大价值
bestValue = left.currValue;
}
}
// 右节点:该节点表示物品不放入背包中,上个节点的价值为当前价值
Node right = new Node(node.currWeight, node.currValue,
node.index + 1);
// 不放入当前物品后可以获得的价值上限
right.upboundValue = getUpboundValue(right);
if (right.upboundValue >= bestValue) {
// 将右节点添加到队列中
nodeList.add(right);
}
}
}
}
// 当前操作的节点,放入物品或不放入物品
class Node {
// 当前放入物品的重量
private int currWeight;
// 当前放入物品的价值
private int currValue;
// 不放入当前物品可能得到的价值上限
private int upboundValue;
// 当前操作的索引
private int index;
public Node(int currWeight, int currValue, int index) {
this.currWeight = currWeight;
this.currValue = currValue;
this.index = index;
}
}
// 价值上限=节点现有价值+背包剩余容量*剩余物品的最大单位重量价值
// 当物品由单位重量的价值从大到小排列时,计算出的价值上限大于所有物品的总重量
private int getUpboundValue(Node n) {
// 获取背包剩余容量
int surplusWeight = totalWeight - n.currWeight;
int value1 = n.currValue;
int i = n.index;
while (i < this.n && weight[i] <= surplusWeight) {
surplusWeight -= weight[i];
value1 += value[i];
i++;
}
// 当物品超重无法放入背包中时,可以通过背包剩余容量*下个物品单位重量的价值计算出物品的价值上限
if (i < this.n) {
value1 += ave[i] * surplusWeight;
}
return value1;
}
public int getBestValue() {
return bestValue;
}
}
public static void method3(int[] w,int[] v,int m,int n) {
FZXJProblem pro= new FZXJProblem(w,v,n,m);
pro.solve();
System.out.println("背包最大价值为:"+pro.getBestValue());
}
实验效果: