動態規劃之 0-1 揹包問題詳解

DoubleFJ發表於2018-09-26

個人部落格:DoubleFJ の Blog

前言

揹包問題是比較經典的動態規劃演算法題,之前沒接觸過演算法都沒聽說過這個,也是後來在 leetcode 中刷題時才瞭解到,慚愧慚愧啊。演算法的世界太奇妙,數學一直都是那麼令人著迷。今天來總結一下這個 01 揹包問題。注:這裡的物品不可拆分。

動態規劃

首先了解下什麼是動態規劃。動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。20世紀50年代初美國數學家R.E.Bellman等人在研究多階段決策過程(multistep decision process)的優化問題時,提出了著名的最優化原理(principle of optimality),把多階段過程轉化為一系列單階段問題,利用各階段之間的關係,逐個求解,創立了解決這類過程優化問題的新方法——動態規劃。1957年出版了他的名著《Dynamic Programming》,這是該領域的第一本著作。

動態規劃演算法通常用於求解具有某種最優性質的問題。在這類問題中,可能會有許多可行解。每一個解都對應於一個值,我們希望找到具有最優值的解。動態規劃演算法與分治法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到原問題的解。與分治法不同的是,適合於用動態規劃求解的問題,經分解得到子問題往往不是互相獨立的。若用分治法來解這類問題,則分解得到的子問題數目太多,有些子問題被重複計算了很多次。如果我們能夠儲存已解決的子問題的答案,而在需要時再找出已求得的答案,這樣就可以避免大量的重複計算,節省時間。我們可以用一個表來記錄所有已解的子問題的答案。不管該子問題以後是否被用到,只要它被計算過,就將其結果填入表中。這就是動態規劃法的基本思路。具體的動態規劃演算法多種多樣,但它們具有相同的填表格式。

詳情可見:動態規劃

問題詳解

問題描述

給定 N 種物品和一個揹包。物品i的重量是 Wi,其價值位 Vi,揹包的容量為 C。問應該如何選擇裝入揹包的物品,使得轉入揹包的物品的總價值為最大。

問題分析

在選擇物品的時候,對每種物品只有兩種選擇,要麼裝入,要麼不裝入。因此,此為一個 0-1 揹包問題。
01 揹包的遞迴公式為:

m[i,0] = m[0,j] = 0
m[i,j] = m[i-1,j] ,j < wi
m[i,j] = max(m[i-1,j-wi]+vi, m[i-1,j]) ,j >= wi

其中,m[i,j]為前 i 件物品中選擇若干件,放入承重為 j 的揹包中,得到的最大的價值。

wi 為第 i 件商品的重量。

vi 為第 i 件商品的價值。

例題講解

有編號為 a,b,c,d,e 的五件物品,他們的重量分別為 4,5,6,2,2,價值分別為 6,4,5,3,6,現在給你一個承重為 10 的揹包,怎麼實現價值最大。

根據上述公式可以得到一個資料表,表從上向下生成:

name weight value 1 2 3 4 5 6 7 8 9 10
a 4 6 0 0 0 6 6 6 6 6 6 6
b 5 4 0 0 0 6 6 6 6 6 10 10
c 6 5 0 0 0 6 6 6 6 6 10 11
d 2 3 0 3 3 6 6 9 9 9 10 11
e 2 6 0 6 6 9 9 12 12 15 15 15

故可以根據公式碼出如下實現程式碼:

    public static void main(String[] args) {

        int n = 5;// 5件物品,物品編號為a,b,c,d,e(下面為多加一件物品,第一個物品為虛擬的物品)
        int weight[] = { 0, 4, 5, 6, 2, 2 };// 物品的重量
        int value[] = { 0, 6, 4, 5, 3, 6 }; // 對應物品的價值
        int c = 10; // 揹包容量
        int state[] = { 0, 0, 0, 0, 0, 0 };// 開始狀態
        char name[] = { ' ', 'a', 'b', 'c', 'd', 'e' };
        int maxValue = getMaxValue(n, weight, value, state, c);
        System.out.println("最大價值為 = " + maxValue);
        System.out.print("放入的物品為 :");
        for (int i = 1; i <= 5; i++) {
            if (state[i] == 1) {
                System.out.print(name[i] + "  ");
            }
        }

        // System.out.println();
    }

    /**
     * 
     * @param n
     *            物品數量
     * @param weight
     *            物品對應重量(陣列下標從0開始,故第一個物品為虛擬物品)
     * @param value
     *            物品對應價值(陣列下標從0開始,故第一個物品為虛擬物品)
     * @param state
     *            物品的開始狀態
     * @param c
     *            揹包的容量
     * @return
     */
    public static int getMaxValue(int n, int weight[], int value[], int state[], int c) {
        // n 為物品的數量,陣列時需要加 1,此時可以從 0,1,...n 個物品,共 n+1 個商品,其中第 0 個為虛構物品
        // 對於物品的價值,可以寫成 2 維陣列
        int m[][] = new int[n + 1][c + 1]; // n 為 0,1,2...(n-1),揹包重量為 0,1,2...C
        int i, j;

        for (i = 0; i <= n; i++) {
            m[i][0] = 0;
        }
        for (j = 0; j <= c; j++) {
            m[0][j] = 0;
        }

        for (i = 1; i <= n; i++) {
            // System.out.println();
            for (j = 1; j <= c; j++) {
                if (j < weight[i]) { // 新的物品太重,無法放下
                    m[i][j] = m[i - 1][j];
                } else {// 分為放和不放 取較大值
                    m[i][j] = Math.max(m[i - 1][j - weight[i]] + value[i], m[i - 1][j]);
                }
                // System.out.print("m["+i+"]["+j+"]="+m[i][j]+" ");
                // System.out.print(m[i][j]+" ");
            }
        }

        // 根據其最大價值,反向推斷是否新增了物品 i

        j = c;
        for (i = n; i > 0; i--) {
            if (m[i][j] > m[i - 1][j]) {// 物品 i 新增到了序列列表
                state[i] = 1;
                j = j - weight[i];
            } else { // 沒有新增
                state[i] = 0;
            }
        }

        return m[n][c]; // 最大價值
    }

輸出結果為:

最大價值為 = 15
放入的物品為 :a  d  e  

具體實現看上述程式碼即可,註釋齊全,簡單易懂。

參考資料

相關文章