[背包问题] 现有若干个物品, 每个物品都有价值和重量两种属性. 给你一个载荷有限的包, 要求在重量不超标的情况下, 尽可能地装入物品, 使其价值最大.

例子:
现有三个物品, 重量分别为 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阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容