1. 程式人生 > >背包問題(01背包,完全背包,多重背包(樸素算法&&二進制優化))

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

one reads aps 喜歡 背包 with 一個 可行性 會有

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

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



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進制以內的數組合得到。

這樣的話,我們就可以通過二進制的拆分來進行優化,我們把每個物品有的所有個數,分開,

核心代碼:

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;
}

好了,今天就講到這了。

技術分享圖片

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