背包问题,相信各位看官肯定都有所耳闻!笔者就在此简单的描述一下背包问题:
给定一背包和n件物品,背包的容量为c,第i件物品的重量为w[i],价值为v[i](1<=i<=n);问装那些物品,可使得价值最大?
思路分析:显然,每种物品不外乎两种选择:装入和不装入背包!若将装入用状态1表示,不装入用状态0表示;那么就可构成一个二叉树!一共有n层,所以就可通过从第一层开始遍历搜索,在装入的物品总重量不大于c的情况下,找到最优解了!
思路还是蛮简单的,别的不多说,直接上代码:
public class Demo1 { private static int c = 10; //背包容量 private static int[] w= {0,2,2,6,5,5}; //每个物体的重量:5个物体 private static int[] v = {0,6,3,5,4,6}; //每个物体的价值 private static int[] x= new int[6]; //存放每个物体的选中情况 private static int bestcp = 0; //当前的最优解 private static int[] bestcpArr = new int[6]; private static int count = 0; private static int n = 5; //物体的数目 public static void main(String[] args) { backtrack(1, 0 ,0); for(int i =0 ; i < bestcpArr.length ;i++){ System.out.print(bestcpArr[i]); } System.out.println(“”); System.out.println(bestcp); System.out.println(“该树有” + count +“种遍历方式”); } /** * * @param i:查找到了第i个物品(i从1开始索引) * @param cv:当前包中价值 * @param cw:当前包中重量 */ private static void backtrack(int i , int cw , int cv) { count++; if (i > n) { if (cv > bestcp) { bestcp = cv; for(int j = 0 ; j < 6 ;j++){ bestcpArr[j]= x[j]; } } }else{ // 开始遍历,如果k=0,则表示不装下此物品,k=1,则表示装下此物品 for(int k = 0 ; k <= 1; k++){ x[i] = k; if (cw + w[i] <= c) { cv += v[i] * k; cw +=w[i] * k; backtrack(i + 1, cw, cv); cv -= v[i] * k; cw -=w[i] * k; } } } } }
我们知道,回溯法的效率并不是很高的!因为回溯法,就其本质而言,还是属于穷举!只不过它就提供了一个穷举的思路:回溯!的确也是这样,上面代码中的例子中的物品只由5个,换句话说,所构成的二叉树也只有5层!但当物品有10个,20个,甚至100个话,构成的二叉树是多么的庞大!(笔者觉得不可想象深度为100的二叉树)!故在上述代码的基础上,很有必要去做一个算法的优化!在网上看了别人的博客之后,大致知道了一个思路:即在不断的遍历左子树(即不断的将物品装入)的过程中,如果出现了不能再装入下一物品的情况时,这时就需要去遍历右子树!但是如果,如果在剩余容量情况下,将剩余容量的背包装满(如果大于0且小于物品的重量的话,按照单位重量的价值乘以剩余容量来算)的情况下 得到的总价值比当前最优解还要的小的话,那么该右子树是完全没有必要去遍历,而需要直接剪除的!这样就达到了优化的目的!废话不多说,直接上代码:
/**
* 本算法的上界函数是这样定义的!首先利用冒泡法排序,按照单位价值右高到低开始顺序排列!
* 这样的话,就能保证,先装进去的是性价比(自己发明的,勿喷)最优的!而在上界函数中,也是如此,求的是剩余
* 容量在右子树所能容纳的最高价值(背包容量允许情况下),一旦小于当前最优解,那么就
* 没有继续遍历该右子树的必要
*
*/
public class Demo2 { private static int n;// 物品数量 private static double c;// 背包容量 private static double[] v = new double[100];// 各个物品的价值 private static double[] w = new double[100];// 各个物品的重量 private static double cw = 0.0;// 当前背包重量 private static double cp = 0.0;// 当前背包中物品价值 private static double bestp = 0.0;// 当前最优价值 private static double[] perp = new double[100];// 单位物品价值排序后 private static int order[] = new int[100];// 物品编号 private static int[] put = new int[100];// 设置是否装入 // 按单位价值排序 private static void knapsack() { int i, j; int temporder = 0; double temp = 0.0; for (i = 1; i <= n; i++) perp[i] = v[i] / w[i]; for (i = 1; i <= n – 1; i++) { for (j = i + 1; j <= n; j++) if (perp[i] < perp[j])// 冒泡排序perp[],order[],sortv[],sortw[] { temp = perp[i]; perp[i] = perp[i]; perp[j] = temp; temporder = order[i]; order[i] = order[j]; order[j] = temporder; temp = v[i]; v[i] = v[j]; v[j] = temp; temp = w[i]; w[i] = w[j]; w[j] = temp; } } } // 回溯函数 private static void backtrack(int i) { if (i > n) { bestp = cp; return; } // 将物品装进背包:此种情况的话 if (cw + w[i] <= c) { cw += w[i]; cp += v[i]; put[i] = 1; backtrack(i + 1); cw -= w[i]; cp -= v[i]; } if (bound(i + 1) > bestp)// 符合条件搜索右子数 backtrack(i + 1); } // 计算上界函数
//算法剩余容量的情况下最多能装的价值 private static double bound(int i) { double leftw = c – cw; double b = cp; while (i <= n && w[i] <= leftw) { leftw -= w[i]; b += v[i]; i++; } if (i <= n) b += v[i] / w[i] * leftw; return b; } public static void main(String[] args) { // private static int[] w= {0,2,2,6,5,5}; //每个物体的重量:5个物体 // private static int[] v = {0,6,3,5,4,6}; //每个物体的价值 v[0] = 0; v[1] = 6; v[2] = 3; v[3] = 5; v[4] = 4; v[5] = 6; w[0] = 0; w[1] = 2; w[2] = 2; w[3] = 6; w[4] = 5; w[5] = 5; n = 5; c = 10; knapsack(); backtrack(1); System.out.println(bestp); for (int i = 1; i <= n; i++) { System.out.print(put[i] + ” “); } System.out.println(“”); } }
现在,我们来分析下时间复杂度,最好的情况当然是能够剪除所有的右子树,而最优的物品选择刚好全在左子树上了!而最坏的情况自然是搜索右子树的次数最多了呀!
到此,直接遍历穷举和优化之后的回溯法来解决10背包问题的分析解答终于写完了!写了一上午啊!
笔者水平有限,望各位看官勿怪!
参考博客:
优化的回溯法解决10背包问题
遍历的回溯法解决10背包问题