4 混合三種背包問題
4.1 問題
如果將前面1、2、3中的三種背包問題混合起來。也就是說,有的物品只可以取一次(01 背包),有的物品可以取無限次(完全背包),有的物品可以取的次數有一個上限(多重背包)。應該怎么求解呢?
4.2 01 背包與完全背包的混合
考慮到01 背包和完全背包中給出的偽代碼只有一處不同,故如果只有兩類物品:一類物品只能取一次,另一類物品可以取無限次,那么只需在對每個物品應用轉移方程時,根據物品的類別選用順序或逆序的循環即可,復雜度是O(V N)。偽代碼如下:
4.3 再加上多重背包
如果再加上最多可以取有限次的多重背包式的物品,那么利用單調隊列,也可以給出均攤O(V N) 的解法。
但如果不考慮單調隊列算法的話,用將每個這類物品分成O(logMi) 個01 背包的物品的方法也已經很優了。
最清晰的寫法是調用我們前面給出的三個過程。
在下一章的末尾我會用一到例題來實現這個三種背包混合的偽代碼
在最初寫出這三個過程的時候,可能完全沒有想到它們會在這里混合應用。我想這體現了編程中抽象的威力。如果你一直就是以這種“抽象出過程”的方式寫每一類背包問題的,也非常清楚它們的實現中細微的不同,那么在遇到混合三種背包問題的題目時,一定能很快想到上面簡潔的解法,對嗎?
4.4 小結
有人說,困難的題目都是由簡單的題目疊加而來的。這句話是否公理暫且存之不論,但它在本講中已經得到了充分的體現。本來01 背包、完全背包、多重背包都不是什么難題,但將它們簡單地組合起來以后就得到了這樣一道一定能嚇倒不少人的題目。但只要基礎扎實,領會三種基本背包問題的思想,就可以做到把困難的題目拆分成簡單的題目來解決。
5 二維費用的背包問題
5.1 問題
二維費用的背包問題是指:對于每件物品,具有兩種不同的費用,選擇這件物品必須同時付出這兩種費用。對于每種費用都有一個可付出的最大值(背包容量)。問怎樣選擇物品可以得到最大的價值。設第i 件物品所需的兩種費用分別為Ci 和Di。兩種費用可付出的最大值(也即兩種背包容量)分別為V 和U。物品的價值為Wi。
5.2 算法
費用加了一維,只需狀態也加一維即可。設F[i, v, u] 表示前i 件物品付出兩種費用分別為v 和u 時可獲得的最大價值。狀態轉移方程就是:
如前述優化空間復雜度的方法,可以只使用二維的數組:當每件物品只可以取一次時變量v 和u 采用逆序的循環,當物品有如完全背包問題時采用順序的循環,當物品有如多重背包問題時拆分物品。這里就不再給出偽代碼了,相信有了前面的基礎,讀者應該能夠自己實現出這個問題的程序。
本章末尾會有一道例題實現二維費用背包的這段偽代碼
5.3 物品總個數的限制
有時,“二維費用”的條件是以這樣一種隱含的方式給出的:最多只能取U 件物品。這事實上相當于每件物品多了一種“件數”的費用,每個物品的件數費用均為1,可以付出的最大件數費用為U。換句話說,設F[v,u] 表示付出費用v、最多選u 件時可得到的最大價值,則根據物品的類型(01、完全、多重)用不同的方法循環更新,最后在f[0 ... V, 0...U] 范圍內尋找答案。
5.4 二維整數域N2 上的背包問題
另一種看待二維背包問題的思路是:將它看待成N2 域上的背包問題。也就是說,背包的容量以及每件物品的費用都是一個二維向量。而常見的一維背包問題則是自然數域上的背包問題。所以說,一維背包的種種思想方法,往往可以應用于二位背包問題的求解中,因為只是數域擴大了而已。作為這種思想的練習,你可以嘗試將后文中提到的“子集和問題”擴展到二維,并試圖用同樣的復雜度解決。
5.5 小結
當發現由熟悉的動態規劃題目變形得來的題目時,在原來的狀態中加一維以滿足新的限制是一種比較通用的方法。希望你能從本講中初步體會到這種方法。
例題來了,這道例題結合了混合背包和二維費用背包。所以用這一道題把之前說的兩部分偽代碼做一下實現,那么我們接下來看一看題目以及題目的代碼實現部分
題目:
Problem Description
暗黑游戲中,裝備直接決定玩家人物的能力。可以使用Pg和Rune購買需要的物品。暗黑市場中的裝備,每件有不同的價格(Pg和Rune)、能力值、最大可購買件數。Kid作為暗黑戰網的一個玩家,當然希望使用盡可能少的Pg和Rune購買更優的裝備,以獲得最高的能力值。請你幫忙計算出現有支付能力下的最大可以獲得的能力值。
Input
輸入有多組數據,每組數據的首行三個整數N(0<N<=150),P(0<P<=100),R(0<R<=100),分別代表市場中物品的種類,Pg的支付能力和Rune的支付能力。第2至N+1行,每行四個整數,前兩個整數分別為購買此物品需要花費的Pg和Rune,第三個整數若為0,則說明此物品能購買無數件,若為其他數字,則此物品可購買的最多件數(0<=S<=32),第四個整數為該裝備的能力值。
Output
對于每組數據輸出一個整數,最大可獲得的能力值。
Sample Input
3 10 105 3 0 1104 3 4 1202 3 1 130
Sample Output
370
思路分析:我們可以看到題目中可以使用pg和rune兩個支付手段去支付裝備,那么這里級可以抽象成二維費用,能力值就是背包的價值。最大可購買件數就是物品的數量。題目中的描述當物品數為0時代表無限件,這里可以抽象成完全背包問題,當物品件數為1時則可以抽象成01背包問題。當物品件數為除了0和1之外的值時變可以抽象成多重背包問題。所以這道題是典型的二維費用背包+混合背包的問題。那么接下來我們看看代碼實現
#include <iostream>
using namespace std;
const int N=150;
const int P=5000;
int pg[N+1]; //pg支付費用
int rune[N+1]; //rune支付費用
int dp[P+1][P+1]; //dp[i][j]=k 表示花費pg為i rune為j時的最大價值為k
int s[N+1]; // 件數
int v[N+1]; //能力值
int max(int a,int b)
{
return a>b?a:b;
}
void ZeroOnePack(int dp[][P+1],int weight_1,int weight_2,int total_1,int total_2,int value)
{
int j,i;
for(i=total_1;i>=weight_1;i--)
{
for(j=total_2;j>=weight_2;j--)
{
dp[i][j]=max(dp[i][j],dp[i-weight_1][j-weight_2]+value);
}
}
}
void completePack(int dp[][P+1],int weight_1,int weight_2,int total_1,int total_2,int value)
{
int j,i;
for(i=weight_1;i<=total_1;i++)
{
for(j=weight_2;j<=total_2;j++)
{
dp[i][j]=max(dp[i][j],dp[i-weight_1][j-weight_2]+value);
}
}
}
void mutiPack(int dp[][P+1],int weight_1,int weight_2,int total_1,int total_2,int amount,int value)
{
if(amount*weight_1>total_1&&amount*weight_2>total_2)
{
completePack(dp,weight_1,weight_2,total_1,total_2,value);
}
else
{
int k=1;
while(amount-k>=0)
{
ZeroOnePack(dp,k*weight_1,k*weight_2,total_1,total_2,k*value);
amount-=k;
k*=2;
}
ZeroOnePack(dp,amount*weight_1,amount*weight_2,total_1,total_2,amount*value);
}
}
int main()
{
int n,P,R;
cin>>n>>P>>R;
int i;
int p,r,num,val;
for(i=0;i<n;i++)
{
cin>>p>>r>>num>>val;
pg[i]=p;
rune[i]=r;
s[i]=num;
v[i]=val;
}
for(i=0;i<n;i++)
{
if(s[i]==1)//01 背包
{
ZeroOnePack(dp,pg[i],rune[i],P,R,v[i]);
}
else if(s[i]==0)// 完全背包
{
completePack(dp,pg[i],rune[i],P,R,v[i]);
}
else //多重背包
{
mutiPack(dp,pg[i],rune[i],P,R,s[i],v[i]);
}
}
cout<<dp[P][R]<<endl;
return 0;
}
6 分組的背包問題
6.1 問題
有N 件物品和一個容量為V 的背包。第i 件物品的費用是Ci,價值是Wi。這些物品被劃分為K 組,每組中的物品互相沖突,最多選一件。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
6.2 算法
這個問題變成了每組物品有若干種策略:是選擇本組的某一件,還是一件都不選。也就是說設F[k, v] 表示前k 組物品花費費用v 能取得的最大權值,則有:
使用一維數組的偽代碼如下:
這里三層循環的順序保證了每一組內的物品最多只有一個會被添加到背包中。
另外,顯然可以對每組內的物品應用2.3中的優化。
6.3 小結
分組的背包問題將彼此互斥的若干物品稱為一個組,這建立了一個很好的模型。不少背包問題的變形都可以轉化為分組的背包問題(例如7),由分組的背包問題進一步可定義“泛化物品”的概念,十分有利于解題。
下面拿了2道和分組背包有關的題目來當例子實踐偽代碼
第一道: HDU 1712
ACboy needs your help
Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 7535 Accepted Submission(s): 4164
Problem Description
ACboy has N courses this term, and he plans to spend at most M days on study.Of course,the profit he will gain from different course depending on the days he spend on it.How to arrange the M days for the N courses to maximize the profit?
Input
The input consists of multiple data sets. A data set starts with a line containing two positive integers N and M, N is the number of courses, M is the days ACboy has.
Next follow a matrix A[i][j], (1<=i<=N<=100,1<=j<=M<=100).A[i][j] indicates if ACboy spend j days on ith course he will get profit of value A[i][j].
N = 0 and M = 0 ends the input.
Output
For each data set, your program should output a line which contains the number of the max profit ACboy will gain.
Sample Input
2 2
1 2
1 3
2 2
2 1
2 1
2 3
3 2 1
3 2 1
0 0
Sample Output
3
4
6
這道題就是很典型的可以抽象成分組背包的問題,每門課代表一種物品,所有的天數代表背包容量。然后每門課準備上的天數代表在這門課中所有可能的值。一門課就是一個組,并且每組內的物品互相沖突,比如你選擇了A課上2天就不能再選擇A課上1天。組內互斥,每組最多選一個。下面看看代碼實現;
#include<iostream>
using namespace std;
const int N=100;
int a[N+1][N+1]; //a[i][j]=k 代表第i門課上j天所獲得的價值為k
int dp[N+1];
//分組背包問題 每組最多拿一個 最典型分組背包
int main()
{
int n,m;
cin>>n>>m;
while(n!=0&&m!=0)
{
int i,j;
for(i=1;i<=n;i++)
{
for(j=1;j<=m;j++)
{
cin>>a[i][j];
}
}
for(i=1;i<=n;i++)//n門課
{
for(j=m;j>=1;j--)//所擁有的天數
{
for(int k=1;k<=m;k++)//第i門課 的課程時間 每門課有多個課程時間 存儲在二維數組a中
{
if(j-k>=0&&dp[j]<dp[j-k]+a[i][k])
{
dp[j]=dp[j-k]+a[i][k];
}
}
}
}
cout<<dp[m]<<endl;
memset(dp,0,sizeof(dp));
cin>>n>>m;
}
return 0;
}
第二道: HDU 3033
I love sneakers!
**Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 5718 Accepted Submission(s): 2344**
Problem Description
After months of hard working, Iserlohn finally wins awesome amount of scholarship. As a great zealot of sneakers, he decides to spend all his money on them in a sneaker store.
There are several brands of sneakers that Iserlohn wants to collect, such as Air Jordan and Nike Pro. And each brand has released various products. For the reason that Iserlohn is definitely a sneaker-mania, he desires to buy at least one product for each brand.
Although the fixed price of each product has been labeled, Iserlohn sets values for each of them based on his own tendency. With handsome but limited money, he wants to maximize the total value of the shoes he is going to buy. Obviously, as a collector, he won’t buy the same product twice.Now, Iserlohn needs you to help him find the best solution of his problem, which means to maximize the total value of the products he can buy.
Input
Input contains multiple test cases. Each test case begins with three integers 1<=N<=100 representing the total number of products, 1 <= M<= 10000 the money Iserlohn gets, and 1<=K<=10 representing the sneaker brands. The following N lines each represents a product with three positive integers 1<=a<=k, b and c, 0<=b,c<100000, meaning the brand’s number it belongs, the labeled price, and the value of this product. Process to End Of File.
Output
For each test case, print an integer which is the maximum total value of the sneakers that Iserlohn purchases. Print "Impossible" if Iserlohn's demands can’t be satisfied.
Sample Input
5 10000 31 4 62 5 73 4 991 55 772 44 66
Sample Output
255
這道題也是分組背包問題的代表,只不過和上一道有一點不一樣的是,每組內至少拿一件物品。我們來看,一個品牌代表一個組,所擁有的錢代表背包容量,每個品牌里面的鞋子產品代表這個組內的物品成員。
dp[i][j] 代表前i個品牌鞋子花費j元 所獲得的最大價值
這道題的初始化可能稍微有點變化,因為如果初始化dp為0則無法區分是買不全那幾款鞋子還是能買全但最大價值是0,因為我們在買不全的時候需要輸出特殊字符。而在之前的01背包問題等其他問題時,只需要計算最大價值即可,所以為0并不影響結果。
這道題的狀態轉移方程:
- dp[i][k]是不選擇當前鞋子;
- dp[i-1][k-v[j]]+w[j]是選擇當前鞋子,但是是第一次在本組中選,由于開始將該組dp賦為了-1,所以第一次取時,必須由上一組的結果推知,這樣才能保證得到全局最優解;
- dp[i][k-v[j]]+w[j]表示選擇當前鞋子,并且不是第一次在本組中取。
代碼實現:
#include <iostream>
using namespace std;
const int N=100;
const int Max_brand=10;
const int Max_money=10000;
int s[N+1];// 鞋子的品牌數組
int v[N+1];// 鞋子的價值數組
int c[N+1];// 鞋子的費用數組
int dp[Max_brand+1][Max_money+1]; //dp[i][j] 代表購買前i組品牌鞋子花費j元所得到的最大家價值 一個品牌的鞋子算一個分組
int max(int a,int b,int c)
{
return a>b?(a>c?a:c):(b>c?b:c);
}
//分組背包問題 每組至少取一個
/*
dp[i][k]是不選擇當前鞋子;
dp[i-1][k-v[j]]+w[j]是選擇當前鞋子,但是是第一次在本組中選,由于開始將該組dp賦為了-1,所以第一次取時,必須由上一組的結果推知,這樣才能保證得到全局最優解;
dp[i][k-v[j]]+w[j]表示選擇當前鞋子,并且不是第一次在本組中取。
*/
/*
這道題dp初始化為-1 因為如果初始化為0
則無法區分是買不全那幾款鞋子還是能買全但最大價值是0
因為我們在買不全的時候需要輸出特殊字符。
*/
int main()
{
int n,m,S;
while(scanf("%d%d%d",&n,&m,&S)!=EOF)
{
int i,j,si,vi,ci;
for(i=0;i<n;i++)
{
cin>>si>>ci>>vi;
s[i]=si;
c[i]=ci;
v[i]=vi;
}
for(i=0;i<=S;i++)
{
for(j=0;j<=m;j++)
{
if(i==0)
{
dp[i][j]=0;
}
else
{
dp[i][j]=-1;
}
}
}
for(i=1;i<=S;i++)
{
for(j=0;j<=n;j++)//遍歷所有鞋子
{
if(s[j]==i)//找到品牌為i的鞋子
{
for(int k=m;k>=c[j];k--)//第i組內選擇
{
dp[i][k]=max(dp[i][k],dp[i][k-c[j]]+v[j],dp[i-1][k-c[j]]+v[j]);
}
}
}
}
if(dp[S][m]<0)
{
cout<<"Impossible"<<endl;
}
else
{
cout<<dp[S][m]<<endl;
}
}
return 0;
}