又鴿了好久……
前言
博主剛剛學會背包問題不久,然后有一段時間沒練習了
今天就來重新溫習一下,順手就寫了這一篇博客。
好了,下面進入正題!
算法簡介
背包問題是動態規划的一個分支
主要是分成了01背包、完全背包和多重背包。
下面從01背包開始講解。
背包算法介紹
01背包
基本概念
01背包是在M件物品取出若干件放在空間為W的背包里,每件物品的體積為W1,W2至Wn,與之相對應的價值為P1,P2至Pn。01背包可謂是背包問題中最簡單的問題。01背包的約束條件是給定幾種物品,每種物品有且只有一個,並且有權值和體積兩個屬性。在01背包問題中,因為每種物品只有一個,對於每個物品只需要考慮選與不選兩種情況。如果不選擇將其放入背包中,則不需要處理。如果選擇將其放入背包中,由於不清楚之前放入的物品占據了多大的空間,需要枚舉將這個物品放入背包后可能占據背包空間的所有情況。
問題雛形
有N件物品和一個容量為V的背包。第i件物品的體積是c[i],價值是w[i]。求解將哪些物品裝入背包可使價值總和最大。
問題解答
了解了基本概念和問題雛形后我們就可以來想做題的方法了。
從題目里看,我們就能看出,01背包有個特點:每種物品僅有一件,可以選擇放或不放。
所以我們就可以把每種情況都枚舉一遍。
首先建立一個二維數組dp[][]表示價值,w[i]是每件物品的價值,c[i]是每件物品的體積
然后就想,由於它只有放和不放兩種狀態,我們就要比較這兩種狀態的價值,用max函數。
狀態轉移方程:dp[i][v]=max{dp[i-1][v],dp[i-1][v-c[i]]+w[i]}
其中,dp[i-1][v]表示不放該物品,dp[i-1][v-c[i]]+w[i]表示放入該物品。
這樣做一個循環枚舉各種情況即可。
for (i = 1; i <= n; i++) for (j = v; j >= c[i]; j--)//在這里,背包放入物品后,容量不斷的減少,直到再也放不進了 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + w[i]);
最后的結果就是最大值。
還有一些優化的方法:01滾動和就地滾動。
01滾動:
我們可以看到每一行的結果實際上只與上一行有關,所以就可以01滾動——f[i][0,1] 一行記錄前一行的值,另一行記錄當前行的值……
所以,這是一種簡化的好辦法!
就地滾動:
就地滾動就是用一個一維數組,把之前的狀態和當前的狀態放在同一個數組,但是在寫的過程中會有問題。
先上代碼吧:
for(i=1 ; i<= n ; i++) for(j= c[i]; j<v ; j++) if(!dp[j-c[i]) dp[j] = dp[j-c[i]];
我們會發現,這樣的話一個物品會被重復計算多次。
實戰演練
問題1:采葯
這是一個經典的問題哦!
飛機場:洛谷P1048 采葯
問題解答(不可用於直接AC本題,可進行參考!)
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #include<cstring> 5 #include<algorithm> 6 #define inf 100000000 7 //狀態:dp[i][j]表示考慮前i個草葯,目前體積之和為j,可以獲得的最大價值 8 //轉移方程:dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]) 9 //答案:dp[m][max(...)] 10 //不需要初始化,直接算 11 //復雜度:O(m*t) 12 int dp[105][1005],w[105],v[105]; 13 using namespace std; 14 int main() 15 { 16 int t,m; 17 cin>>t>>m; 18 for(int i=1;i<=m;i++) 19 { 20 cin>>w[i]>>v[i]; 21 } 22 for(int i=1;i<=m;i++) 23 for(int j=t;j>=0;j--) 24 { 25 if(j-w[i]>=0) 26 dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]); 27 else 28 dp[i][j]=dp[i-1][j]; 29 } 30 int ans=0; 31 for(int i=0;i<=t;i++) 32 { 33 ans=max(dp[m][i],dp[m][i-1]); 34 } 35 cout<<ans; 36 return 0; 37 }
當然還有一種做法。可以直接使用一維數組:參見一本通。
完全背包
基本概念
這個問題非常類似於01背包問題,所不同的是每種物品有無限件,也就是從每種物品的角度考慮,與它相關的策略已並非取或不取兩種,而是有取0件、取1件、取2件……取[V/c]件等很多種。
問題雛形
有 N 種物品和一個容量為 V 的背包,每種物品都有無限件可用。放入第 i 種物品的費用是 Ci ,價值是 Wi 。求解:將哪些物品裝入背包,可使這些物品的耗費的費用總和不超過背包容量,且價值總和最大。
問題解答
這個題目有一個和01背包不一樣的地方:每種物品有無數件!
然后我們想前面我說過的就地滾動,會計算多次,這不正巧?
實戰演練
問題2:瘋狂的采葯
這個……這個題目的介紹不大正經,未成年人請在家長的陪伴下觀看。
飛機場:洛谷P1616 瘋狂的采葯
問題解答(不可用於直接AC本題,可進行參考!)
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #include<cstring> 5 #include<algorithm> 6 #define inf 100000000 7 //狀態:dp[i][j]表示考慮前i個草葯,目前體積之和為j,可以獲得的最大價值 8 //轉移方程:dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]) 9 //答案:dp[m][max(...)] 10 //不需要初始化,直接算 11 //復雜度:O(m*t) 12 int f[1000001],t,m,ti,v; 13 using namespace std; 14 int main() 15 { 16 cin>>t>>m; 17 for(int i=1;i<=m;i++) 18 { 19 cin>>ti>>v; 20 for(int j=ti;j<=t;j++) 21 f[j]=max(f[j],f[j-ti]+v); 22 } 23 cout<<f[t]; 24 return 0; 25 }
多重背包
基本概念&問題雛形
有N種物品和一個容量為V的背包。第i種物品最多有n[i]件可用,每件體積是w[i],價值是v[i]。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
問題解答
這個問題的特點是:每種物品有一定數量
這個運用的是二進制思想!
轉化為01背包求解:把第i種物品換成n[i]件01背包中的物品,則得到了物品數為Σn[i]的01背包問題。
我們考慮把第i種物品換成若干件物品,使得原問題中第i種物品可取的每種策略——取0..n[i]件——均能等價於取若干件代換以后的物品。
另外,取超過n[i]件的策略必不能出現。 方法是:將第i種物品分成若干件物品,其中每件物品有一個系數,這件物品的費用和價值均是原來的費用和價值乘以這個系數。
使這些系數分別為1,2,4,...,2^(k-1),n[i]-2^k+1,且k是滿足n[i]-2^k+1>0的最大整數。例如,如果n[i]為13,就將這種物品分成系數分別為1,2,4,6的四件物品。
解決問題的道理
1) 1+2+4+...+2^(k-1)+n[i]-2^k+1 = n[i] 這就保證了最多為n[i]個物品
2)1,2,4,……,2^(k-1),可以湊出1到2^k – 1的所有整數(聯系一個數的二進制拆分即可證明,證明過程在下面的題解中)
3) 2^k……n[i]的所有整數可以用若干個上述元素湊出(可以理解為湊n[i]-t, 而n[i]為上面所有數的和,t則是一個小於2^k 的數,那么在所有的數中去掉組成2^k 的那些數剩下的就可以組成n[i]-t了)
當然,這個二進制的道理我在之前的一篇博客上寫過,一會的實戰演練會帶你們去。
實戰演練
問題3:寶物篩選
這是一道很水的藍題……
飛機場:洛谷P1776 寶物篩選題解(我寫的,你們還有更詳細的多重背包解決思路)
后記
本文就寫這么多了,背包問題在考試中會經常出現,希望大家深入理解其中的思想。
客官,給個贊再走唄?