chapter12-2-揹包問題

paopaotangzu發表於2024-03-17

動態規劃最經典並且在機試中重點考查的問題——揹包問題。揹包問題的變體繁多,這裡主要討論3種。

1.0-1揹包

0-1揹包問題描述的是,有\(n\)件物品,每件物品的重量為\(w[i]\),其價值為\(v[i]\),現在有容量為\(m\)的揹包,如何選擇物品使得裝入揹包物品的價值最大。

首先介紹求解這個問題的動態規劃方法,其時間複雜度為\(O(nm)\),空間複雜度也為\(O(nm)\)

設定一個二維陣列\(dp[ ][ ]\),令\(dp[i][j]\)表示前\(i\)個物品裝進容量為\(j\)的揹包能獲得的最大價值。那麼\(dp[n][m]\)的值就是0-1揹包問題的解。

0-1揹包的遞推式如下:
0-1揹包遞推式.jpg
0-1揹包.jpg

點菜問題
//0-1揹包問題 點菜問題 2024-03-17
#include <iostream>
#include <cstdio>

using namespace std;
const int MAXN = 100 + 10;
const int MAXM = 1000 + 10;

int weight[MAXN];
int value[MAXN];

int dp[MAXN][MAXM];


int main() {
    int c, n;//容量為c,物品共n件
    while(scanf("%d%d", &c, &n) != EOF) {
        for(int i = 1; i <= n; ++i) {
            scanf("%d%d", &weight[i], &value[i]);
        }

        for(int i = 0; i <= n ; ++i) {
            dp[i][0] = 0;
        }
        for(int i = 0; i <= c; ++i) {
            dp[0][i] = 0;
        }
        //考慮第i件物品放入容量為j的揹包
        for(int i = 1; i <= n; ++i) {
            for(int j = 1; j <= c; ++j) {
                if(j < weight[i]) {
                    dp[i][j] = dp[i -1][j];
                } else {
                    dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i]);
                }
            }
        }
        //查表
        printf("%d\n",dp[n][c]);
    }
    return 0;
}

最佳化T__.jpg

最佳化空間複雜度-dp設為一維陣列
//0-1揹包問題 點菜問題-最佳化空間複雜度 2024-03-17
#include <iostream>
#include <cstdio>

using namespace std;
const int MAXN = 100 + 10;
const int MAXM = 1000 + 10;

int weight[MAXN];
int value[MAXN];

int dp[MAXM];


int main() {
    int c, n;//容量為c,物品共n件
    while(scanf("%d%d", &c, &n) != EOF) {
        for(int i = 1; i <= n; ++i) {
            scanf("%d%d", &weight[i], &value[i]);
        }

        for(int j = 0; j <= c; ++j) {
            dp[j] = 0;
        }
        //考慮第i件物品放入容量為j的揹包
        for(int i = 1; i <= n; ++i) {
            for(int j = c; j >= weight[i]; --j) {
                dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
        //查表
        printf("%d\n",dp[c]);
    }
    return 0;
}

FYI:一般採用下標為1開始儲存weight和value,因為我們規定的是下標為i的話是前i件物品,那麼預設下標為0的時候是什麼物品都沒有,是一個空集,這樣可以方便我們更加好的進行處理。

2.完全揹包

完全揹包問題和0-1揹包問題很像,二者唯一的區別就在於完全揹包裡面,同一件物品有無窮多件。有\(n\)件物品,每件物品的重量為\(w[i]\),其價值為\(v[i]\),每種物品的數量均為無限個,現在有容量為\(m\)的揹包,如何選擇物品使得裝入揹包物品的價值最大?

同樣設定一個二維陣列\(dp[MAXN][MAXM]\),令\(dp[i][j]\)表示前i個物品裝進容量為j的揹包能夠獲得的最大價值。陣列\(dp[n][m]\)即為完全揹包問題的解。

和0-1揹包問題一樣,只考慮第i件物品時,可將情況分為是否放入第i件物品兩種:

完全揹包.jpg

HDU 4508_減肥記
//完全揹包 2024-03-17
#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int MAXN = 100 + 10;
const int MAXM = 1e5 + 10;

int weight[MAXN];
int value[MAXN];

int dp[MAXN][MAXM];

int main() {
    int n, m;
    while(scanf("%d", &n) != EOF) {
        for(int i = 1; i <= n; ++i) {
            scanf("%d%d", &value[i], &weight[i]);
        }
        scanf("%d", &m);

        for(int i = 0; i <= n; ++i) {
            dp[i][0] = 0;
        }
        for(int j = 0; j <= m; ++j) {
            dp[0][j] = 0;
        }
        for(int i = 1; i <= n; ++i) {
            for(int j = 1; j <= m; ++j) {
                if(j < weight[i]) {
                    dp[i][j] = dp[i - 1][j]; 
                } else {
                    dp[i][j] = max(dp[i-1][j], dp[i][j - weight[i]] + value[i]);
                }
            }
        }
        printf("%d\n",dp[n][m]);
    }
    return 0;
}

最佳化空間複雜度,請看這張圖修改dp陣列和遞推式(也可以說是狀態轉移)。
完全揹包最佳化.jpg

總之,完全揹包問題的特點是每類物品可選的數量為無窮,其解法與0-1揹包問題整體保持一致,與其不同的僅為狀態更新時的遍歷順序。

3.多重揹包

每件物品既不是隻有1件,又不是無限多件,而是有限個。多重揹包問題:有\(n\)種物品,每種物品的重量為\(w[i]\),其價值為\(v[i]\),每種物品的數量為\(k[i]\),現在有個容量為\(m\)的揹包,如何選擇使得裝入揹包物品的價值最大。

求解多重揹包的方法就是直接轉化為0-1揹包問題,不過(1)簡單粗暴,把有多個同一物品的每一個都看成是相同重量、相同價值的不同物品;(2)是對同種物品進行有技巧地拆分和捆綁,得到不同的組合,使得可以任意想獲得幾件此個物品,都能用捆綁的組合加一加得到

多重揹包-1.jpg
多重揹包-2.jpg

注意這個技巧性的拆分:就是類似二進位制法拆,最後不滿足2的冪次倍的單獨捆綁為一個新物品。

HDU 2191
/*
* 題目名稱:珍惜現在,感恩生活
* 題目來源:HDU 2191
* 題目連結:http://acm.hdu.edu.cn/showproblem.php?pid=2191
* 程式碼作者:楊澤邦(爐灰)
*/

#include <iostream>
#include <cstdio>
#include <climits>

using namespace std;

const int MAXN = 100 + 10;
const int MAXM = 1e4 + 10;

int dp[MAXM];
int value[MAXN];                        //物品價值
int weight[MAXN];                       //物品重量
int amount[MAXN];                       //物品數目
int newValue[20 * MAXN];                //拆分後物品價值
int newWeight[20 * MAXN];               //拆分後物品重量

int main() {
    int caseNumber;
    scanf("%d", &caseNumber);
    while (caseNumber--) {
        int n, m;
        scanf("%d%d", &m, &n);          //n件物品,m容量的揹包
        int number = 0;                 //分解後物品數量
        for (int i = 0; i < n; ++i) {
            scanf("%d%d%d", &weight[i], &value[i], &amount[i]);
            for (int j = 1; j <= amount[i]; j <<= 1) {
                newValue[number] = j * value[i];
                newWeight[number] = j * weight[i];
                number++;
                amount[i] -= j;
            }
            if (amount[i] > 0) {
                newValue[number] = amount[i] * value[i];
                newWeight[number] = amount[i] * weight[i];
                number++;
            }
        }
        for (int i = 0; i <= m; ++i) {
            dp[i] = 0;                  //初始化
        }
        for (int i = 0; i < number; ++i) {
            for (int j = m; j >= newWeight[i]; --j) {
                dp[j] = max(dp[j], dp[j - newWeight[i]] + newValue[i]);
            }
        }
        printf("%d\n", dp[m]);
    }
    return 0;
}

不知道咋,我程式碼AC不了,感覺邏輯沒問題,拆分再重新捆綁,記錄新物品的數量,眼睛快瞎了,先這樣吧。

相關文章