关于贪心算法:背包问题算法全解析动态规划和贪心算法详解

36次阅读

共计 4307 个字符,预计需要花费 11 分钟才能阅读完成。

计算机背包问题是动静布局算法中的经典问题。本文将从实践和实际两个方面深入探讨计算机背包问题,并通过理论案例剖析,帮忙读者更好地了解和利用该问题。

问题背景

背包问题是一种经典的优化问题。有的时候咱们须要将有一堆不同分量或者体积的物品放入背包,然而背包容量无限,这时就要寻找一种最优的物品组合,也就是让背包中的物品 价值最大化或者分量最小化

背包问题分为 0/ 1 背包问题分数背包问题

  • 0/ 1 背包问题是指在背包容量肯定的状况下,每个物品只能抉择放入背包一次或不放入,要求放入背包中的物品的总价值最大化或者总重量最小化。
  • 分数背包问题是指在背包容量肯定的状况下,每个物品能够抉择放入局部或全副,要求放入背包中的物品的总价值最大化或者总重量最小化。

解决办法

动静布局和贪婪算法

  1. 动静布局算法:动静布局算法是解决背包问题的经典办法。它的基本思路是将问题分解成更小的子问题,而后逐渐解决这些子问题,并将后果合并为最终解决方案。动静布局算法能够分为自顶向下和自底向上两种形式。
  2. 贪婪算法:贪婪算法是另一种解决背包问题的办法。它的基本思路是在每一步抉择中,选取以后最优的抉择,而不思考将来的影响。在某些状况下,贪婪算法能够取得更好的性能,但在某些状况下,贪婪算法可能无奈失去最优解。

它们的优缺点?

下面两种算法都是解决 0 / 1 背包问题中罕用的两种算法,它们也各自有着不同的优缺点,留神辨别:

动静布局算法的长处:

  1. 能够解决个别的背包问题,包含 0 / 1 背包问题和齐全背包问题等。
  2. 求解过程中,每个子问题只须要求解一次,因而实用于解决不同的背包问题。
  3. 能够通过记录状态转移方程的形式,不便地找到问题的最优解。

动静布局算法的毛病:

  1. 工夫复杂度较高,在解决较大规模的背包问题时可能会消耗较长时间。
  2. 对于某些问题,可能须要解决的状态数目较多,因而空间复杂度也较高。

贪婪算法的长处:

  1. 工夫复杂度较低,因而实用于解决大规模的背包问题。
  2. 算法的实现较为简单,易于了解和实现。

贪婪算法的毛病:

  1. 只能解决局部背包问题,不能解决个别的背包问题,因而在解决某些问题时可能无奈失去最优解。
  2. 算法的抉择策略可能会导致不同的后果,因而须要对问题特点进行充沛的剖析。

有哪些理论利用?

  1. 商业畛域中的利用 背包问题在商业畛域中失去了广泛应用,如零售商和物流公司须要决定哪些商品应该放入他们的仓库或卡车中,以最大化收益并缩小运输成本。此时,背包问题能够帮忙他们作出最优决策。
  2. 工业畛域中的利用 背包问题也在工业畛域中失去了广泛应用,如计算机芯片的设计和制作须要思考如何最大化应用给定的面积和老本,而背包问题能够帮忙工程师作出最优设计。

在理论问题中,应依据问题的特点抉择适合的算法。如果问题较为简单,能够思考应用贪婪算法;如果问题较为简单,能够思考应用动静布局算法。同时,对于某些非凡的背包问题,也能够应用其余算法来解决,例如分支界线算法和遗传算法等。

案例剖析

背包问题,应用动静布局算法例子如下:

    /**
     * 应用动静布局算法求解 0 / 1 背包问题
     *
     * @param values  物品的价值数组
     * @param weights 物品的分量数组
     * @param W       背包的最大承载重量
     * @return 最大价值
     */
    public static int knapsack(int[] values, int[] weights, int W) {
        int n = values.length;
        int[][] dp = new int[n + 1][W + 1];

        // 初始化第一行和第一列为 0,示意背包容量为 0 和没有物品的时候的最大价值都为 0
        for (int i = 0; i <= n; i++) {dp[i][0] = 0;
        }
        for (int j = 0; j <= W; j++) {dp[0][j] = 0;
        }

        // 填充 dp 数组
        for (int i = 1; i <= n; i++) {for (int j = 1; j <= W; j++) {if (weights[i - 1] > j) {
                    // 物品分量大于背包容量,不能装入背包,最大价值与上一次的最大价值雷同
                    dp[i][j] = dp[i - 1][j];
                } else {
                    // 物品能够装入背包,比拟装入该物品和不装入该物品的最大价值,取较大值
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
                }
            }
        }
        return dp[n][W];
    }

应用贪婪算法,首先计算每个物品的 性价比 (也就是用价值除以分量),而后依照性价比从大到小排序。而后咱们从高到低顺次选取物品,直到无奈再选取为止。当咱们选取一个物品时,如果退出该物品不会导致超出背包容量,则将其退出背包;否则,就将 其局部退出背包(贪婪抉择)。

贪婪算法的工夫复杂度为O(nlogn),其中 n 为物品数量。因为贪婪算法不须要计算子问题的最优解,因而其空间复杂度为 O(1),即常数级别。贪婪算法具备疾速、简略的特点,但不保障失去最优解。

/**
     * 应用贪婪算法求解 0 / 1 背包问题,返回最大价值
     *
     * @param weights 物品分量数组
     * @param values  物品价值数组
     * @param capacity       背包容量
     * @return 能放入背包的最大价值
     */
    public static int knapsackGreedy(int[] values, int[] weights,  int capacity) {
        // 构建物品元组数组
        Tuple[] tuples = new Tuple[weights.length];
        for (int i = 0; i < weights.length; i++) {tuples[i] = new Tuple(weights[i], values[i]);
        }
        // 依照单位分量价值降序排序
        Arrays.sort(tuples, Comparator.comparingDouble(Tuple::getValuePerUnitWeight).reversed());

        int currentWeight = 0; // 以后已装进背包的物品分量
        int currentValue = 0; // 以后已装进背包的物品价值

        // 从价值最高的物品开始,尝试装入背包
        for (Tuple tuple : tuples) {int weight = tuple.getWeight();
            int value = tuple.getValue();
            // 如果装入该物品不会超重,则装入背包
            if (currentWeight + weight <= capacity) {
                currentWeight += weight;
                currentValue += value;
            } else {
                        // 0/1 背包问题不须要退出局部
                                int remain = capacity - currentWeight;
                                currentValue += value * ((double) remain / weight);
                break;
            }
        }

        return currentValue;
    }


    private static class Tuple {
        private int weight;
        private int value;
        private double valuePerUnitWeight;

        public Tuple(int weight, int value) {
            this.weight = weight;
            this.value = value;
            this.valuePerUnitWeight = (double) value / weight;
        }

        public int getWeight() {return weight;}

        public int getValue() {return value;}

        public double getValuePerUnitWeight() {return valuePerUnitWeight;}
    }

为了更好地了解和利用背包问题咱们进行两个案例剖析:假如你要去徒步旅行,你须要带上一些必要的物品,包含帐篷、睡袋、衣服、食品等。你的背包容量无限,不能超过肯定分量。你须要在这些物品中抉择一些,使得它们的总重量不超过背包容量,同时满足你的旅行需要,例如保暖、饱腹等。同时,你也心愿这些物品的总价值尽可能高。

具体来说,你的背包容量为 10 公斤,你须要抉择以下物品:

物品 分量(公斤) 价值(元)
帐篷 3 200
睡袋 2 150
衣服 1 80
食品 5 160

你须要抉择哪些物品能力满足旅行需要,并使得它们的总重量不超过 10 公斤,同时总价值尽可能高?

咱们应用下面的两种算法来求解:

动静布局算法

    public static void main(String[] args) {int[] weights = {3, 2, 1, 5};
        int[] values = {200, 150, 80, 160};
        int capacity = 10;

        int dyMax = knapsack(values, weights, capacity);
        System.out.println("动静布局算法最大价值为:" + dyMax);
    }

结果显示在背包容量为 10 时可能失去的最大价值,即 510 元。对应的物品抉择计划为 帐篷、睡袋、食品

贪婪算法

    public static void main(String[] args) {int[] weights = {3, 2, 1, 5};
        int[] values = {200, 150, 80, 160};
        int capacity = 10;
        int greedyMax = knapsackGreedy(values, weights, capacity);
        System.out.println("贪婪算法最大价值为:" + greedyMax);
    }

贪婪算法失去的后果是 558 元,具体计算过程:

  1. 排列性价比:衣服 > 睡袋 > 帐篷 > 食品;
  2. 而后它顺次抉择了衣服、睡袋、帐篷;
  3. 当抉择食品的时候,如果全副抉择就超过了容量 10,所以它抉择了放入局部食品,也就是 4kg,所以最终 558 元。

值得注意的是:如果这是一个 0 / 1 背包问题(也就是不能放入局部),那么贪婪算法失去的后果就是 430 元,抉择衣服、睡袋、帐篷,所以每种算法不肯定都能失去最优解,须要咱们依据理论状况进行抉择。

小结

贪婪算法与动静布局算法的比拟 从上述案例能够看出,贪婪算法和动静布局算法的解法后果可能不雷同,咱们须要依据问题场景从理论登程进行抉择。

在上述案例中,动静布局算法的工夫复杂度为O(nW),其中 n 是物品数量,W 是背包的最大容量。对于规模较小的背包问题,动静布局算法能够失去较好的解决方案。然而,对于规模较大的背包问题,动静布局算法的工夫复杂度会变得很高,难以承受。

相比之下,贪婪算法的工夫复杂度为O(nlogn),其中 n 是物品数量。因而,贪婪算法在解决规模较大的背包问题时具备较大的劣势。然而,贪婪算法只能失去近似最优解,不能保障肯定失去最优解。因而,在解决须要准确最优解的背包问题时,应该抉择动静布局算法。

正文完
 0