回溯算法 - 0-1背包问题
(1)算法描述
给定 num 种物品和一背包。物品 i 的重量是 weighti > 0,其价值为 pricei > 0,背包的容量为 capacity。问应如何选择装入背包中的物品,使得装入背包中物品的总价值最大?
(2)举例
对于 0-1 背包问题的一个实例,num = 4,capacity = 7,price = [9, 10, 7, 4],weight = [3, 5, 2, 1]。这 4 个物品的单位重量价值分别为 [3, 2, 3.5, 4],以物品单位重量价值的递减顺序装入物品。先装物品 4,然后装入物品 3 和 1。装入这 3 个物品后,剩余的背包容量为 1,只能装入 0.2 的物品 2。由此可以得到一个解为 x = [1, 0.2, 1, 1],其相应的价值为 22。尽管这不是一个可行解,但可以证明其价值是最优解的上届。因此,对于这个实例,最优解不超过 22。
(3)算法描述
0-1 背包问题是子集选取问题。0-1 背包问题的解空间可用子集树表示。解 0-1 背包问题的回溯法与解最优装载问题十分相似,在搜索解空间树时,只要其左子树结点是一个可行结点,搜索就进入其左子树。当右子树有可能包含最优解时才进入右子树搜索。否则将右子树剪枝。设 indeterminacyPrice 是当前剩余物品价值总和;currentPrice 是当前价值;bestPrice 是当前最优价值。当 currentPrice + indeterminacyPrice <= bestPrice 时,可剪去右子树。计算右子树中解的上界更好的方法是将剩余物品依其单位重量价值排序,然后依次装入物品,直至装不下时,再装入该物品一部分而装满背包。由此得到的价值是右子树中的一个解。
(4)代码编写
public class Knapsack01 { // 背包容量 private static Integer capacity; // 物品个数 private static Integer num; // 物品重量数组 private static Integer[] weight; // 物品存放数组【0:不存放 1:存放】 private static Integer[] store; // 物品最优装载数组序号【0:不存放 1:存放】 private static Integer[] bestIndex; // 物品价值数组 private static Integer[] price; // 背包当前重量 private static Integer currentWeight = 0; // 背包当前价值 private static Integer currentPrice = 0; // 背包最优价值 private static Integer bestPrice = 0; /** * 初始化数据 */ private static void initData() { Scanner input = new Scanner(System.in); System.out.println("请输入背包容量:"); capacity = input.nextInt(); System.out.println("请输入物品数量:"); num = input.nextInt(); weight = new Integer[num]; store = new Integer[num]; bestIndex = new Integer[num]; System.out.println("请输入各个物品的重量:"); for (int i = 0; i < weight.length; i++) { weight[i] = input.nextInt(); store[i] = 0; bestIndex[i] = 0; } price = new Integer[num]; System.out.println("请输入各个物品的价值:"); for (int i = 0; i < price.length; i++) { price[i] = input.nextInt(); } } /** * 根据物品价值降序排列,同时调整物品重量数组 */ private static void sortDesc() { Integer temp; int change = 1; for (int i = 0; i < price.length && change == 1; i++) { change = 0; for (int j = 0; j < price.length - 1 - i; j++) { if (price[j] < price[j + 1]) { temp = price[j]; price[j] = price[j + 1]; price[j + 1] = temp; temp = weight[j]; weight[j] = weight[j + 1]; weight[j + 1] = temp; change = 1; } } } } /** * 计算上届【判断】 */ private static Integer bound(int i) { Integer cleft = capacity - currentWeight; // 记录剩余背包的容量 Integer p = currentPrice; // 记录当前背包的价值 //【已经按照物品价值降序排列,只要物品能装下,价值一定是最大】物品装入背包时,一定要确保背包能装下该物品 while(i < weight.length && weight[i] <= cleft) { cleft -= weight[i]; p += price[i]; i++; } // 将第 i + 1 个物品切开,装满背包,计算最大价值 if (i < weight.length) { p = p + cleft * (price[i] / weight[i]); } return p; } /** * 回溯寻找最优价值 */ private static void backtrack(int i) { // 递归结束条件 if (i == price.length) { if (currentPrice > bestPrice) { for (int j = 0; j < store.length; j++) { bestIndex[j] = store[j]; } bestPrice = currentPrice; } return; } if (currentWeight + weight[i] <= capacity) { // 确保背包当前重量 + 物品 i 的重量 <= 当前背包容量,才有意义继续进行 store[i] = 1; currentWeight += weight[i]; currentPrice += price[i]; backtrack(i + 1); currentWeight -= weight[i]; currentPrice -= price[i]; } // 剪枝函数【判断 (背包当前价值 + 未确定物品的价值) 大于 背包最优价值,搜索右子树;否则剪枝】 if (bound(i + 1) > bestPrice) { store[i] = 0; backtrack(i + 1); } } /** * 输出 */ private static void print() { System.out.println("\n降序后各个物品重量如下:"); Stream.of(weight).forEach(element -> System.out.print(element + " ")); System.out.println(); System.out.println("降序后各个物品价值如下:"); Stream.of(price).forEach(element -> System.out.print(element + " ")); System.out.println(); System.out.println("物品最优装载数组序号【0:不装载 1:装载】:"); Stream.of(bestIndex).forEach(element -> System.out.print(element + " ")); System.out.println(); System.out.println("背包最大价值:bestPrice = " + bestPrice); } public static void main(String[] args) { // 初始化数据 initData(); // 根据物品价值降序排列,同时调整物品重量数组 sortDesc(); // 回溯寻找最优价值 backtrack(0); // 输出 print(); } }
(5)输入输出
请输入背包容量: 7 请输入物品数量: 4 请输入各个物品的重量: 3 5 2 1 请输入各个物品的价值: 9 10 7 4 各个物品重量如下: 5 3 2 1 各个物品价值如下: 10 9 7 4 物品最优装载数组序号【0:不装载 1:装载】: 0 1 1 1 背包最大价值:bestPrice = 20
(6)总结
0-1 背包使用【回溯法-子集树】来求解,时间复杂度为 O(2n),使用深度优先遍历,递归方式求出最优解;
建议:可以依照我的代码,自行在纸上画一画,走一遍算法代码的详细流程,进而熟悉回溯法的核心思想,理解 0-1 背包问题的求解过程。