一、问题描述

有n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中第i个集装箱i的重量为w[i],要求确定是否有一个合理的装载方案可将这些集装箱装上这2艘轮船。如果有,找出一种装载方案。

二、问题分析

(1) n个集装箱的重量之和肯定小于等于2艘轮船的载重量c1+c2,不然不可能全部装入,即∑w[i] <= c1 + c2

(2) 为将所有集装箱装入2艘轮船,我们无需考虑如何将集装箱分配到2艘轮船,只需考虑如何将1艘轮船尽可能装满。第1艘轮船能最大程度装满,第2艘轮船能装下就是能装下,装不下就是装不下。

三、算法思路

(1) 何为解空间树?以该问题为例,每个集装箱可以看成一个结点,而每一个集装箱有装或不装两种选择。因此,每个结点会出现两条分支,利用深度优先搜索,最后会形成一棵完全二叉树,这就是解空间树。

(2) 何为回溯法?回溯法,利用深度优先搜索策略,从根节点出发搜索解空间树。搜索至任一结点时,判断该节点是否包含问题的解。如果不包含,跳过即可;如果包含,进入该子树,继续按照深度优先搜索策略搜索。

(3) 目标函数:在解空间树中找到一条路径,使所有集装箱可以装入2艘轮船。

(4) 约束条件:装入第1艘轮船的重量之和需要小于等于第1艘轮船的载重量,即∑x[i]w[i] <= c1。

(5) 限界条件:如果要搜索整棵二叉树,会消耗许多时间。因此,利用限界函数剪去不包含最优解的子树,减少不必要的搜索,加快搜索进度。在该问题中,假设cw是已装入的重量,rw是未装入的重量,bestW是当前的最优值(装入的重量和最大)。当搜索至任一结点时,如果不选择当前结点,形成一条分支,则该分支必然可行。但是,该分支不一定包含最优解。此时,若cw+rw <bestW,即小于最优值,明显不需要搜索该路径。因此,剪去该路径,减少搜索时间。

四、源代码

import java.util.Scanner;
public class MaxLoading {
    static final int NUM = 100;
    // 集装箱重量
    static int[] w=new int[NUM];
    // 最优解
    static int[] bestX = new int[NUM];
    // 当前解
    static int[] x= new int[NUM];
    // 最优装船重量
    static int bestW = 0;
    // 物品个数
    static int n;
    // 第一个船的载重量
    static int c1;
    // 第二个船的载重量
    static int c2;
    // 当前已装船重量
    static int cw = 0;

    private static int bound(int t) {
        // 初始化剩余重量
        int rw = 0;
        for (int i = t+1;i <= n;++i) {
            // 计算剩余集装箱重量
            rw += w[i];
        }
        // 返回可装的总重量
        return rw+cw;
    }

    /**
     * 回溯法
     * @param t :第几个集装箱
     */
    public static void loadingRec(int t) {
        if (t > n) { // 集装箱装箱完毕
            if (cw > bestW) {  // 如果当前重量大于最优重量
                // 更新最优重量
                bestW = cw;
                for (int i = 1; i <= n; i++) {
                    // 最优解更新为当前解
                    bestX[i] = x[i];
                }
            }
            return;
        }else { // 尚未结束装箱
            if (cw + w[t] <= c1) { // 若装入当前集装箱,且重量小于船载重量
                // 加上此集装箱重量
                cw +=w[t];
                // 选择该集装箱
                x[t] = 1;
                // 下一个
                loadingRec(t+1);
                cw -= w[t];
                x[t] = 0;
            }
            if (bound(t) > bestW) { // 不装第t个集装箱,若总重量大于最优重量
                loadingRec(t+1);
            }
        }
    }

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.print("请输入集装箱的个数:");
        n = in.nextInt();
        System.out.println("请输入集装箱的重量(整数):");
        for (int i = 1; i <= n;++i) {
            w[i] = in.nextInt();
        }
        in.nextLine();
        System.out.print("请输入第一船的载重量:");
        c1 = in.nextInt();
        in.nextLine();
        System.out.print("请输入第二船的载重量:");
        c2 = in.nextInt();
        loadingRec(1);
        System.out.println("第一船的最优重量为:" + bestW);
        System.out.println("第一船的最优解为:");
        for (int i = 1; i <= n;++i) {
            if (bestX[i] == 1) { // bestX[i] == 1,表示选中
                System.out.println("物品" + i + "装入第1个集装箱");
            }
        }
        int cw2 = 0;
        for (int i = 1;i <= n;++i) {
            // 计算剩余重量
            if (bestX[i] == 0) {
                cw2 += w[i];
            }
        }
        // 剩余重量小于第二个船的载重量,可以装入
        // 下行为小于第二艘船的载重量
        if (cw2 <= c2) {
            System.out.println("可以将剩余集装箱装入第二船,问题有解");
        }else { // 剩余重量大于第二个船的重量,不能装入
            System.out.println("不能将剩余集装箱装入第二船,问题无解");
        }
        in.close();
    }
}