变种 背包问题_【朝夕的ACM笔记】动态规划-背包问题
【朝夕的ACM筆記】目錄與索引
背包問題
一、0/1背包
1.1 問題描述
有 件物品和一個大小為 的背包,以及若干物品,每種物品只有一件,大小分別為 ,其價值分別為 。問題:將哪些物品裝入背包,可使得背包內的物品價值總和最大?輸入:
第一行:
接下來 行,每行兩個整數, 和 。
輸出:最大價值 .
1.2 解題思路
由于每種物品都只有一件,所以只存在兩個選擇:裝這個物品或者不裝。(也就是0/1背包的含義)
我們定義一個數組
用于儲存——當背包大小為 ,且選擇處理完第 個物品后(前 個物品選擇裝或不裝)的最大價值總和,那么最終的答案就是 。我們通過一個例子來推出
的遞推式。有3個物品,大小價值如下表,和一個大小為3的背包。
先考慮裝不裝A。如下表。
由于A的大小只有1,所以大小1~3的背包都可以裝下它,于是更新所有值為A的價值2。
接下來考慮B。如下表。
由于B的大小為2,所以大小為1的背包裝不下它,值不變。
大小為2的背包可裝下它,且值為5,比原來的2大,于是更新值為5。
大小為3的背包可裝下它,且裝完后剩余大小為1。
如果裝B,那么價值=B的價值+未裝B的、大小為1的背包的最大價值=5+2=7。
如果不裝B,那么價值就是原來的值=2。在二者里取max,就能得到最大值,也就是7。
最后考慮C。如下表。
C的大小為1,背包1可裝,值更大,更新為3。
背包2可裝,如果裝,那么價值=C的價值+未裝C的、大小為1的背包的最大價值=3+2=5。
如果不裝,值就是原來的5。二者取
即可。背包3可裝,如果裝,那么價值=C的價值+未裝C的、大小為2的背包的最大價值=5+3=8。
如果不裝,值就是原來的7。二者取
即可。所以最后輸出的值就是8。
小結論:
我們可以發現遞推公式:
。也就是處理第
件物品,背包大小為 時的最大價值,有兩種可能。①裝入第
件物品那么價值=該物品的價值+未裝入該物品的、大小足夠塞入該物品的背包的最大價值=
。②不裝入第
件物品那么價值就是處理
件物品時計算出來的值,不變。二者取最大值即可。
1.2.1 代碼:
#include <stdio.h> int max(int a,int b) {if(a>b) return a;else return b; } int bag[1000][1000];//儲存解的數組 int w[1000],val[1000];//每件物品的大小與價值 int main() {int n,m;scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)scanf("%d%d",&w[i],&val[i]);//讀入數據for(int i=1;i<=n;i++) //從第一件物品一直處理到第i件物品for(int j=1;j<=m;j++) //從大小為1的背包處理到大小為m的背包if(j>=w[i])//塞得進去這件物品的話bag[i][j]=max(bag[i-1][j],bag[i-1][j-w[i]]+val[i]);//就判斷是塞還是不塞比較好else//塞不進去就是原來的值bag[i][j]=bag[i-1][j];printf("%dn",bag[n][m]);//全部處理完后,輸出最后的答案return 0; }1.3 算法的空間優化
1.3.1 為什么要優化?
通過1.3的代碼可以很容易的看出,當物品有
件,背包大小為 時,我們就需要開一個 大小的數組了,要是處理的數據再稍微大一點,就會導致數組開的過大,然后內存溢出。所以為了解決更大的數據,空間的優化是必要的。
1.3.2 怎么優化?
我們可以發現,當我們處理第
件物品時,要用到的只是處理第 件物品時的數據,再之前的就用不到了。所以,我們可以每次處理物品時,就把數據進行一次覆蓋,這樣二維數組就能降維到一維了。
但需要注意!降維到一維數組后,我們處理第
件物品時,就不能從大小為1的背包一直順序處理到大小為 的背包了。為什么?因為這樣的話,處理 件物品時的數據會在本次處理中被覆蓋,而處理更大的背包時是會用到小背包的數據的。這樣就會出現——已經塞了第
件物品了,處理時就當做沒塞,就會WA了。所以我們這里進行倒序處理,因為處理小背包用不到大背包的數據,覆蓋了也就覆蓋了,無所謂。
1.3.3 優化代碼
#include <stdio.h> int max(int a,int b) {if(a>b) return a;else return b; } int bag[1000];//降維到一維的數組 int w[1000],val[1000];//每件物品的大小與價值 int main() {int n,m;scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)scanf("%d%d",&w[i],&val[i]);//讀入數據for(int i=1;i<=n;i++) //從第一件物品一直處理到第i件物品for(int j=m;j>0;j--) //倒序處理if(j>=w[i])//塞得進去這件物品的話bag[j]=max(bag[j],bag[j-w[i]]+val[i]);//就判斷,處理后覆蓋上一次處理的數據else//塞不進去就是原來的值bag[j]=bag[j];//這行代碼完全沒有意義,但是為了方便對比,留在這里printf("%dn",bag[m]);//全部處理完后,輸出最后的答案return 0; }優化完后,空間復雜度就從
縮小到了 ,大大節省了內存,也就可以處理更大的數據了。1.4 0/1背包的常見例題及變種
1.4.1 采藥問題 洛谷 P1048
有 株藥草,每株藥草有其對應的價值及采摘該藥草要花費的時間,在 時間內,求采摘的藥草的最大價值。這其實就是換了個題干的0/1背包模板題,上面的代碼直接復制粘貼就可AC。
1.4.2 購物問題 洛谷 P1060
你有 元錢,有 種物品,每種一件,每件物品都有對應的價格,并且你對每件物品都賦予了一個重要度(從1~5),求購買的物品的最高性價比和。注:性價比=物品價格*重要度。
依然是換了個題干的0/1背包模板,只要把價值的計算變成物品價格*重要度即可
(
)。1.4.3 裝箱問題 洛谷 P1049
有一個容量為 的箱子,和 個物品,每個物品的體積為整數,裝任意個物品,問剩余的最小體積是多少?本題中,物品的價值與消費相同,也就是
,代換進原來的代碼即可。注意最后輸出的是剩余的最小體積,故應當輸出
。1.4.4 點菜問題 洛谷 P1164
你有 元錢,有 種菜,每種只有一份,且有對應的價格。要求你把所有的錢都花光,問有多少種不同的點菜方案?如果只有0元錢,那只有一種點菜方式——啥都不點。
由于問的是方案數而非價值數,所以遞推公式里的
要變成 。即接下來的遞推公式為
。即花
元的點菜方式 = 花( -某道菜價格)元錢的點菜方式的和。1.4.5 精衛填海 洛谷 P1510
1.4.6 烹飪方案 洛谷 P1417
有 件食材,每件食材有 三個屬性,在 時刻完成一件食材,將得到 的美味度,每件食材所需時間為 。求在 時間內能得到的最大美味度。
這道題很容易誤解為是裸的0/1背包,但實際上不是。
0/1背包的做法里,不管先裝哪件物品其得到的價值都是不會變的,都是這件物品的價值
。但這道題里,先做食材A還是先做食材B,其得到的美味度是不同的,因為美味度與完成時刻
(可理解為背包剩余容量)是有關聯的。此時如果再順序從第1件物品處理到第 件物品,所得到的美味度就不一定是最優解。所以我們要判斷先做哪一件食材比較好,然后進行排序,最后才是0/1背包的過程。
關于做食材的順序推斷,可以拿相鄰兩件物品進行比較。
假設已經消耗時間
,接下來是第 件物品和第 件物品:先做
再做 獲得的價值:先做
再做 獲得的價值:前者大于后者的條件是,
根據這個條件排序即可。還要注意,最后的答案并不是
,因為美味度的計算存在減法,到某時刻之后可能所有食材做了都是負美食度的,所以要遍歷 到 。參考代碼:
#include <bits/stdc++.h> #include <algorithm> #define LL long long//注意數據量較大,不用LL會爆一個測試點拿95分 using namespace std; struct node{LL a;LL b;LL c;}food[100005]; long long bag[100005]; bool cmp(node a,node b) {return a.c*b.b<a.b*b.c; } int main() {int t,n;while(scanf("%d%d",&t,&n)!=EOF){for(int i=0;i<n;i++)scanf("%lld",&food[i].a);for(int i=0;i<n;i++)scanf("%lld",&food[i].b);for(int i=0;i<n;i++)scanf("%lld",&food[i].c);sort(food,food+n,cmp);LL ans=0;for(int i=0;i<n;i++)for(int j=t;j>=food[i].c;j--)bag[j]=max(bag[j],bag[j-food[i].c]+food[i].a-j*food[i].b);for(int i=0;i<=t;i++)ans=max(ans,bag[i]);printf("%lldn",ans);}return 0; }1.4.7 集合問題 洛谷 P1466
1.4.8 最大約數和 洛谷 P1734
1.4.9 Checkout Assistant CF19B
1.5 關于背包問題與貪心問題的小提醒
如果給的物品可以選擇只拿一部分——比如說一件物品大小為3,價值為3,我可以選擇拿走這件物品的三分之一,其大小為1,價值為1——像這樣的題目,實際上并不是背包問題,而是簡單的貪心問題。
此時只需要算出每種物品的性價比——即價值/大小,然后從性價比最高的開始裝就好了。
二、完全背包
2.1 問題描述
將0/1背包問題中的物品,全部改為可以取無數件,則在這種情況下,求能獲得的最大價值2.2 解題思路
解題思路與0/1背包完全相同,只是此時因為每件物品都變成了可取無數件,所以需要考慮的不再是取or不取,而是不取or取1件or取2件……直到徹底塞不下為止。
也可以理解為,完全背包問題就是0/1背包問題的變種,只是此時有無數件價值和大小都相同的物品罷了。
在這里,我們不妨回憶一下,在將0/1背包由二維降維到一維時,為了防止——在處理第i件物品時,用到已經取了第i件物品的數據的情況,我們選擇了倒序處理第i件物品。
那么在完全背包中,我們只需要將這一步驟改為正序處理即可,那么在處理大背包時引用到的小背包的數據,就是已經考慮了裝X件該物品的數據了。
如果覺得難以理解,請重新返回到0/1背包降維那一步,好好地理解倒序處理的意義是什么。
2.3 代碼實現
#include <stdio.h> int max(int a,int b) {return a>b?a:b; } int bag[1000];//降維到一維的數組 int w[1000],val[1000];//每件物品的大小與價值 int main() {int n,m;scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)scanf("%d%d",&w[i],&val[i]);//讀入數據for(int i=1;i<=n;i++) //從第一件物品一直處理到第i件物品for(int j=w[i];j<=m;j++) //改為正序處理,由于僅裝得下時數據才會覆蓋更新,所以從w[i]開始循環bag[j]=max(bag[j],bag[j-w[i]]+val[i]);//此時必定裝得下,于是直接處理,覆蓋上一次處理的數據printf("%dn",bag[m]);//全部處理完后,輸出最后的答案return 0; }需要注意這里的代碼有了一定的更新優化,內循環直接從
開始,則必定是裝得下第 件物品的,這樣就可以減少一個判斷是否裝得下的步驟,0/1背包問題內的代碼也可以進行同樣的優化。2.4 剪枝優化
在完全背包問題中,可以采取一個方法來對算法進行優化。
由于每件物品都可以取無數件——所以當存在兩種物品,其中一種物品的價值比另一種低,大小還比另一種大時,我們在循環處理中就可以忽視掉這件物品。
即存在
時,就可以將 物品從序列中刪去。這是結合了貪心算法的剪枝。但這種剪枝方法對于最差情況是無法優化的,即所有物品的價值、大小都是成梯度的情況,時間復雜度仍然是
。僅對于隨機數據時可以做到較好的優化。另外需要注意,0/1背包不可使用該優化,因為0/1背包的物品是有限的,若是刪去多種物品可能導致背包空空。
2.5 完全背包相關習題及變種
2.5.1 瘋狂的采藥 洛谷 P1616
題干與采藥問題基本相同,只是每種藥草都可以無限摘取,數據本題就是個完全背包的裸題,沒有任何變動,唯一需要注意的是數據較大,開二維數組必定爆內存,所以需要降維。
2.5.2 剪緞帶問題 CF189A(在洛谷或CF上都可找到)
有一長度為 的緞帶,要求將其剪成長度為 的小緞帶,且緞帶數量盡可能多,問最多多少條。同一長度的緞帶可剪出無數條,所以本題實際上是完全背包。但需要注意的是,緞帶必須全部剪成所給的三種長度,也就是說——“背包”必須完全裝滿,不能有剩余的空間——否則剩下來的緞帶,就相當于剪出了一條新長度的緞帶。
這種題型屬于“恰好”背包問題,要求就是必須裝滿。
所以這道題在DP循環時,更新的數據必須是
的倍數或其倍數相加。處理方法如下:
memset(bag,-1,sizeof(bag)); bag[0]=0; for(int i=0;i<3;i++)for(int j=len[i];j<=n;j++)if(dp[j-bag[i]]!=-1) bag[j]=max(bag[j],bag[j-len[i]]+1);最開始先將所有大小的背包都初始化為-1。(代表所有大小背包不可更新)
然后將大小為0的背包賦值為0。接下來DP循環中,當且僅當所有剪下的緞帶都是所給長度時,才進行更新。
2.5.3 買干草 洛谷 P2918
2.5.4 數字和數量計算 HDU 1028
完全背包型的點菜問題。可以參考0/1背包點菜問題例題的思路。
2.5.5 質數和分解 洛谷 P2563
三、多維費用背包
3.1 問題描述
此時背包的花費是多維情況,比如說每件物品有一個大小和一個重量,而背包所能裝的大小和重量都有限。其他與完全、0/1背包相同。3.2 解題思路
費用變成多維,那么狀態也變成多維即可。
假設有二維費用,那么就設
來存儲在背包所裝物品大小不超過v,重量不超過w時的最大價值。只需要在循環求解時,多加上一層循環即可,兩層循環一層來考慮
,一層來考慮 。3.3 代碼實現
(這里的代碼以0/1二維背包為例,完全背包或是更多維費用參考上述進行改造即可)
#include <stdio.h> #include <stdlib.h> #include <string.h> int vi[50],wi[50],k[50]; int bag[400][400]; int max(int a,int b) {return a>b?a:b; } int main() {int v,w,n;while(scanf("%d%d%d",&v,&w,&n)!=EOF)//最大可裝體積,最大可裝重量,物品件數{memset(bag,0,sizeof(bag));for(int i=1;i<=n;i++)scanf("%d%d%d",&vi[i],&wi[i],&k[i]);for(int i=1;i<=n;i++)for(int j=w;j>=wi[i];j--)//一層循環考慮重量for(int l=v;l>=vi[i];l--)//一層循環考慮體積bag[j][l]=max(bag[j][l],bag[j-wi[i]][l-vi[i]]+k[i]);printf("%dn",bag[w][v]);}return 0; }3.4 多維費用的例題
3.4.1 NASA的食物計劃 洛谷 P1507
這題就是裸的二維費用0/1背包,上述代碼直接復制就可AC。
3.4.2 限制件數的背包問題 原創
有一個大小為 的背包,以及若干件物品。每件物品都有對應的價值與大小。背包最多只允許裝 件物品,且所裝物品總大小不能超過背包容量。
問所裝物品的最大價值是多少?
這題看上去像是一個裸的0/1背包,只是加上了一個“最多允許裝
件物品”的限制,但是換個角度上來看,這題就是一道標準的二維費用0/1背包。每件物品多了一種花費“件數”,且每件物品該花費的值都是1。最多允許的花費為
。設
表示所裝物品不超過 件,且總體積不大于 時的最大價值。然后老套路寫就可以了。3.4.3 找女朋友問題 洛谷 P1509
四、多重背包
4.1 問題描述
在0/1背包的基礎上加上一個條件:此時每種物品不再只有一件,而是最多有 件可用。4.2 解題思路
其實就是在0/1背包的代碼基礎上再多加入一層循環:每件物品都從裝1件到裝
件進行測試,如果當前背包能裝得下,就更新當前背包的價值。需要注意,處理時依然是倒序處理,正序處理的話就變成完全背包而不符題意了。
還需要注意,在循環更新價值時,物品的價值要記得乘上數量,因為價值只是每一件物品的價值。
PS:多重背包還可以運用單調隊列進行進一步優化,其最終的時間復雜度可以縮減到
。4.3 代碼實現
#include <cstdio> #include <algorithm> //使用c++的標準庫,內置max函數 int bag[1000];//已經降維為一維的背包 int w[1000],val[1000],num[1000];//每件物品的大小與價值、最多能裝的數量 int main() {int n,m;scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)scanf("%d%d%d",&w[i],&val[i],&num[i]);//讀入數據for(int i=1;i<=n;i++) //從第一件物品一直處理到第i件物品for(int j=m;j>=w[i];j--) //倒序處理for(int l=1;l<=num[i];l++)//多了一層循環if(j>=l*w[i])//裝得進去l件物品的話bag[j]=max(bag[j],bag[j-w[i]*l]+val[i]*l);//就判斷是塞還是不塞比較好,注意乘lprintf("%dn",bag[m]);//全部處理完后,輸出最后的答案return 0; }4.4 多重背包的例題
4.4.1 選課時間 HDU 2079
一道多重背包的模板題,不過差別在于求的不是最大價值而是方法數,具體解法可以參考洛谷的P1164。
4.4.2 買米問題 HDU 2191
4.4.3 擺花 洛谷 P1077
五、分組背包
5.1 問題描述
在分組背包問題中, 件物品被分為了 組,其中每組內的物品最多只能取一件,求所能拿的最大價值。5.2 解題思路
我們可以建立一個二維數組,其中一維表示組別,另外一維則表示該物品為其所在組中的第幾件。也可以通過其他的方案,將同一組別的物品放在一起。
其他的處理方法,基本與0/1背包沒有較大的差別。
具體看下方代碼。
5.3 代碼實現
#include <stdio.h> #include <algorithm> #include <string.h> using namespace std; struct node{int w;int val;int t;}art[1005]; int num[105]; int dp[1005]; int cmp(node a,node b) {return a.t<b.t; } int main() {int n,m;while(scanf("%d%d",&m,&n)!=EOF)//n件物品,背包大小為m{int k=0;for(int i=0;i<n;i++){scanf("%d%d%d",&art[i].w,&art[i].val,&art[i].t);//物品的大小、價值、組別num[art[i].t]++;//該組別所含物品數增多k=max(k,art[i].t);//判定有多少個組}sort(art,art+n,cmp);//按照組別進行排序,同組的排在一起int p=0,q;//用于順序處理的指針for(int i=1;i<=k;i++)//k個組{for(int j=m;j>=0;j--)//01背包的倒序處理方案{q=p;for(int l=1;l<=num[i];l++)//每個組有num[i]件物品{if(j>=art[q].w)dp[j]=max(dp[j],dp[j-art[q].w]+art[q].val);//這種處理方案能保證一組里只取一件q++;}if(j==0) p=q;}}printf("%dn",dp[m]);} }這里以洛谷的P1757為例。
5.4 分組背包的例題
5.4.1 排兵布陣 洛谷 P5322
一道分組背包的變式題,稍有難度。
本質上就是
座城堡 個組別,每個組別 件物品,當然,這里需要預處理,將每個組別內的物品價值進行處理計算使其符合預期,然后就是分組背包的處理方案了。總結
以上是生活随笔為你收集整理的变种 背包问题_【朝夕的ACM笔记】动态规划-背包问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ws配置 zuul_SpringClou
- 下一篇: python不及格_10 个 Pytho