[背包問題] 現有若干個物品, 每個物品都有價值和重量兩種屬性. 給你一個載荷有限的包, 要求在重量不超標的情況下, 盡可能地裝入物品, 使其價值最大.

例子:
現有三個物品, 重量分別為 3, 4, 6, 價值分別為 20, 60, 70. 有一個載荷為 8 的包, 它應該裝入哪些物品才能使其價值最大呢?
答案是第 1, 2 個物品, 它們的總重量是 7, 總價值為 80, 此時背包的價值達到最大.

分析:
設 OPT(n, w) 表示: 有 n 個物品, 一個載荷為 w 的背包的場景下的最優解. 這里的最優解指的是這種裝填方案使得背包的價值最大.
假設我們現在有 n - 1 個物品, 它在各種載荷的背包下的最優解我們都已經知曉. 即 OPT(n - 1, 0), OPT(n - 1, 1), OPT(n - 1, 2), ..., OPT(n - 1, w) 都是已知的.
現在我們再加一個物品, 設這個物品的重量為 wn, 價值為 vn. 對于載荷為 w 的背包來說, 它有兩種選擇: 放入這個物品或者不放入這個物品.
如果不放入這個新增的物品, 此時背包的內容物沒有變化, 其價值仍是 OPT(n - 1, w);
如果放入這個物品, 此時背包的內容物發生了變化. 除去這個物品的重量 wn, 背包還剩余 w - wn 的載荷, 這部分剩余載荷的最高價值可以到多少呢? 很顯然, 這是另外 n - 1 個物品在載荷為 w - wn 的背包下的最優解, 即 OPT(n - 1, w - wn). 此時背包的總價值最高可達 OPT(n - 1, w - wn) + vn.
顯然, 這兩種情況下, 誰的價值大, 誰就是最優解. 于是我們得到下面的遞推公式:
OPT(n, w) = max{OPT(n - 1, w), OPT(n - 1, w - wn) + vn}
當然, 0 個物品的情況下, 任意背包的最優解都是 0, 即 OPT(0, w) = 0.
基于以上遞推公式和初始條件, 我們就可以求解任意多個物品在任意載荷的背包下的最優解.

代碼實現:
若有 n 個物品, 背包的載荷為 w, 以下實現的時間復雜度為 O(nw).

public class Test {
    public static void main(String[] args) {
        int capacity = 30;
        int[] weightArr = {4, 2, 8, 3, 1, 9, 11, 7, 8, 13};
        int[] valueArr = {28, 50, 62, 10, 20, 88, 101, 43, 97, 155};

        Knapsack result = optimize(capacity, weightArr, valueArr);
        System.out.println(
                String.format("背包中物品的索引為: %s, 總價值: %s",
                        Arrays.toString(result.getItems()), result.getValue()));
    }

    public static Knapsack optimize(int capacity, int[] weightArr, int[] valueArr) {
        Knapsack[] temp0 = new Knapsack[capacity + 1];
        Arrays.fill(temp0, Knapsack.EMPTY);

        for (int i = 0; i < weightArr.length; i++) {
            int itemWeight = weightArr[i];
            int itemValue = valueArr[i];

            Knapsack[] temp1 = new Knapsack[capacity + 1];
            for (int j = 0; j < temp0.length; j++) {
                int value0 = temp0[j].getValue();
                int value1 = 0;
                if (itemWeight <= j) {
                    value1 = temp0[j - itemWeight].getValue() + itemValue;
                }

                if (value0 < value1) {
                    temp1[j] = temp0[j - itemWeight].addItem(i, itemValue);
                } else {
                    temp1[j] = temp0[j];
                }
            }

            temp0 = temp1;
        }

        return temp0[capacity];
    }

    public static class Knapsack {
        public static final Knapsack EMPTY = new Knapsack(new int[0], 0);

        private final int[] items;
        private final int value;

        private Knapsack(int[] items, int value) {
            this.items = items;
            this.value = value;
        }

        public Knapsack addItem(int itemIdx, int itemValue) {
            int[] newItems = Arrays.copyOf(items, items.length + 1);
            newItems[newItems.length - 1] = itemIdx;
            int newValue = value + itemValue;
            return new Knapsack(newItems, newValue);
        }

        public int[] getItems() {
            return items;
        }

        public int getValue() {
            return value;
        }
    }
}

進階 1:
若背包的載荷和物品的重量都是浮點數呢? 我們第一反應是把浮點數轉換成整數, 再用以上的算法來解決. 這樣是可以的, 但是效率呢? 請考慮如果一個物品的重量為 23.0985, 我們要乘以 10000 才能將其轉換為整數. 根據 O(nw), 相應的時間復雜度也得增加 10000 倍. 當然, 空間復雜度也是如此.
還有另一種情況, 就是背包載荷和物品的重量很大, 比如重量為 230985, 嗯, 其實就回到了上面那種情況. 此時如果用第一種算法, 時間復雜度和空間復雜度會不受控的.
這里有一個思路: 對于 n 個物品, 我們是否有必要把它在所有載荷下的最優解都計算一遍? 即我們是否有必要計算全部的 OPT(n, 0), OPT(n, 1), OPT(n, 2), ..., OPT(n, 230985), ..., OPT(n, w)?
當然是沒必要的啦, 實際上我們只需要計算那些有可能產生變化的點, 即關鍵點的 OPT 即可. 那么哪些點是關鍵點呢? 這個留給讀者自己思考吧, 下面附上這種思路的實現.
實測這種算法在物品重量緊湊且規模不大時, 效率不如第一種算法. 但是一旦上面提到的情況出現時, 其效率可大幅領先第一種算法.

public class Test {

    public static void main(String[] args) {
        double capacity = 36.66;
        double[] weightArr = {1.22, 6.3, 3.33, 5.25, 7.1, 2.12, 8.06, 7.32, 6.66, 5.42};
        double[] valueArr = {4.9, 5.5, 8.93, 8.14, 5.37, 4.22, 8.8, 10.31, 7.36, 6.21};

        Knapsack result = optimize(capacity, weightArr, valueArr);
        System.out.println(
                String.format("背包中物品的索引為: %s, 總價值: %s",
                        Arrays.toString(result.getItems()), result.getValue()));
    }

    public static Knapsack optimize(double capacity, double[] weightArr, double[] valueArr) {
        TreeMap<Double, Knapsack> temp0 = new TreeMap<>();
        temp0.put(0D, Knapsack.EMPTY);

        for (int i = 0; i < weightArr.length; i++) {
            double itemWeight = weightArr[i];
            double itemValue = valueArr[i];

            TreeMap<Double, Knapsack> temp1 = new TreeMap<>();
            temp1.put(0D, Knapsack.EMPTY);
            for (Map.Entry<Double, Knapsack> entry0 : temp0.entrySet()) {
                Double weight0 = entry0.getKey();
                Knapsack knapsack0 = entry0.getValue();
                double value0 = knapsack0.getValue();

                Knapsack knapsack1 = temp1.floorEntry(weight0).getValue();
                double value1 = knapsack1.getValue();
                if (value1 < value0) {
                    temp1.put(weight0, knapsack0);

                    temp1.tailMap(weight0, false).entrySet()
                            .removeIf(_entry -> _entry.getValue().getValue() <= value0);
                }

                if (weight0 + itemWeight <= capacity) {
                    temp1.put(weight0 + itemWeight, knapsack0.addItem(i, itemValue));
                }
            }

            temp0 = temp1;
        }

        return temp0.floorEntry(capacity).getValue();
    }

    public static class Knapsack {
        public static final Knapsack EMPTY = new Knapsack(new int[0], 0D);

        private final int[] items;
        private final double value;

        private Knapsack(int[] items, double value) {
            this.items = items;
            this.value = value;
        }

        public Knapsack addItem(int itemIdx, double itemValue) {
            int[] newItems = Arrays.copyOf(items, items.length + 1);
            newItems[newItems.length - 1] = itemIdx;
            double newValue = value + itemValue;
            return new Knapsack(newItems, newValue);
        }

        public int[] getItems() {
            return items;
        }

        public double getValue() {
            return value;
        }
    }
}

進階 2:
若物品之間存在關聯, 例如如果取了物品 A, 就必須同時取物品 B. 或者取了物品 C, 就不能再取物品 D 等等. 這種情況下, 背包算法怎么實現? 其實這里的基本思路和基礎版的背包問題是一致的, 只是在決定是否放入第 n 個物品時, 要把與之關聯的物品考慮進來, 其遞推公式的基本形式是不變的. 具體算法實現請有興趣和耐心的讀者自己嘗試吧.

進階 3:
若物品可以重復獲取, 背包算法怎么實現? 將基礎背包算法稍微改動一下就可以得到這種情況的解. 有興趣的讀者自己思考一下吧.

進階 4:
若某些物品可以重復獲取, 另一些物品不可以呢? 若已經解決了進階 3 的算法, 聯立基礎版背包算法即可.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容