對01背包的分析與理解(圖文)


 

首先謝謝Christal_R文章(點擊轉到鏈接)讓我學會01背包

本文較長,但是長也意味着比較詳細,希望您可以耐心讀完。

題目:

現在有一個背包(容器),它的體積(容量)為V,現在有N種物品(每個物品只有一個),每個物品的價值W[i]和占用空間C[i]都會由輸入給出,現在問這個背包最多能攜帶總價值多少的物品?

一.動態規划與遞推解決01背包

初步分析:

0. 淺談問題的分解

在處理到第i個物品時,可以假設一共只有i個物品,如果前面i-1個物品的總的最大價值已經定下來了,那么第i個物品選不選將決定這1~i個物品能帶來的總的最大價值

剛剛是自頂向下,接下來反過來自底向上,第1個物品選不選可以輕松地用初始化解決,接下來處理第i個物品時,假設只有2個物品就好,那他處理完后前2個物品能帶來的最大總價值就確定了,這樣一直推下去,就可以推出前n個物品處理完后能帶來的最大總價值

 

1.分層考慮解決"每個物品最多只能裝一次"

每個物品只能裝一次,那么就應該想到常用的一種方法,就是用數組的縱軸來解決,對於n個物品,為它賦予i=1~n的編號,那么數組的縱軸就有n層,每層只考慮裝不裝這個物品,那么分層考慮就可以解決最多裝一個的問題了

 

2.對0,1的理解

對於每個背包,都只有0和1的情況,也就是拿或者不拿兩種情況

如果拿:那么空間就會減一點,比如說現在在考慮第i個物品拿不拿,如果說當前剩余空間為j,那么拿了之后空間就變為j-c[i],但是總價值卻會增加一點,也就是增加w[i]

如果不拿:那么空間不會變,還是j,但是總價值也不會變化

 

3.限制條件

所以對於這題來說有一個限制條件,就是空間不超出,然后目標就是在空間不超出的情況塞入物品使總價值最大,在前面,我們已經講了數組的縱軸用來表示當前處理到第幾個物品,那么只靠這個是不夠的,而且這個數組的意義還沒有講

這題就是限制條件(空間)與價值的平衡,你往背包中塞東西,價值多了,可是空間少了,這空間本來可能遇到性價比更高的物品但也可能沒遇到

4.具體的建立數組解決問題

有了前面的限制情況和0,1的分析就可以建立數組了

對於這個數組,結合題目要求來說,數組的意義肯定是當前的總價值,也就是第i個物品的總價值,那么題目還有一個限制條件,只靠一個n層的一維數組是不夠的,還需要二維數組的橫軸來分析當前的剩余容量

所以我們有了一個數組可以來解決問題了,這個數組就叫f好了,然后它是一個二維數組,它的縱軸有i層,我希望它從i=1~n,不想從下標0開始是為了美觀,然后這個二維數組的橫軸代表着當前剩余的空間,就用j來表示,j=0~V,0就是沒有空間的意思,V前面說了,是這個背包的總容量

我們把這個二維數組建立在int main()的上面,所以它一開始全部都是0,省去了接下來賦初值為0的功夫

有了數組f[i][j],然后對於每個f[i][j],它表示的是已經處理到第i個物品了,當剩余空間還有j時,能帶有的最大價值,也就是說f[i][j]存儲的是總價值

說是總價值,可是涉及到放物品還是不放物品的問題,所以再細致點就是:當前剩余空間為j,用這j空間取分析第i個物品裝不裝如,處理執行完行為后,f[i][j]就表示了當前能裝入的最大價值

 

5.推導遞推方程

PS:談一下對於動態規划遞推的理解:處理到第i層時,假設前i-1層的數據都知道而且可以根據1~i-1層的數據推出i,那么就成功了一半了,因為第i層如此,那么第i-1層也可以根據1~i-2層推出,接下來只需要定義好數組的初始條件和注意邊緣問題以及一些細節就可以了

對於第i個物品,假設前i-1個物品都已經處理完

如果第i個物品不能放入:這種情況就是背包已經滿了,也就是當前剩余空間j小於第i個物品的占用空間C[i],

這種情況下,空間沒有變化,價值也沒有變化,對於空間沒有變化,即第i個物品的空間和第i-1個物品的空間j相同,對於價值沒有變化,也就是數組f的值相同,然后開始利用前面的數據,也就是f[i][j]]=f[i-1][j]

 

如果第i個物品不想放入,那么和不能放入其實是一樣的,動機不同但結果相同,f[i][j]]=f[i-1][j]

 

如果第i個物品放入了,那么f[i][j]=f[i-1][j-c[[i]]+w[i],下面解釋一下這個公式,第i個物品的占用空間為c[i],價值為w[i],f[i-1][j-c[[i]]+w[i]表示前i-1個物品在給它們j-c[[i]空間時能帶來的最大價值

再回到第i個物品的角度,此時有j個空間,如果已經確定要放入,為了使空間充分利用,肯定是這j個空間只分c[i](剛好夠塞下第i個物品),剩下的j-c[[i]全部給前面i-1個物品自由發揮,反正前面f[i-1][j-c[[i]]已經知道了,然后前面i-1個物品用j-c[i]的空間能帶來最大的利益f[i-1][j-c[[i]],第i個物品用c[i]的空間帶來利益w[i],所以如果第i個物品放入后,總利益是f[i][j]=f[i-1][j-c[[i]]+w[i]

 

但是,長遠來說,有一些偏極端情況,放入這個物品,也許它價值w[i]很高,但是它占用空間c[i]也大,它的性價比可能很低,所以這時候就需要max函數了

當還有空間時:F[i,j] = max[F[i−1,j],F[i−1,j−C[i]] + W[i]

當空間不夠時:F[i,j] = F[i−1,j]

下面一個個解釋:

當還有空間時:這時有兩種方法,放還是不放,如果放,那么利益由兩段組成1~i-1是一段,i是另一段;如果不妨,那么利益和上一層剩j空間時相同,這兩個東西大小需要比較,因為如果放入,雖然加上了w[i],利益,可是沖擊了前i-1個物品的利益,如果不放,那么沒有收獲到第i個物品的利益,但是把原來屬於1~i的空間j,分給了1~i-1個物品,說不定前1~i-1的每個物品都空間小,價值高,性價比高呢?

當空間不夠時,它也只能F[i,j] = F[i−1,j]了,沒有選擇的余地

 

#include<bits/stdc++.h>//萬能頭文件
#define ll long long
using namespace std;
const ll maxn=100;
ll n,v,f[maxn][maxn];
ll c[maxn];//每個物品占用空間
ll w[maxn];//每個物品的價值
int main()
{
    cin>>n>>v;
    for(ll i=1;i<=n;i++)
        scanf("%lld",&c[i]);
    for(ll i=1;i<=n;i++)
        scanf("%lld",&w[i]);
    for(ll i=1;i<=n;i++)//第i個物品
        for(ll j=v;j>=0;j--)//剩余空間j
        {
            if(j >= c[i])//如果裝得下
                    f[i][j]=max( f[i-1][j-c[i]]+w[i],f[i-1][j]);
            else//如果裝不下
                f[i][j]=f[i-1][j];
        }
    cout<<f[n][v]<<endl;//輸出答案

}
01背包普通版代碼
點擊加號展開代碼,如果點不開可以看底下的代碼

 

 

二.01背包的空間優化

有了前面基礎版01背包的學習,現在學習這個就容易多了

1.何為空間優化,為什么要空間優化

在01背包中通過對數組的優化(用了滾動數組的方法),可以使本來N*V的空間復雜度降低V,也就是把關於第幾個物品的N去掉了(下面會解釋為什么可以這么做)

至於為什么要空間優化,首先是因為遞推本來就是用空間換時間,消耗的空間比較大,然后關於算法的競賽一般都會有空間的限制要求,最后,在找工作面試時,面試官肯定會問一些優化的問題,平時養成優化的習慣面試時也有好處

2.為什么這題可以降維

通過觀察可以發現對於普通版的01背包遞推式,f[i][...]只和f[i-1][...]有關,那么我們可以用一種占用,一種滾動的方法來循環使用數組的空間,所以這個方法叫滾動數組,對於將來肯定用不到的數據,直接滾動覆蓋即可,具體的如何滾動會放下面講

還有就是滾動數組的缺點犧牲了抹除了大量數據,不是每道題都可以用,但是在這,答案剛好是遞推的最后一步,所以直接輸出即可,遞推完后不需要調用那些已經沒了的數據,所以這題可以

下面先畫個圖理解一下滾動的大致概念

反正就是不斷覆蓋的過程

3.這題如何具體優化

下面開始具體化的分析

對於第i層,它只和第i-1層有關,但是對於剩余空間j無法優化,所以現在拿i開刀,把他砍掉,用一個長度為V(總空間)的數組來表示,然后每次相鄰的兩個i和i-1在上面一直滾動

所以現在建立一個數組f[V],一維數組大小為V

首先建立兩個復合for循環

for(i=1~n)

  for(j=v~0)

記住這里第二層循環必須是v~0而不是0~v,先記着,后面會解釋,

接下來的分析建議配合下面圖片學習

然后在循環的過程中,還是老樣子,假設我們已經循環到i=2這層了(也就是說i=1已經循環完了),然后對於i=2這一層,我們對j循環,j從v到0

假如現在j=v,我們讓f[j]=max(f[j],f[j-c[i]]+w[i])

在沒有覆蓋之前,所有的f數據都是屬於上一層也就是第一層的,我們就當作i-1層數據已經准備好了,然后把max內的拆成兩半分析,對於f[j]=f[j]就是不放的情況,那么總價值沒有改變,所以對於f[j]=f[j]就是形式上的更新數據,把i-1層的f[j],給了i-1層的f[j]...對於f[j]=f[j-c[i]]+w[i],那個w[i]是肯定要加的不用討論,然后我們觀察一下,對於下標j-c[i]是不是肯定會小於j,那么如果說j從V~0也就是從最大到最小,每次賦值處理都是從前面的格子看看數據參考,並沒有修改

再詳細點說的話就是對於f[j]=f[j-c[i]]+w[i],f[j-c[i]]是第i-1層的東西,讓j=v~0是為了讓f數組每次滾動覆蓋時都是覆蓋接下來不需要用的位置,比如說處理到第f[8]位時,假如接下來的max判定后面那種方法總價值大,然后假設c[i]=3,這時后就相當與f[8]=f[8-c[i]=5]+w[i],我們這里只是參考了f[5]的數據,並沒有改變它,因為說不定計算新一輪f[6]時又要用到舊的f[5]呢,可是我們刷新了f[8]的數字后,再j--,計算f[7],再j--,計算f[6],都不會再用到f[8]這個數據,這是由於f[j-c[i]] 中的減c[i]導致的,反之,假若我們讓j=0~v,就可能出現新數據被新數據覆蓋的結果,我們是有"底線"的,只允許新數據覆蓋舊數據

對於j,如果要處理f[j]=max(f[j],f[j-c[i]]+w[i]),就得當j>=c[i]時處理,因為如果j<c[i],那么j-c[i]為負,下標負的情況沒必要考慮,如果考慮了還可能會溢出

 其實對於max,還用另一個小東西代替,有沒有發現,如果f[j-c[i]]+w[i]>f[j],就選f[j-c[i]]+w[i],如果f[j-c[i]]+w[i]<f[j],那選f[j]和沒選一樣,所以待會的空間優化版省掉了max函數,少用一種函數

#include<bits/stdc++.h>//萬能頭文件
#define ll long long
using namespace std;
const ll maxn=100;
ll n,v,f[maxn];
ll c[maxn];//每個物品占用空間
ll w[maxn];//每個物品的價值

int main()
{
    cin>>n>>v;
    for(ll i=1;i<=n;i++)
        scanf("%lld",&c[i]);
    for(ll i=1;i<=n;i++)
        scanf("%lld",&w[i]);
    for(ll i=1;i<=n;i++)//第i個物品
        for(ll j=v;j>=1;j--)//剩余空間j
        {
            if(f[j]<=f[j-c[i]]+w[i] && j-c[i]>=0 )//二維數組變一維數組
                 f[j]=f[j-c[i]]+w[i];//如果值得改變並且j的空間還裝得下就賦新值
        }
    cout<<f[v]<<endl;//輸出答案

}
空間優化版01背包
點擊"+"號展開代碼,為了排版好看把代碼折疊了,為了防止有人點不開文章底部還有一份沒折疊的

三.初始化的細節

初始化有兩種,一種情況是只要求價值最大,另外一種是要求完全剛好塞滿,第一種的初始化是賦值為0,第二種的初始化是賦值為負無窮,因為沒有塞滿,所以數據實際上不存在,也就是讓不存在的數不現實化,讓與這種數相關的數據都不可用化

下面貼一些背包九講的文字

1.4 初始化的細節問題
我們看到的求最優解的背包問題題目中,事實上有兩種不太相同的問法。 有的題目要求“恰好裝滿背包”時的最優解,有的題目則並沒有要求必須把背 包裝滿。一種區別這兩種問法的實現方法是在初始化的時候有所不同。
3
如果是第一種問法,要求恰好裝滿背包,那么在初始化時除了F[0]為0,其 它F[1..V ]均設為−∞,這樣就可以保證最終得到的F[V ]是一種恰好裝滿背包的 最優解。 如果並沒有要求必須把背包裝滿,而是只希望價格盡量大,初始化時應該 將F[0..V ]全部設為0。 這是為什么呢?可以這樣理解:初始化的F數組事實上就是在沒有任何物 品可以放入背包時的合法狀態。如果要求背包恰好裝滿,那么此時只有容量 為0的背包可以在什么也不裝且價值為0的情況下被“恰好裝滿”,其它容量的 背包均沒有合法的解,屬於未定義的狀態,應該被賦值為-∞了。如果背包並非 必須被裝滿,那么任何容量的背包都有一個合法解“什么都不裝”,這個解的 價值為0,所以初始時狀態的值也就全部為0了。 這個小技巧完全可以推廣到其它類型的背包問題,后面也就不再對進行狀 態轉移之前的初始化進行講解。
初始化的細節問題-背包九講

四.常數級的優化

1.5 一個常數優化
上面偽代碼中的
for i = 1 to N for v = V to Ci
中第二重循環的下限可以改進。它可以被優化為
for i = 1 to N for v = V to max(V −ΣN i Wi,Ci) 這個優化之所以成立的原因請讀者自己思考。(提示:使用二維的轉移方程思 考較易。)
常數級優化-背包九講

不得不說,這也太摳門了,算法效率追求到極致

 

 五.小結

01背包很重要,是后面的基礎

要學會推導狀態轉移方程與實現它

要學會去優化空間復雜度

PS:祝每個看到這里的人都能掌握01背包

 

 

接下來放一下代碼大合集

普通版代碼
#include<bits/stdc++.h>//萬能頭文件
#define ll long long
using namespace std;
const ll maxn=100;
ll n,v,f[maxn][maxn];
ll c[maxn];//每個物品占用空間
ll w[maxn];//每個物品的價值
int main()
{
    cin>>n>>v;
    for(ll i=1;i<=n;i++)
        scanf("%lld",&c[i]);
    for(ll i=1;i<=n;i++)
        scanf("%lld",&w[i]);
    for(ll i=1;i<=n;i++)//第i個物品
        for(ll j=v;j>=0;j--)//剩余空間j
        {
            if(j >= c[i])//如果裝得下
                    f[i][j]=max( f[i-1][j-c[i]]+w[i],f[i-1][j]);
            else//如果裝不下
                f[i][j]=f[i-1][j];
        }
    cout<<f[n][v]<<endl;//輸出答案

}
空間優化版代碼
#include<bits/stdc++.h>//萬能頭文件
#define ll long long
using namespace std;
const ll maxn=100;
ll n,v,f[maxn];
ll c[maxn];//每個物品占用空間
ll w[maxn];//每個物品的價值

int main()
{
    cin>>n>>v;
    for(ll i=1;i<=n;i++)
        scanf("%lld",&c[i]);
    for(ll i=1;i<=n;i++)
        scanf("%lld",&w[i]);
    for(ll i=1;i<=n;i++)//第i個物品
        for(ll j=v;j>=1;j--)//剩余空間j
        {
            if(f[j]<=f[j-c[i]]+w[i] && j-c[i]>=0 )//二維數組變一維數組
                 f[j]=f[j-c[i]]+w[i];//如果值得改變並且j的空間還裝得下就賦新值
        }
    cout<<f[v]<<endl;//輸出答案

}

 


免責聲明!

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



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