1. 程式人生 > 其它 >2. 01揹包問題

2. 01揹包問題

題目傳送門

一、知識架構

(1) \(01\)揹包

\(N\)個物品(\(0\)不選擇,\(1\)選擇\(1\)次),容量\(V\)的揹包(上限),\(v_i\)表示物品的體積,\(w_i\)表示價值
如何組裝揹包,在\(V\)的上限限制情況下,使得價值最大,求價值最大值。
總結:每個物品只有1個,可以選或不選,求在容量限制下的重量(價值)最大值。

(2) 完全揹包

每個物品有無限個,求價值最大是多少。

(3) 多重揹包

每個物品個數不一樣,不是無限,也不是隻有一個,有\(k_i\)的個數限制。

(4) 分組揹包問題

物品有\(n\)組,各一組有若干個。每一組最多選擇一個物品,比如水果組選擇了蘋果,就不能選香蕉。

注意:不一定要裝滿揹包

二、01揹包的遞迴表示法

#include <bits/stdc++.h>
using namespace std;
const int N=110;
int w[N],v[N],n,m;

//dfs函式定義的意義: 在已經放入揹包i個物品,剩餘的揹包體積為j的情況下,計算可以獲取到的最大價值
int dfs(int i,int j){
    int res=0;   //儲存每一步求得的當前最大價值
    if(i==n+1)  return res;   // 當到n+1個物品時就開始回溯
    if(w[i]>j)
        res=dfs(i+1,j);  //若當前物品的重量大於揹包容量時,就無法放入揹包。此時,放棄第i個物品,走到i+1個物品前進行下一步嘗試
    else
       res=max(dfs(i+1,j),dfs(i+1,j-w[i])+v[i]);//若能放下,則取放入或不放入兩種情況中較大值
    return res;
}
int main(){
    scanf("%d %d",&n,&m); //物品個數與揹包容量
    //讀入每個物品的體積和價值
    for(int i=1;i<=n;i++)   scanf("%d %d",&w[i],&v[i]);
    
    cout<<dfs(0,m)<<endl;
    return 0;
}

遞迴表示法的缺點:
因為有大量的重複計算,所以,不出意外的在大資料量的情況下,\(TLE\)了!我們需要找一個辦法進行優化。

三、記憶化搜尋

https://blog.csdn.net/Krismile_/article/details/83212954?share_token=91367e0d-bbc5-482e-afcd-6836a54d96e5

#include <bits/stdc++.h>

using namespace std;
const int N = 1010;

int w[N], v[N], n, m, dp[N][N]; //引入dp陣列儲存已經計算過的結果

int dfs(int i, int j) {
    //判斷這種情況是否被計算過,若被計算過就可以直接返回結果不用進行下面的一系列判斷計算
    if (dp[i][j] >= 0) return dp[i][j];

    int res = 0;
    if (i == n + 1) return res;

    if (w[i] > j) res = dfs(i + 1, j);
    else res = max(dfs(i + 1, j), dfs(i + 1, j - w[i]) + v[i]);

    dp[i][j] = res; //儲存求出結果
    return res;
}

int main() {
    //初始化陣列元素為-1
    memset(dp, -1, sizeof(dp));
    cin >> n >> m;

    for (int i = 1; i <= n; i++)
        cin >> w[i] >> v[i];

    cout << dfs(0, m) << endl;
    return 0;
}

我們由此又可以得出打表的方法即:

for(int i = 1;i<= n;i++)
    for(int j = 1;j <= v;j++)
        dp[i][j]=max(dp[i-1][j] ,dp[i-1][j-w[i]] + v[i]);

四、二維陣列法

#include <iostream>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][N];

int main() {
    //物品個數n
    cin >> n >> m;      //m:揹包容積,就是大V
    //讀入體積和重量
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            if (j >= v[i]) f[i][j] = max(f[i-1][j], f[i - 1][j - v[i]] + w[i]); //兩種情況取最大值
            else f[i][j] = f[i - 1][j]; 
        }
    cout << f[n][m] << endl;

    return 0;
}

2、為什麼要用一維陣列優化01揹包問題

因為我們看到上面的儲存方式,其實第\(i\)件物品選擇或不選擇兩種方式,都只和\(i-1\)時的情況相關聯,也就是說在\(i-1\)再以前的狀態和資料與\(i\)無關,換句話說,就是沒啥用了,可以清除掉了。這樣一路走一路依賴它前面一行就OK,可以把二維陣列優化為一維。

那麼,怎麼個優化法呢?答案是用一個一維陣列。

這裡最核心的問題是為什麼容量的遍歷要從大到小,和上面二維的不一樣了呢??
這是因為第\(i\)個要依賴於第\(i-1\)個,如果從小到大,那麼\(i-1\)先變化了,而\(i\)說的依賴於\(i-1\),其實是在沒變化前的\(i-1\) 資訊,這麼直接來就錯了。那怎麼才能對呢?就是從大到小反向遍歷,這樣\(i-1\)都是以前的資訊,還沒變,就OK了!有點繞啊,但還算好理解!

五、一維陣列法

#include <iostream>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main() {
    //物品個數n
    cin >> n >> m;      //m:揹包容積,就是大V
    //讀入體積和重量
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];

    for (int i = 1; i <= n; i++)
        for (int j = m; j >=0; j--) {
            if (j >= v[i]) f[j] = max(f[j], f[j - v[i]] + w[i]); //兩種情況取最大值
            else f[j] = f[j]; 
        }
    cout << f[m] << endl;

    return 0;
}

感覺上面的程式碼怪怪的,可以簡寫成下面的樣子:

#include <iostream>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main() {
    //物品個數n
    cin >> n >> m;      //m:揹包容積,就是大V
    //讀入體積和重量
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];

    for (int i = 1; i <= n; i++)
        for (int j = m; j >=v[i]; j--) 
            f[j] = max(f[j], f[j - v[i]] + w[i]); 
        
    cout << f[m] << endl;

    return 0;
}

4、01揹包之恰好裝滿

恰好裝滿:

求最大值時,除了\(dp[0]\) 為0,其他都初始化為無窮小 -0x3f3f3f3f

求最小值時,除了\(dp[0]\) 為0,其他都初始化為無窮大 0x3f3f3f3f

不必恰好裝滿: 全初始化為0

為什麼呢?因為初始化的陣列,實際上是在沒有任何物品可以放入揹包的情況下的合法狀態。如果要求揹包恰好裝滿,那麼此時只有容量為0的揹包可以在什麼都不裝且價值為0的情況下被“恰好裝滿”,其他容量的揹包均沒有合法的解,因此屬於未定義的狀態,應該設定為負無窮大。如果揹包不需要被裝滿,那麼任何容量的揹包都有合法解,那就是“什麼都不裝”。這個解的價值為0,所以初始狀態的值都是0。

#include <iostream>
#include <cstring>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main() {
    //物品個數n
    cin >> n >> m;      //m:揹包容積,就是大V
    
    //初始化f[0]=0,其它設定負無窮
    memset(f,-0x3f,sizeof f);
    f[0]=0;
    
    //讀入體積和重量
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];

    for (int i = 1; i <= n; i++)
        for (int j = m; j >=v[i]; j--) {
            f[j] = max(f[j], f[j - v[i]] + w[i]); 
        }
    cout << f[m] << endl;

    return 0;
}