01背包詳解第一版


title: "01背包詳解"
author: Sun-Wind
date: October 27, 2021

本貼背景:蒟蒻突然被要求去講題.............

什么是01背包

0-1 背包問題:給定n種物品和一個容量為C的背包,物品i的重量是wi,其價值為vi 。
問:應該如何選擇裝入背包的物品,使得裝入背包中的物品的總價值最大?

在上述例子中,由於每個物體只有兩種可能的狀態(取與不取),對應二進制中的0和1,這類問題便被稱為「0-1 背包問題」。

0-1背包問題實質上是一個動態規划問題,解決這個問題我們需要從前一個狀態遞推到下一個狀態,最終遞推到我們想要的狀態

遞推函數

考慮這樣一個函數B(n,c)
這個函數表示從n個物品里面選擇物品,背包容量為c所能達到的最大價值
既然是動態規划的問題,我們應該從上一個狀態尋求思路,找尋兩個狀態之間的聯系

狀態轉換

試想一下,假如我們需要從4個物品里面選擇物品,我們應該先考慮前3個物品的狀態,然后再考慮第4個物品是放還是不放
利用二進制的思想,如果四個物品都不放我們用狀態表示為0000
都放用狀態表示為1111,其他的狀態可以類比推理
顯然,要想得到最優解,對應這個最優解的狀態就一定是0000~1111其中的一種

細節思考

假如,我們知道放前三個物品所對應的最優解是101,也就是取第1件和第3件,第2件不選,這樣選讓目前的背包能達到最大的價值
現在考慮第四件,第四件我們知道要么選要么就不選
如果要選第4件物品,並且這時候背包還能放第4件物品,那么顯然我們應該把這件物品放入背包中
當然存在另外的一種矛盾的情況,就是這個時候背包的容量已經不夠了,有些物品已經占據了背包的格子,但是把這些物品拿出來放第4個物品的價值反而要更大
就是說如果考慮第4件物品最好的狀態可能是1001,0011,甚至可能是0001
如下圖所示
pic1
當然,如果不拿這第4個物品的價值更大,那最優解當然是不拿

既然這樣,我們的遞推方程就可以自然地得出
B(n,c) = max(B(n-1,c),B(n-1,c-w) + v)
其中w指的是這個物品的體積,v指的是這個物品的價值。在之前的例子中指的是第4個物品的體積和價值
也就是說前面的某個物品可能會多>=w的容量
我們把之前的物品拿出來,然后放入現在考慮的物品,就像之前所討論的物品4一樣

核心代碼

根據上述的推論,我們可以得到如下的代碼

for(i = 1; i <= m; ++i)//枚舉個數
    for(j = 1; j <= n; ++j)//枚舉容量
    {
        if(w[i] <= j)//如果能放
            dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - w[i]] + v[i]);
        else//上一個物品的狀態
            dp[i][j] = dp[i - 1][j];
    }

注意一點,這里在考慮每一個物品時我們列舉了背包的每一份容量考慮,目的是保證考慮到每一個狀態
這里的dp數組模擬的就是上述的B(n,c)函數
時間復雜度為O(NV)

例題講解1

hdu2602

題目翻譯

很多年前,在泰迪的故鄉有一個被稱為“骨頭收集者”的人。這個男人喜歡收集各種各樣的骨頭,如狗,牛,鳥......
骨收集器有一個很大的袋子,沿着他收集的旅行有很多骨骼,顯然,不同的骨骼有不同的價值和不同的體積,現在給出了每次骨頭的價值和體積,你可以計算骨收集器可以獲得的總價值的最大值最多?

輸入

第一行包含整數T,案例的數量。
其次是T例,每種情況三行,第一行包含兩個整數n,v,(n <= 1000,v <= 1000)表示骨骼的數量和他袋子的體積。第二行包含表示每個骨骼體積的n個整數。第三行包含表示每個骨骼的價值的n個整數。

輸入

1
5 10
1 2 3 4 5
5 4 3 2 1

輸出

14

顯然,這是一道01背包的板子題,我們直接套上我們的核心代碼就可以解決
代碼如下

#include<iostream>
using namespace std;
const int N = 1e3+5;
int dp[N][N];//表示B函數
int w[N];//表示體積
int v[N];//表示價值
int n,m;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int t;
    cin >> t;
    while(t--){
       cin >> n >> m;
       for(int i = 1; i <= n; ++i)
       for(int j = 0; j <= m; ++j)
        dp[i][j] = 0; //每一次要重新把數組更新為初始狀態,防止被上一個樣例影響
        //輸入
       for(int i = 1; i <= n; ++i)
        cin >> v[i];
       for(int i = 1; i <= n; ++i)
        cin >> w[i];
        //核心代碼
       for(int i = 1; i <= n; ++i)
        for(int j = 0;j <= m;j++)
            if(j >= w[i])
                dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]] + v[i]);
            else
                dp[i][j] = dp[i-1][j];

       cout << dp[n][m] << endl;//最后的結果,即遞推到最后的狀態是考慮n個物品,背包容量為m時能得到的最大價值
    }
    return 0;
}

01背包空間復雜度的優化

剛剛我們從二維的角度來思考B(n,c)函數,空間復雜度為O(nv),現在我們嘗試把空間復雜度降到O(v)
這時我們的B函數只有一個參數C(背包的容量)
也就是說我們在每次遍歷時,背包里面剛開始存的是上一個狀態的,核心代碼變成了這樣

for(i = 1; i <= m; ++i)//枚舉個數
    for(j = w[i]; j <= n; ++j)//枚舉容量
        dp[j] = max(dp[j],dp[j - w[i]] + v[i]);

像我們之前的思考那樣
如果j < w[i] 之前是dp[i][j] = dp[i-1][j]
這里就不考慮dp[j],所以dp[j]將保存上一次的狀態,等價於上述的式子
如果j >= w[i],之前是dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]] + v[i]);
現在是dp[j] = max(dp[j],dp[j - w[i]] + v[i]);
兩者都是在考慮i-1個物品時容量為j的最大價值和上一狀態要把這個物品放進去這兩個狀態之間
得到的最大價值
既然都是等價的,理論上我們應該可以直接套用這個新的板子,而且還省了一點代碼

細節思考

其實依然存在一些問題,等價但不完全等價,關鍵點在於循環順序
試着考慮這樣的一個問題,我們考慮j狀態和2j狀態
j狀態的所面臨的問題

dp[j] = max(dp[j],dp[j - w[i]] + v[i]);

2j狀態所面臨的問題

dp[2j] = max(dp[2j],dp[2j-w[i]] + v[i]);

當j=w[i]時我們可以看到

dp[j] = max(dp[j],dp[0] + v[i]);
dp[2j] = max(dp[2j],dp[j] + v[i]);

對於同一個物品,在循環到j=w[i]和2j時都要考慮放與不放的問題
所以我們可能在dp[j]時已經把這個物品放進去了,但是在dp[2j]時我們又放了一次
這就違背了題目中每個物品只有一件的題意

問題出在哪里?
理論上難道不是等價的嗎
其實我們可以發現dp[2j] = max(dp[2j],dp[j] + v[i]);這里的dp[j]如果已經被更新過(也就是已經被放進去過一次了)那么它保存的就是這個狀態,而不是上一個狀態

真正的優化

所以我們重新考慮循環的順序,我們采用倒序循環,也就是

for(i = 1; i <= m; ++i)//枚舉個數
    for(j = n; j >= w[i]; --j)//枚舉容量
        dp[j] = max(dp[j],dp[j - w[i]] + v[i]);

顯然,這樣我們就可以保證max中比較的狀態都是上一個狀態
空間優化迎刃而解

優化過后的代碼

#include<iostream>
using namespace std;
const int N = 1e3+5;
int dp[N];//表示B函數
int w[N];//表示體積
int v[N];//表示價值
int n,m;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int t;
    cin >> t;
    while(t--){
       cin >> n >> m;
       for(int i = 0; i <= m; ++i)
        dp[i] = 0; //每一次要重新把數組更新為初始狀態,防止被上一個樣例影響
        //輸入
       for(int i = 1; i <= n; ++i)
        cin >> v[i];
       for(int i = 1; i <= n; ++i)
        cin >> w[i];
        //核心代碼
       for(int i = 1; i <= n; ++i)
        for(int j = m;j >= w[i];--j)
                dp[j] = max(dp[j],dp[j-w[i]] + v[i]);

       cout << dp[m] << endl;//最后的結果,即遞推到最后的狀態是考慮n個物品,背包容量為m時能得到的最大價值
    }
    return 0;
}

背景:之所以要寫擴展是害怕到時候沒到下課就把上面講完了

01背包擴展之完全背包

什么是完全背包

完全背包問題:和01背包大致類似,唯一不一樣的是每個物品不是只有一件了,而是有無限多件了,這時候問你背包所能獲得的最大價值是多少

思路解析

既然很多地方都和01背包一樣,那么我們可以從01背包中來獲取思路
我們發現無限多件物品其實就等價於可以重復地放這個物品

想到什么了沒
我們在討論01背包問題的時候,其中就考慮了重復放置物品的問題

for(i = 1; i <= m; ++i)//枚舉個數
    for(j = w[i]; j <= n; ++j)//枚舉容量
        dp[j] = max(dp[j],dp[j - w[i]] + v[i]);

還記得我們最初優化的那個錯誤的代碼嗎,沒錯,它討論的就是完全背包的問題,每個物品可以重復的放在背包當中
所以其實我們在討論01背包時已經順帶解決了完全背包的問題,上述代碼就是完全背包的核心代碼

例題講解2

洛谷P1616

題目大意

一個人有m的時間采n種葯,每種葯可以無限次采摘,問在規定時間內所能采得葯物得最大價值

輸入

70 3
71 100
69 1
1 2

輸出

140

這是一道完全背包的板子題
在題目中,時間相當於背包,葯物相當於物品

#include<iostream>
using namespace std;
const int N = 1e7+5;
long long dp[N];
int w[10005],v[10005];
int main()
{
    int n,m;
    cin >> m >> n;
    for(int i = 1; i <= n; ++i)
        cin >> w[i] >> v[i];
    for(int i = 1; i <= n; ++i)
        for(int j = w[i]; j <= m; ++j)
            dp[j] = max(dp[j],dp[j-w[i]] + v[i]);//這一段核心代碼和之前解釋的一樣
    cout << dp[m] << endl;
}

01背包擴展之多重背包

什么是多重背包

多重背包也是 0-1 背包的一個變式。與 0-1 背包的區別在於每種物品有ki個,而非一個,也不是無窮多個
多重背包和01背包,完全背包都不相同,關鍵在於它的每個物品都有上限

朴素做法

考慮一個朴素的做法,既然總的物品有數量限制,假設物品的數量和為sum
那么問題就轉化為有sum個物品,每個物品只有一件的01背包問題
轉化之后的代碼

for(int i = 1; i <= sum; ++i)
    for(int j = b; j >= w[i]; --j)
        dp[j] = max(dp[j],dp[j - w[i]] + m);

時間復雜度為O(sum*V)

例題講解3

Acwing4
此題是完全背包的模板題,上述解釋看懂了應該就沒有什么問題

#include<iostream>
using namespace std;
int w[105],v[105];
int dp[105];
int main()
{
    int a,b;
    cin >> a >> b;
    while(a--)
    {
        int n,m,s;
        cin >> n >> m >> s;
        for(int i = 1; i <= s; ++i)//分割為01背包問題
            for(int j = b; j >= n; --j)
                dp[j] = max(dp[j],dp[j - n] + m);
    }
    cout << dp[b] << endl;
}

這是第一版的01背包
后續還有完全背包的二進制優化,分組背包和混合背包問題
有興趣的同學可以看一下
如果支持過5,可以考慮寫第二版
好吧,今天的分享就到這里,創作不易,感謝大家的支持


免責聲明!

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



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