揹包問題(01揹包,徹底揹包,多重揹包(樸素算法&&二進制優化))

寫在前面:我是一隻蒟蒻~~~html

今天咱們要講講動態規劃中最最最最最簡單的揹包問題c++



1. 首先,咱們先介紹一下 數組


 01揹包

你們先看一下這道01揹包的問題  
題目  
有m件物品和一個容量爲n的揹包。第i件物品的大小是w[i],價值是k[i]。求解將哪些物品裝入揹包可以使這些物品的費用總和不超過揹包容量,且價值總和最大。    
題目分析:
咱們剛剛看到這個題目時,有的人可能會第一想到貪心,可是通過實際操做後你會很~~神奇~~的發現,貪心並不能很好的解決這道題(沒錯,本蒟蒻就是這麼錯出來的)。這個時候就須要咱們很是強大的動態規劃(DP)出馬。  
  咱們能夠看出,本題主要的一個特色就是關於物品的選與不選。這時候咱們就會想如何去處理,纔可使咱們裝的物品價值總和最大,並且這道題的物品只有一個,要麼選一個,要麼不選。因此這個時候咱們就能夠推出它的狀態轉移方程(啥!你不知道啥是狀態轉移方程?那你自行理解吧)。  
    咱們設f[i][j]爲其狀態。就有了如下式子
 1 f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+k[i]); 
  i表示件數,j表示空間大小。  
  f[i][j]就表示i件物品下揹包空間爲j的狀態。  
  f[i-1][j]表示在i-1件時揹包空間爲j的狀態(在這中間則表明了在i件時不取這件物品)。   
  f[i-1][j-w[i]]+k[i]表示取這件物品後該揹包的空間爲j-w[i],而總價值則增長了k[i]。   
  可能會有人問,這個式子跟個人貪心式子比有什麼不同的嗎?  
  固然,這個式子能切掉這道題而貪心不行(這不是廢話嗎!!!)
   嗯,說重點,這個式子只是牽扯到i-1件物品的問題,與其餘無關,因此這就很好的解決了貪心對全局的影響。  
   能夠顯而易見的是其時間複雜度O(mn)(m是件數,n是枚舉的空間)已經很優秀了,可是它的空間複雜度仍是比較高,因此咱們就可使用一維數組進行優化,具體怎樣優化,咱們下面再說。   
   好了,說完這一題的核心碼咱們就能夠得出f[m][n]所獲得的是最優解。(爲何??!!,若是你還不理解的話那我建議你上手動模擬一下,固然你也能夠進入這裏看一下是怎麼操做的。
 嗯,這道題就結束了,咱們來一道確切存在的題目(洛谷)P1060 開心的金明
         下面就是這道題的AC代碼(若是你看懂了上面,代碼就不難理解了)
         ide

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 int n,m;
 4 int f[30][30007],w[30],v[30],k[30];//根據題目要求設置變量,f就表示狀態
 5 void dp(){
 6     memset(f,0,sizeof(f));//初始化(通常可忽略)
 7     for(int i=1;i<=m;i++){//枚舉物品數量
 8         for(int j=w[i];j<=n;j++){//枚舉揹包空間
 9             if(j>=w[i]){//若是揹包空間可以裝下下一件物品進行狀態轉移
10                 f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+k[i]);//轉移方程
11             }
12         }
13     }
14 }
15 int main(){
16     scanf("%d%d",&n,&m);
17     for(int i=1;i<=m;i++){
18         cin>>w[i]>>v[i];
19         k[i]=w[i]*v[i];//讀入+處理
20     }
21     dp();//進行處理
22     printf("%d",f[m][n]);
23     return 0;
24 }

 


這裏對於01揹包的講解基本就結束了,下面給你們推薦幾道題來練習,P1164 小A點菜      P1048 採藥    P1049 裝箱問題  。    
     
 最後,我來填一下我上面留下來的坑,如何優化二維01揹包的空間複雜度。  
 很簡單,就是把二維變爲一維(啥!你說不明白?)這難道不是很顯然的事情嗎?你從f[i][j]變爲f[i]直接縮小一維,空間不就小了一維嗎。好了,下面,咱們就談談如何實現的減維。   
 咱們知道枚舉從1~i來算出來f[i][j]的狀態。因此,咱們是否是能夠用一個f[j]來表示每地i次循環結束後是f[i][j]的狀態,而f[i][j]是由max(f[i-1][j],f[i-1][j-w[i]]+k[i])遞推出來的,而咱們只有從j=n到0的順序進行枚舉,這樣才能保證推f[j]時f[j-w[i]]保存的是f[i-1][j-w[i]]的狀態值。   
     核心代碼   學習

1 for(int i=1;i<=m;i++){
2     for(int j=n;j>=w[i];j--){
3         f[j]=max(f[j],f[j-w[i]]+k[i]);
4     }
5 }

 


這是一種比較好的寫法,但還有的人(~~好比說我~~)就喜歡這樣寫(由於我很~~勤奮~~)  優化

1 for(int i=1;i<=m;i++){
2     for(int j=n;j>=0;j--){
3         if(j>=w[i]){
4             f[j]=max(f[j],f[j-w[i]]+k[i]);
5            }
6     }
7 }

 


   這樣咱們均可以達到咱們優化空間複雜度的目的(固然,我推薦你們寫第一種,這樣就不用擔憂判斷大小的問題了)。  
    掌握這個優化其實十分重要的,有的題會卡二維數組的空間,這樣咱們只能用一維數組進行解題。   
    嗯,01揹包就講到這裏了,但願可以幫到各位Oier,若有錯誤,請指出,本人定改正。
    

----手動分割一波=^ω^= ------


this



二、瞭解完01揹包,咱們來看一看  spa


徹底揹包.net


老規矩,上題。  
題目(P1616 瘋狂的採藥):因爲本蒟蒻~~比較懶~~,請你們點開自行看題。   
下面進行題目分析:   
咱們不難看出,徹底揹包與01揹包只是物品數量的不一樣,一個是隻有1個,而物品的狀況也只有    取和不取。但徹底揹包倒是有無數多個,這就牽扯到一個物品取與不取和取多少的問題。這是的時間複雜度就再也不是O(nm)了。而通過一些優化(這裏給你們一個地址,你們能夠在這裏去看一看,本蒟蒻就再也不展開講解)  
既然你們都已經明白了怎樣進行優化(哪來的已經啊!!!僞裝僞裝嗎≥﹏≤)  
無論怎麼說,咱們就能夠獲得這個轉移方程  
 1 f[j]=max(f[j],f[j-w[i]]+c[i]); 
相信你們在理解01揹包後,對徹底揹包的狀態轉移方程理解容易些。
其中的思想仍是和01揹包是相同的。    
下面貼出AC代碼   

翻譯

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 int T,n,v[10007],t[10007],f[100007];//變量的定義,f[]表示狀態
 4 
 5 int main(){
 6     scanf("%d%d",&T,&n);//讀入
 7     for(int i=1;i<=n;i++){
 8         cin>>t[i]>>v[i];
 9     }
10     memset(f,0,sizeof(f));//初始化(通常可忽略)
11     for(int i=1;i<=n;i++)//枚舉物品i
12     {
13         for(int j=t[i];j<=T;j++){//揹包空間(必須從t[i]開始,因爲數量是無限的,因此,咱們必需要遞增枚舉)
14                 f[j]=max(f[j],f[j-t[i]]+v[i]);//狀態轉移
15         }
16     }
17     cout<<f[T];//輸出答案
18 }

 

 
綜上,就是徹底揹包的講解,因爲我懶,因此就不給你們推薦題了,我相信你們必定可以練習好的,嗯!我相信你們。(相信什麼相信,快點幹活!!(粉筆飛來)我閃 嗯,不存在的,正中靶心。   
咳咳!咱們來推薦最後一道題P2918 [USACO08NOV]買乾草Buying Hay這一題但願你們好好想想,有點坑,可是並非太難,你們加油吧!!!!!   




三、下一個,本蒟蒻不會!!!!  

 
多重揹包  


等我學會,再來更新~~~~~  
送給你們一個博客揹包九講


hello!我又回來了,今天我就來給你們來說一講我上回留下來的坑。

首先,咱們先介紹一下何爲多重揹包

問題描述:

多重揹包:有N種物品和一個容量爲V的揹包。第i種物品最多有n[i]件可用,每件費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可以使這些物品的費用總和不超過揹包容量,且價值總和最大。
     
 這裏,咱們能夠看到多重揹包與徹底揹包和01揹包多不一樣的在於每件物品有有限多個,因此咱們就產生了一種思路,那就是:將多重揹包的物品拆分紅01揹包~~

這樣一來,咱們就能夠用01揹包的套路來解決這個問題,而這個代碼呢,也很簡單:

1 for(int i=1;i<=n;i++){
2     for(int j=1;j<=num[i];j++){
3         a[++cnt]=v[i];
4     }
5 }

 

這樣一來,咱們就能夠十分簡單的解決這道題了!!!

可是,簡單歸簡單,咱們能夠看到這個時間複雜度是十分不優秀的,因此咱們能夠想想如何優化,

這時候咱們來考慮一下進制的方法,

二進制
 首先,咱們先補充一個結論,就是1~n之內的數,都可以經過n進制之內的數組合獲得。

這樣的話,咱們就能夠經過二進制的拆分來進行優化,咱們把每一個物品有的全部個數,分開,

就好比咱們有這樣一個數,

如今要進行二進制的拆分:

這時咱們進行拆分以後發現還沒法徹底表示整個狀態。。。因此咱們就把這些都加起來:

(just like this)

這樣就OK了

 

核心代碼:

1 for(int i=1;i<=6;i++){
2             for(int j=1;j<=num[i];j<<=1){
3                 v[++cnt]=a[i]*j;
4                 num[i]-=j;
5             }
6             if(num[i]>0)v[++cnt]=num[i]*a[i];//若是還有剩餘,就所有加入 
7         }

 

下面,咱們來看一道例題:
題目描述:

POJ1742 Coins

總時間限制: 
3000ms
 
內存限制: 
65536kB
描述
People in Silverland use coins.They have coins of value A1,A2,A3...An Silverland dollar.One day Tony opened his money-box and found there were some coins.He decided to buy a very nice watch in a nearby shop. He wanted to pay the exact price(without change) and he known the price would not more than m.But he didn't know the exact price of the watch.
You are to write a program which reads n,m,A1,A2,A3...An and C1,C2,C3...Cn corresponding to the number of Tony's coins of value A1,A2,A3...An then calculate how many prices(form 1 to m) Tony can pay use these coins.
輸入
The input contains several test cases. The first line of each test case contains two integers n(1<=n<=100),m(m<=100000).The second line contains 2n integers, denoting A1,A2,A3...An,C1,C2,C3...Cn (1<=Ai<=100000,1<=Ci<=1000). The last test case is followed by two zeros.
輸出
For each test case output the answer on a single line.
樣例輸入
3 10
1 2 4 2 1 1
2 5
1 4 2 1
0 0
樣例輸出
8
4


  這是什麼意思呢?

我大概給你們翻譯一下(原諒我蒟蒻的英語)

就是什麼意思吧,給定N種硬幣,其中第i種硬幣的面值爲Ai,共有Ci個。從中選出若干個硬幣,把面值相加,若結果爲S,則稱「面值S能被拼成」。求1~M之間能被拼成的面值有多少個。

題目分析:

咱們看到題目中給的是一個可行性的問題,咱們只須要依次考慮每種硬幣是否被用於拼成最終的面值,以「已經考慮過的物品種數」i做爲dp的階段,在階段i時咱們用f[i]表示前i種硬幣可否拼成面值j。

法1:(樸素拆分法)

代碼:

 1 bool f[100010];
 2 memset(f,0,sizeof(f));
 3 f[0]=1;
 4 for(int i=1;i<=;i++){
 5     for(int j=1;j<=c[i];j++){
 6         for(int k=m;k>=a[i];k--){
 7             f[k]+=f[k-a[i]];
 8         }
 9     }
10 }
11 int ans=0;
12 for(int i=1;i<=m;i++){
13     ans+=f[i];
14 }
15  

 

這個題,這樣解的話時間複雜度就過高,因此咱們轉換一個思路,來進行二進制拆分,

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 #define maxn 3004
 4 int f[maxn][maxn],a[maxn],b[maxn],n;
 5 
 6 int main(){
 7     scanf("%d",&n);
 8     for(int i=1;i<=n;i++){
 9         scanf("%d",&a[i]);
10     }//讀入 
11     for(int i=1;i<=n;i++){
12         scanf("%d",&b[i]);
13     } 
14     for(int i=1;i<=n;i++){
15         int val=0;//val表明f[i-1][j] 
16         if(b[0]<a[i])val=f[i-1][0];
17         for(int j=1;j<=n;j++){
18              if(b[j]==a[i])f[i][j]=val+1;
19             else f[i][j]=f[i-1][j];//轉移
20             if(b[j]<a[i])val=max(val,f[i-1][j]);//判斷 
21         } 
22     }
23     int maxx=0;
24     for(int i=1;i<=n;i++){
25         maxx=max(maxx,f[n][i]);
26     } 
27     printf("%d\n",maxx);
28      
29     return 0;
30 }

 

 

下面,咱們來看一下另外一道題:

劃分大理石

題目描述:

描述

有價值分別爲1..6的大理石各a[1..6]塊,現要將它們分紅兩部分,使得兩部分價值之和相等,問是否能夠實現。其中大理石的總數不超過20000。 

輸入格式

有多組數據!
因此可能有多行
若是有0 0 0 0 0 0表示輸入文件結束
其他的行爲6個整數

輸出格式

有多少行可行數據就有幾行輸出
若是劃分紅功,輸出Can,不然Can't

樣例輸入

4 7 4 5 9 1
9 8 1 7 2 4
6 6 8 5 9 2
1 6 6 1 0 7
5 9 3 8 8 4
0 0 0 0 0 0

樣例輸出

Can't
Can
Can't
Can't
Can

看完這道題,咱們不難看出,這是一道與P1164 小A點菜 十分類似的題,其中的不一樣點就是一個是01揹包,一個是多重揹包,因此咱們就能夠先用二進制進行拆分,而後再跑一遍DP便可。

代碼:

 

#include<bits/stdc++.h>
using namespace std;
int num[7],a[7],dp[500007],v[100008],sum,cnt;
int main(){
    for(int i=1;i<=6;i++)a[i]=i;
    while(scanf("%d%d%d%d%d%d",&num[1],&num[2],&num[3],&num[4],&num[5],&num[6])){
        if(!num[1]&&!num[2]&&!num[3]&&!num[4]&&!num[5]&&!num[6])break;
        sum=0;
        memset(v,0,sizeof(v));
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=6;i++)sum+=(a[i]*num[i]);
//        printf("%d\n",sum);
        if(sum%2==1){
            printf("Can't\n");
            continue;
        }
        sum=sum/2;
        cnt=0;
        for(int i=1;i<=6;i++){
            for(int j=1;j<=num[i];j<<=1){
                v[++cnt]=a[i]*j;
                num[i]-=j;
            }
            if(num[i]>0)v[++cnt]=num[i]*a[i];//若是還有剩餘,就所有加入 
        }
        dp[0]=1;
        for(int i=1;i<=cnt;i++){
            for(int j=sum;j>=v[i];j--){
                dp[j]+=dp[j-v[i]];
            }
        }
        if(dp[sum])printf("Can\n");
        else printf("Can't\n");
    }
    return 0;
}

 


 

2019.7.16

更新:單調隊列優化多重揹包

嗯,今天咱們在課上學習了單調隊列優化多重揹包的方法(學的什麼呀,都不會好吧),

首先,咱們先來講一下,若使用單調隊列來優化的話,時間複雜度可降至O(NM),

首先,題面已經再也不須要敘述了。

咱們上一次的狀態轉移方程是將「階段」這一維省略掉,

f[j]表示在前i種物品中選出若干放入到揹包中體積之和爲j的時候,價值的和的最大。

因此咱們第一開始的狀態轉移的方程爲:

 F[i]=max1≤cnt≤Ci{F[j-cnt*vi]+cnt*Wi}
將決策換到一個數軸上表示每個可能取值的點:以下圖(從書上偷的(逃~)

當咱們將j-1時獲得的取值是這樣的

這時,咱們會發現對於j和j-1來講,轉移以後所更新的內容並不快,由於兩種的狀況並無重疊的部分。

咱們如今考慮一下對於j和j-vi是什麼狀況

不難看出,這裏的狀態在更新時若是使用這樣的更新的方式,速率會很快。

可是對於中間的這些數來講,就是至關於將j按照除以Vi的狀態去分,對於每一組來講進行分別計算便可。

不難發現,這些分組的依據實現以後,咱們獲得的序列是單調的,這樣的話咱們就能夠考慮來使用單調隊列進行優化。

 

 

 

好了,今天就講到這了。

後續揹包推薦題目( 持續更新……):洛谷P1156(最優可將狀態降至一維)  ,洛谷P1417(揹包加排序的組合)  ,洛谷P5020 (經數學證實後,實質爲徹底揹包)

相關文章
相關標籤/搜索