美文网首页
[背包问题] 现有若干个物品, 每个物品都有价值和重量两种属性.

[背包问题] 现有若干个物品, 每个物品都有价值和重量两种属性.

作者: 风骚的风 | 来源:发表于2018-07-15 16:42 被阅读0次

例子:
现有三个物品, 重量分别为 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 的算法, 联立基础版背包算法即可.

相关文章

网友评论

      本文标题:[背包问题] 现有若干个物品, 每个物品都有价值和重量两种属性.

      本文链接:https://www.haomeiwen.com/subject/wnpppftx.html