(1)問題描述:有一批共 n 個集裝箱要裝上 2 艘載重量分別為 capacity1 和 capacity2 的輪船,其中集裝箱 i 的重量為 wi,且裝載問題要求確定是否有一個合理的裝載方案可將這些集裝箱裝上這 2 艘輪船。如果有,找出一種裝載方案。
例如:當 n = 3, capacity1 = capacity2= 50, 且 w = [10, 40, 40] 時,則可以將集裝箱 1 和 2 裝到第一艘輪船上,而將集裝箱 3 裝到第二艘輪船上;如果 w = [20, 40, 40],則無法將這 3 個集裝箱都裝上輪船。
(2)基本思路: 容易證明,如果一個給定裝載問題有解,則采用下面的策略可得到最優裝載方案。
a. 首先將第一艘輪船盡可能裝滿;
b. 將剩余的集裝箱裝上第二艘輪船。
將第一艘輪船盡可能裝滿等價於選取全體集裝箱的一個子集,使該子集中集裝箱重量之和最接近 capacity1 。由此可知,裝載問題等價於以下特殊的 0-1 背包問題。
變量 Xi = 0 表示不裝入集裝箱 i,Xi = 1 表示裝入集裝箱 i;
用回溯法設計解裝載問題的O(2n)計算時間算法。在某些情況下該算法優於動態規划算法。
(3)算法設計:
子集樹模板算法,時間復雜度為:O(2n)
用回溯法解裝載問題時,用子集樹表示其解空間顯然是最合適的,用可行性約束函數可剪去不滿足約束條件的子樹。
可以引入一個上界函數,用於剪去不含最優解的子樹,從而改進算法在平均情況下的運行效率。設z是解空間樹第 i 層上的當前擴展結點。currentWeight 是當前載重量;bestWeight 是當前最優載重量;indeterminacyWeight 是剩余集裝箱的重。定義上界函數為 currentWeight + indeterminacyWeight。在以 z 為根的子樹中任一葉結點所相應的載重量,當currentWeight + indeterminacyWeight <= bestWeight 時,可將z的右子樹剪去。
(4)算法代碼:

public class ExcellentLoading { /** * 物品數量 */ private static Integer num; /** * 物品重量數組 */ private static Integer[] weight; /** * 物品存儲數組【0:不存放 1:存放】 */ private static Integer[] store; /** * 船的容量 */ private static Integer capacity; /** * 船的最優載重量【最優解】 */ private static Integer bestWeight = 0; /** * 未確定物品的載重量 */ private static Integer indeterminacyWeight = 0; /** * 船的當前載重量 */ private static Integer currentWeight = 0; /** * 物品最優解的下標數組 */ private static Integer[] bestIndex; /** * 初始化數據 */ private static void initData() { Scanner input = new Scanner(System.in); System.out.println("請輸入船的容量:"); capacity = input.nextInt(); System.out.println("請輸入物品的數量:"); num = input.nextInt(); System.out.println("請輸入各個物品的重量"); weight = new Integer[num]; store = new Integer[num]; bestIndex = new Integer[num]; for (int i = 0; i < num; i++) { weight[i] = input.nextInt(); indeterminacyWeight += weight[i]; store[i] = 0; bestIndex[i] = i; } } /** * 裝載 */ private static void loadingBacktrack(Integer i) { if (i == weight.length) { // 到達葉子結點 if (currentWeight > bestWeight) { // 當前船的裝載量 > 最優解,賦值操作 for (int j = 0; j < weight.length; j++) { bestIndex[j] = store[j]; } bestWeight = currentWeight; } return; } indeterminacyWeight -= weight[i]; // 減去已被確定該討論的物品重量 if (currentWeight + weight[i] <= capacity) { // 搜索左子樹 store[i] = 1; // 物品裝載 currentWeight += weight[i]; // 當前船的載重量 + 該物品重量 loadingBacktrack(i + 1); currentWeight -= weight[i]; // 當前船的載重量 - 該物品重量【回溯到上一層,討論該物品不裝】 } if (currentWeight + indeterminacyWeight > bestWeight) { // 搜索右子樹 || 剪枝函數【如果船的當前載重量 + 未確定物品的重量 <= 當前船的最優值,直接剪掉】 store[i] = 0; // 該物品不裝 loadingBacktrack(i + 1); } indeterminacyWeight += weight[i]; } /** * 輸出 */ private static void print() { System.out.println("船裝載物品最優解:"); Stream.of(bestIndex).forEach(element -> System.out.print(element + " ")); System.out.println(); } public static void main(String[] args) { // 初始化數據 initData(); // 裝載 loadingBacktrack(0); // 輸出 print(); } }
(5)輸入輸出

請輸入船的容量: 60 請輸入物品的數量: 5 請輸入各個物品的重量 10 25 30 10 5 船裝載物品最優解: 0 1 1 0 1
(6)總結:最優裝載詮釋了回溯算法中子集樹的核心思想,類似於 0 - 1 背包問題,集裝 i 箱裝與不裝,回溯判斷最優解,使用剪枝函數去除不必要的無效搜索,否則進入右子樹再次進入深度搜索,直至深度搜索完整棵二叉樹,搜索出所有的解,找出最優解即可;
回溯算法子集樹的時間復雜度 O(2n),遞歸求解,不太好想,希望各位在紙上畫一畫,模擬我的代碼走一遍流程,便於大家理解回溯算法,遞歸算法。