(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 背包問題的求解過程。
