背包問題(01背包,完全背包,多重背包(朴素算法&&二進制優化))


寫在前面:我是一只蒟蒻~~~

今天我們要講講動態規划中最最最最最簡單的背包問題



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代碼(如果你看懂了上面,代碼就不難理解了)
         

 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,如有錯誤,請指出,本人定改正。
    

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




2、了解完01背包,我們來看一看  


完全背包


老規矩,上題。  
題目(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這一題希望大家好好想一想,有點坑,但是並不是太難,大家加油吧!!!!!   




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

 
多重背包  


等我學會,再來更新~~~~~  
送給大家一個博客背包九講


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 (經數學證明后,實質為完全背包)

 


免責聲明!

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



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