揹包問題演算法全解析:動態規劃和貪心演算法詳解

程式設計碼農發表於2023-05-02

計算機揹包問題是動態規劃演算法中的經典問題。本文將從理論和實踐兩個方面深入探討計算機揹包問題,並透過實際案例分析,幫助讀者更好地理解和應用該問題。

問題背景

揹包問題是一種經典的最佳化問題。有的時候我們需要將有一堆不同重量或者體積的物品放入揹包,但是揹包容量有限,這時就要尋找一種最優的物品組合,也就是讓揹包中的物品價值最大化或者重量最小化

揹包問題分為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公斤,你需要選擇以下物品:

物品重量(公斤)價值(元)
帳篷3200
睡袋2150
衣服180
食品5160

你需要選擇哪些物品才能滿足旅行需求,並使得它們的總重量不超過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是物品數量。因此,貪心演算法在處理規模較大的揹包問題時具有較大的優勢。但是,貪心演算法只能得到近似最優解,不能保證一定得到最優解。因此,在處理需要精確最優解的揹包問題時,應該選擇動態規劃演算法。

相關文章