多背包問題(動態規划)


多重背包

有N種物品和一個容量為T的背包,第i種物品最多有M[i]件可用,價值為P[i],體積為V[i],求解:選哪些物品放入背包,可以使得這些物品的價值最大,並且體積總和不超過背包容量。

對比一下完全背包,其實只是多了一個限制條件,完全背包問題中,物品可以選擇任意多件,只要你裝得下,裝多少件都行。

但多重背包就不一樣了,每種物品都有指定的數量限制,所以不是你想裝,就能一直裝的。

舉個例子:有A、B、C三種物品,相應的數量、價格和占用空間如下圖:

 

 

跟完全背包一樣,貪心算法在這里也不適用,我就不重復說明了,大家可以回到上一篇中看看說明。

遞歸法

還是用之前的套路,我們先來用遞歸把這個問題解決一次。

用ks(i,t)表示前i種物品放入一個容量為t的背包獲得的最大價值,那么對於第i種物品,我們有k種選擇,0 <= k <= M[i] && 0 <= k * V[i] <= t,即可以選擇0、1、2…M[i]個第i種物品,所以遞推表達式為:

ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k <= M[i] && 0 <= k * V[i] <= t)

同時,ks(0,t)=0;ks(i,0)=0;

對比一下完全背包的遞推關系式:

ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k * V[i] <= t)

簡直一毛一樣,只是k多了一個限制條件而已。

使用上面的栗子,我們可以先寫出遞歸解法:

public static class MultiKnapsack {
    private static int[] P={0,2,3,4};
    private static int[] V={0,3,4,5};
    private static int[] M={0,4,3,2};
    private static int T = 15;

    @Test
    public void soleve1() {
        int result = ks(P.length - 1,T);
        System.out.println("最大價值為:" + result);
    }

    private int ks(int i, int t){
        int result = 0;
        if (i == 0 || t == 0){
            // 初始條件
            result = 0;
        } else if(V[i] > t){
            // 裝不下該珠寶
            result = ks(i-1, t);
        } else {
            // 可以裝下
            // 取k個物品i,取其中使得總價值最大的k
            for (int k = 0; k <= M[i] && k * V[i] <= t; k++){
                int tmp2 = ks(i-1, t - V[i] * k) + P[i] * k;
                if (tmp2 > result){
                    result = tmp2;
                }
            }
        }
        return result;
    }
}

同樣,這里的數組P/V/M分別添加了一個元素0,是為了減少越界判斷而做的簡單處理,運行結果如下:

最大價值為:11

對比一下完全背包中的遞歸解法:

private int ks(int i, int t){
    int result = 0;
    if (i == 0 || t == 0){
        // 初始條件
        result = 0;
    } else if(V[i] > t){
        // 裝不下該珠寶
        result = ks(i-1, t);
    } else {
        // 可以裝下
        // 取k個物品i,取其中使得總價值最大的k
        for (int k = 0; k * V[i] <= t; k++){
            int tmp2 = ks(i-1, t - V[i] * k) + P[i] * k;
            if (tmp2 > result){
                result = tmp2;
            }
        }
    }
    return result;
}

僅僅多了一個判斷條件而已,所以只要弄懂了完全背包,多重背包就不值一提了。

最優化原理和無后效性的證明跟多重背包基本一致,所以就不重復證明了。

動態規划

參考完全背包的動態規划解法,就很容易寫出多重背包的動態規划解法。

自上而下記憶法

ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k <= M[i]&& 0 <= k * V[i] <= t)
public static class MultiKnapsack {
    private static int[] P={0,2,3,4};
    private static int[] V={0,3,4,5};
    private static int[] M={0,4,3,2};
    private static int T = 15;

    private Integer[][] results = new Integer[P.length + 1][T + 1];

    @Test
    public void solve2() {
        int result = ks2(P.length - 1,T);
        System.out.println("最大價值為:" + result);
    }

    private int ks2(int i, int t){
        // 如果該結果已經被計算,那么直接返回
        if (results[i][t] != null) return results[i][t];
        int result = 0;
        if (i == 0 || t == 0){
            // 初始條件
            result = 0;
        } else if(V[i] > t){
            // 裝不下該珠寶
            result = ks2(i-1, t);
        } else {
            // 可以裝下
            // 取k個物品,取其中使得價值最大的
            for (int k = 0; k <= M[i] && k * V[i] <= t; k++){
                int tmp2 = ks2(i-1, t - V[i] * k) + P[i] * k;
                if (tmp2 > result){
                    result = tmp2;
                }
            }
        }
        results[i][t] = result;
        return result;
    }
}

這里其實只是照葫蘆畫瓢。

自下而上填表法

同樣也可以使用填表法來解決,此時需要將數組P、V、M額外添加的元素0去掉。

除了k的限制不一樣之外,其他地方跟完全背包的解法完全一致:

public static class MultiKnapsack {
    private static int[] P={2,3,4};
    private static int[] V={3,4,5};
    private static int[] M={4,3,2};
    private static int T = 15;

    private int[][] dp = new int[P.length + 1][T + 1];

    @Test
    public void solve3() {
        for (int i = 0; i < P.length; i++){
            for (int j = 0; j <= T; j++){
                for (int k = 0; k <= M[i] && k * V[i] <= j; k++){
                    dp[i+1][j] = Math.max(dp[i+1][j], dp[i][j-k * V[i]] + k * P[i]);
                }
            }
        }
        System.out.println("最大價值為:" + dp[P.length][T]);
    }
}

跟01背包問題一樣,完全背包的空間復雜度也可以進行優化,具體思路這里就不重復介紹了,可以翻看前面的01背包問題優化篇。

優化后的狀態轉移方程為:

ks(t) = max{ks(t), ks(t - Vi) + Pi}
public static class MultiKnapsack {
    private static int[] P={2,3,4};
    private static int[] V={3,4,5};
    private static int[] M={4,3,2};
    private static int T = 15;

    private int[] newResults = new int[T + 1];

    @Test
    public void resolve4() {
        int result = ksp(P.length,T);
        System.out.println(result);
    }

    private int ksp(int i, int t){
        // 開始填表
        for (int m = 0; m < i; m++){
            // 考慮第m個物品
            // 分兩種情況
            // 1: M[m] * V[m] > T 則可以當做完全背包問題來處理
            if (M[m] * V[m] >= T) {
                for (int n = V[m]; n <= t ; n++) {
                    newResults[n] = Math.max(newResults[n], newResults[n - V[m]] + P[m]);
                }
            } else {
                // 2: M[m] * V[m] < T 則需要在 newResults[n-V[m]*k] + P[m] * k 中找到最大值,0 <= k <= M[m]
                for (int n = V[m]; n <= t ; n++) {
                    int k = 1;
                    while (k < M[m] && n > V[m] * k ){
                        newResults[n] = Math.max(newResults[n], newResults[n - V[m] * k] + P[m] * k);
                        k++;
                    }
                }
            }
            // 可以在這里輸出中間結果
            System.out.println(JSON.toJSONString(newResults));
        }
        return newResults[newResults.length - 1];
    }
}

輸出如下:

[0,0,0,0,2,2,2,4,4,4,6,6,6,8,8,8]
[0,0,0,0,2,3,3,4,5,6,6,7,8,9,9,10]
[0,0,0,0,2,3,4,4,5,6,7,8,8,9,10,11]
11

這里有一個較大的不同點,在第二層循環中,需要分兩種情況考慮,如果 M[m] * V[m] >= T ,那么第m個物品就可以當做完全背包問題來考慮,而如果 M[m] * V[m] < T,則每次選擇時,需要從 newResults[n-V[m]*k] + P[m] * k(0 <= k <= M[m])中找到最大值。

代碼很簡單,但要理解卻並不容易,為了加深理解,再畫一張圖:

多重背包問題同樣也可以轉化成01背包問題來求解,因為第i件物品最多選 M[i] 件,於是可以把第i種物品轉化為M[i]件體積和價值相同的物品,然后再來求解這個01背包問題。

 

轉載說明:在查閱多背包問題時,發現了一篇講解的挺好的文章,就是在百度搜索結果中太靠后了,為了下次方便找到這篇文章,便轉載於此。
作者:MFrank
鏈接:https://www.imooc.com/article/286257


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM