多重背包也是一種基本的背包問題模型,其基本特點是:每種物品有一個固定的裝入次數上限。
多重背包問題的一般描述為:有N個物品,第i個物品的重量與價值分別為W[i]與P[i]且第i種物品最多有C[i] 件。背包容量為V,試問在每個物品不超過其上限的件數(物品必須保持完整)的情況下,如何讓背包裝入的物品具有更大的價值總和。
其一般解題思路為:
設f[i][j] 表示從編號1~i的物品中挑選任意數量的任意物品放入容量為j的背包中得到的最大價值,那么有 f[i][j]=max { f[i−1][j−k∗w[i]]+k∗v[i] ∣0≤k≤p[i]&k∗w[i]≤j }。
編寫代碼時,可以寫成如下的循環。
for ( i = 1; i <= N; i++)
for ( j = 1; j <= V; j++)
for (k = 0; k <= C[i] && k * W[i] <= j; k++)
{
f[i][j] = max( f[i][j], f[i - 1][j - k * W[i]] + k *P[i]);
}
求得的最優值為f[N][V]。
同樣多重背包也可以進行空間優化,將二維數組優化為一維數組,即
f[j]=max { f[j],f[j−k∗W[i]]+k∗P[i] } (0≤k && k∗W[i]≤j)
編寫代碼時,一般采用如下的循環。
for (i=1; i<=N; i++)
for (j=V; j>=0; j--)
for (k=0; k<=c[i] && k*W[i] <=j; k++)
f[j] = max( f[j], f[j - k * W[i]] + k *P[i]);
求得的最優值為f[V]。
從上面的程序代碼可以看出,多重背包的處理一般采用三重循環,時間復雜度較高。為了對時間進行優化,可以采用二進制優化法將多重背包轉變為0/1背包(采用二重循環)進行處理。
所謂二進制優化法就是將第 i 種c[i]件物品按二進制的方法分拆成s份“不同”的物品。例如,設第i件物品有20件,每件重量和價值分別為w和p,則可以分拆別5份“物品”如下:
第1份含有1件物品,重量為w,價值為p;
第2份含有2件物品,重量為2w,價值為2p;
第3份含有4件物品,重量為4w,價值為4p;
第4份含有8件物品,重量為8w,價值為8p;
第5份含有5件物品,重量為5w,價值為5p。
因為,1+2+4+8+5=20,則由這5份“物品”可組合成0~20件之間任意件數的物品。這5份“物品”在進行組合時,每份物品要么選取,要么不選取,正好就是對這5份物品進行0/1背包處理。由此,多重背包可以通過這種方式轉化為0/1背包進行處理。從而將三重循環優化為二重循環處理。
我們知道,k位二進制數可以表示0~2k-1之間的任意整數,k位二進制數從低位到高位,各位上的權值依次為1(20)、2(21)、4(22)、…、2k-1。因此,對於任意一個正整數x,則可以將其拆分為1+2+4+…+2k-1+y(其中y是二進制拆分剩余的部分,y<2k)。
通過拆分后,就可以將多重背包問題轉換為0/1背包問題求解了。
編寫代碼如下:
for (i=1; i<=N; i++)
{
int s =C[i];
for (j=1; j<=s; s-=j, j=j*2) // 二進制拆分
{
for (k =V; k >=0 && k>= j*W[i]; k--) // 0/1背包
{
f[k] = max(f[k], f[k - j*W[i]] + j *P[i]);
}
}
if (s > 0) // 拆分的剩余部分
{
for ( j =V; j >= s * W[i]; j--)
{
f[j] = max(f[j], f[j - s * W[i]] + s * P[i]);
}
}
}
【例1】購買糧食
問題描述
你准備自己采購一些糧食支援災區,現在假設你一共有資金n元,而市場有m種大米,每種大米都是袋裝產品,其重量和價格不等,並且只能整袋購買。
請問:你用有限的資金最多能采購多少公斤糧食呢?
輸入
輸入數據首先包含一個正整數C,表示有C組測試用例,每組測試用例的第一行是兩個整數n和m(1<=n<=100,1<=m<=100),分別表示經費的金額和大米的種類,然后是m行數據,每行包含3個數p,h和c(1<=p<=20,1<=h<=200,1<=c<=20),分別表示每袋的價格、每袋的重量以及對應種類大米的袋數。
輸出
對於每組測試數據,請輸出能夠購買大米的最多重量,你可以假設經費買不光所有的大米,並且經費你可以不用完。每個實例的輸出占一行。
輸入樣例
1
8 2
2 100 4
4 100 2
輸出樣例
400
(1)編程思路。
典型的多重背包問題。按上面介紹的方法,采用二維數組編寫源程序1;采用一維數組編寫源程序2;采用二進制優化的方法編寫源程序3。
(2)采用二維數組編寫的源程序1。
#include <stdio.h> #include <string.h> int max(int a,int b) { return a>b?a:b; } int main() { int p[105],h[105],c[105],f[105][105]; int t; scanf("%d",&t); while (t--) { int n,m; scanf("%d%d",&n,&m); int i,j,k; for (i=1;i<=m;i++) scanf("%d%d%d",&p[i],&h[i],&c[i]); memset(f,0,sizeof(f)); for (i=1;i<=m;i++) for (j=1; j<=n; j++) for (k=0; k<=c[i] && k*p[i] <=j; k++) f[i][j]=max(f[i][j],f[i-1][j-k*p[i]]+k*h[i]); printf("%d\n",f[m][n]); } return 0; }
(3)采用一維數組編寫的源程序2。
#include <stdio.h> #include <string.h> int max(int a,int b) { return a>b?a:b; } int main() { int p[105],h[105],c[105],f[105]; int t; scanf("%d",&t); while (t--) { int n,m; scanf("%d%d",&n,&m); int i,j,k; for (i=1;i<=m;i++) scanf("%d%d%d",&p[i],&h[i],&c[i]); memset(f,0,sizeof(f)); for (i=1;i<=m;i++) for (j=n;j>=0;j--) for (k=0; k<=c[i] && k*p[i] <=j; k++) f[j]=max(f[j],f[j-k*p[i]]+k*h[i]); printf("%d\n",f[n]); } return 0; }
(4)采用二進制優化的方法編寫的源程序3。
#include <stdio.h> #include <string.h> int max(int a,int b) { return a>b?a:b; } int main() { int p[105],h[105],c[105],f[105]; int t; scanf("%d",&t); while (t--) { int n,m; scanf("%d%d",&n,&m); int i,j,k; for (i=1;i<=m;i++) scanf("%d%d%d",&p[i],&h[i],&c[i]); memset(f,0,sizeof(f)); for (i=1; i<=m; i++) { int s =c[i]; for (j=1; j<=s; s-=j, j=j*2) // 二進制拆分 { for (k =n; k >=0 && k>= j*p[i]; k--) // 0/1背包 { f[k] = max(f[k], f[k - j*p[i]] + j *h[i]); } } if (s > 0) // 拆分的剩余部分 { for ( j =n; j >= s * p[i]; j--) { f[j] = max(f[j], f[j - s * p[i]] + s * h[i]); } } } printf("%d\n",f[n]); } return 0; }
【例2】擺花
問題描述
小明的花店新開張,為了吸引顧客,他想在花店的門口擺上一排花,共m盆。通過調查顧客的喜好,小明列出了顧客最喜歡的n種花,從1到n標號。為了在門口展出更多種花,規定第i種花不能超過 ai盆,擺花時同一種花放在一起,且不同種類的花需按標號的從小到大的順序依次擺列。
試編程計算,一共有多少種不同的擺花方案。
輸入
第一行包含兩個正整數n和m(0<n≤100,0<m≤100),中間用一個空格隔開。
第二行有n個整數,每兩個整數之間用一個空格隔開,依次表示 a1,a2,…,an。
輸出
一個整數,表示有多少種方案。注意:因為方案數可能很多,請輸出方案數對 10^6+7取模的結果。
輸入樣例
2 4
3 2
輸出樣例
2
(1)編程思路。
典型的多重背包問題。按前面的介紹進行處理即可。
采用二維數組,定義int f[105][105]={0}。設f[i][j]表示前i種花中擺放j盆花的方案數,初始值初f[0][0]=1(什么也不擺)外,其余全部為0。可以編寫如下的源程序1。
采用一維數組,int f[105]={0};設f[i]表示擺放i盆花的方案數。可以編寫如下的源程序2。
(2)使用二維數組編寫的源程序1。
#include <stdio.h> int main() { int f[105][105]={0},a[105]; int n,m; scanf("%d%d",&n,&m); int i,j,k; for (i=1; i<=n; i++) { scanf("%d",&a[i]); } f[0][0]=1; for (i=1; i<=n; i++) for (j=0; j<=m; j++) for (k=0; k<=(a[i]<j?a[i]:j); k++) f[i][j] = (f[i][j] + f[i-1][j-k])%1000007; printf("%d\n",f[n][m]); return 0; }
(3)使用一維數組編寫的源程序2。
#include <stdio.h> int main() { int f[105]={0},a[105]; int n,m; scanf("%d%d",&n,&m); int i,j,k; for (i=1; i<=n; i++) scanf("%d",&a[i]); f[0] = 1; for (i=1; i<=n; i++) for (j=m; j>=0; j--) for (k=1; k<=(a[i]<j?a[i]:j); k++) f[j] = (f[j] + f[j-k])%1000007; printf("%d\n",f[m]); return 0; }
將上面的源程序1和2提交給洛谷題庫P1077 [NOIP2012 普及組] 擺花(https://www.luogu.com.cn/problem/P1077),測評結果均為“Accepted”。
【例3】收集櫻花
問題描述
有k棵櫻花樹,在第i棵樹下最多能收集到 si朵櫻花(收集了0朵櫻花也算收集了櫻花)。
你有多少種方案能夠收集到恰好n朵櫻花呢?
輸入
第一行兩個正整數 n,k,表示要收集n朵櫻花,有k棵櫻花樹。
接下來一行k個正整數 s1,s2,…,sk,其中 si表示最多在第i棵櫻花樹下收集到si朵櫻花。
輸出
一行一個整數,表示恰好收集到n朵櫻花的方案數。
由於答案可能太大,請輸出答案對 10086001 取模后的值。
特殊地,如果收集不到n朵櫻花,請輸出一個字符串 impossible。
輸入樣例#1
3 4
1 1 1 1
輸出樣例 #1
5
輸入樣例 #2
10 9
9 6 8 7 9 6 5 4 3
輸出樣例#2
68345
輸入樣例 #3
10 5
2 2 2 2 1
輸出樣例 #3
Impossible
(1)編程思路1。
定義二維數組int f[5001][5001];其中f[i][j]表示前i顆櫻花樹下共收集到j朵櫻花的方案數。顯然,有f[i][0]=1(1≤i≤n,每顆樹下可不收集櫻花,收集了0朵櫻花也算收集了櫻花,方案數為1),還有f[1][j]=1(1≤j≤s[1],第1顆櫻花樹下可分別收集1~s[1]朵櫻花)。
轉移方程為: f[i][j]=f[i][j]+f[i-1][j-k] (其中k值為第i可櫻花樹收集櫻花的朵數)。
(2)源程序1。
#include <stdio.h> #include <string.h> int f[5001][5001]={0}; int main() { int v,n; scanf("%d%d",&v,&n); int s[5001]; int i,j,k; int tot=0; for (i=1;i<=n;i++) { scanf("%d",&s[i]); tot+=s[i]; } if (tot<v) { printf("impossible\n"); return 0; } for (i=1;i<=n;i++) f[i][0]=1; for (i=1;i<=s[1];i++) f[1][i]=1; for (i=2; i<=n; i++) { for (j=1; j<=v; j++) { for (k=0; k<=s[i] && k<=j; k++) { f[i][j]=(f[i][j]+f[i-1][j-k])%10086001; } } } int ans=0; for (i=1; i<=n; i++) ans = (ans+f[i][v])%10086001; printf("%d\n",ans); return 0; }
將上面的源程序1提交給洛谷題庫P6394 櫻花,還有你(https://www.luogu.com.cn/problem/P6394),測評結果為“Unaccepted”,其中測試點#17、測試點#19和測試點#20,顯示“TLE”,測試點#18顯示“MLE”。
(3)編程思路2。
由於數據量較大,因此采用二維數組存儲,會存在超內存限制的情況,因此采用一維數組進行處理。
(4)源程序2。
#include <stdio.h> #include <string.h> int f[5001]={0}; int main() { int v,n; scanf("%d%d",&v,&n); int s[5001]; int i,j,k; int tot=0; for (i=1;i<=n;i++) { scanf("%d",&s[i]); tot+=s[i]; } if (tot<v) { printf("impossible\n"); return 0; } f[0]=1; int ans=0; for (i=1;i<=n;i++) // 前i棵樹 { for (j=v;j>=0;j--) for (k=1;k<=s[i] && k<=j;k++) f[j]=(f[j]+f[j-k])%10086001; ans=(ans+f[v])%10086001; } printf("%d\n",ans); return 0; }
將上面的源程序2提交給洛谷題庫P6394 櫻花,還有你(https://www.luogu.com.cn/problem/P6394),測評結果仍為“Unaccepted”,其中測試點#17、測試點#18、測試點#19和測試點#20,均顯示“TLE”。
(5)編程思路3。
源程序2中的多重背包采用三重循環實現,由於數據量較大,循環處理太耗時,因此會超時。觀察第三層循環,每次的f[j]都是加上一個區間,所以可以直接用一個前綴和來計算,這樣第三層循環就可以省掉了。
(6)源程序3。
#include <stdio.h> int main() { int v,n; scanf("%d%d",&v,&n); int f[5001]={0}; int s[5001]; int sum[5001]={0}; // 保存前綴和 int i,j; int tot=0; for (i=1;i<=n;i++) { scanf("%d",&s[i]); tot+=s[i]; } if (tot<v) { printf("impossible\n"); return 0; } sum[0]=f[0]=1; int ans=0; for (i=1;i<=n;i++) // 前i棵樹 { for (j=1;j<=v;j++) // 更新前綴和 sum[j]=(sum[j-1]+f[j])% 10086001; for (j=v;j>=1;j--) { int k=s[i]<j?s[i]:j; if (k==j) f[j]=(f[j]+sum[j-1])% 10086001; else f[j]=(f[j]+sum[j-1]-sum[j-k-1]+10086001)% 10086001; //利用前綴和 } ans=(ans+f[v])%10086001; } printf("%d\n",ans); return 0; }
將上面的源程序3提交給洛谷題庫P6394 櫻花,還有你(https://www.luogu.com.cn/problem/P6394),測評結果為“Accepted”。
【例4】砝碼稱重
問題描述
設有1g、2g、3g、5g、10g、20g的砝碼各若干枚(其總重≤1000),計算用這些砝碼能稱出的不同重量的個數N,但不包括一個砝碼也不用的情況。
輸入
輸入方式:a1 , a2 ,a3 , a4 , a5 ,a6(表示1g砝碼有a1個,2g砝碼有a2個,…,20g砝碼有a6個)。
輸出
輸出方式:Total=N
輸入樣例
1 1 0 0 0 0
輸出樣例
Total=3
(1)編程思路。
設f[i]表示重量i是否可以稱出。初始時,f[0]=1,其余全部為0。輸入6種砝碼的個數w[i],計算出所有砝碼全部使用時,可稱出的總重量tot,這也是背包的容量。
將6種砝碼按多重背包的處理方法加入背包,若重量j-w[i]*k可稱出,即f[j-w[i]*k]==1,則加上k個重w[i]的砝碼后,重量j也可以稱出,置f[j]=1。
多重背包處理完后,f[1]~f[tot]元素值為1的個數就是可稱出的不同重量的個數。
(2)源程序。
#include <stdio.h> int main() { int f[1001]={0}; int w[7]={0,1,2,3,5,10,20}; int a[7]; int i,j,k; int tot=0; // 總重量 for (i=1;i<=6;i++) { scanf("%d",&a[i]); tot+=w[i]*a[i]; } f[0]=1; for (i=1;i<=6;i++) { for (j=tot;j>=0;j--) for (k=0;k<=a[i];k++) { if (j-w[i]*k>=0 && f[j-w[i]*k]!=0) f[j]=1; } } int ans=0; for (i=1;i<=tot;i++) if (f[i]!=0) ans++; printf("Total=%d\n",ans); return 0; }
將上面的源程序提交給洛谷題庫P2347 [NOIP1996 提高組] 砝碼稱重(https://www.luogu.com.cn/problem/P2347),測評結果為“Accepted”。
【例5】買表
問題描述
Jimmy 到 Symbol 的手表店買手表,Jimmy 只帶了n種錢幣,第i種錢幣的面額為 ki元,張數為 ai張。Symbol 的店里一共有m 塊手表,第i 塊手表的價格為 ti元。
Symbol 的手表店不能找零,所以 Jimmy 只能在湊出恰好的錢數時才能購買一塊手表。現在對於店里的每塊手表,Jimmy 想知道他能不能湊出恰好的錢數進行購買。
輸入
第一行兩個空格分隔的整數 n(1≤n≤200)和 m(1≤m≤100000) 表示錢幣數與手表數。
接下來 n 行每行兩個空格分隔的整數 ki(1≤ki≤500000)和 ai(1≤ai≤1000)表示錢幣的面額和張數。
第 n+2行,共 m個用空格分隔的整數 ti(0≤ti≤500000),表示每塊手表的價格。
輸出
一共m 行,對於第i 行,如果能湊出恰好的錢數購買第i 塊手表則輸出 Yes 否則輸出 No,注意只有首字母大寫。
輸入樣例
3 5
1 2
5 1
6 3
3 19 21 1 7
輸出樣例
No
Yes
No
Yes
Yes
(1)編程思路。
設f[i]表示錢數i是否可以用n種錢幣湊出。初始時,f[0]=1,其余全部為0。由於每種錢幣的張數較多(最多可達1000張),因此按照前面介紹的二進制數優化方法,將多重背包變化成0/1背包進行處理。
(2)源程序。
#include <stdio.h> #include <string.h> int main() { int f[500005]={0},w[2005]; int n,m; scanf("%d%d",&n,&m); int i,j; int cnt=0; for (i=1; i<=n; i++) { int k,a; scanf("%d%d",&k,&a); for (j=1; j<=a; j*=2) // 多重背包轉成0/1背包 { w[++cnt]=j*k; a-=j; } if (a>0) { w[++cnt]=k*a; } } f[0]=1; for (i=1; i<=cnt; i++) // 01背包的求解 for(j=500000; j>=w[i]; j--) if (f[j-w[i]]) f[j]=1; for (i=1;i<=m;i++) { int t; scanf("%d",&t); if (f[t]) printf("Yes\n"); else printf("No\n"); } return 0; }
將上面的源程序提交給洛谷題庫P6567 [NOI Online #3 入門組] 買表(https://www.luogu.com.cn/problem/P1776),測評結果為“Accepted”。
【例6】英雄聯盟
問題描述
正在上大學的小皮球熱愛英雄聯盟這款游戲,而且打的很菜,被網友們戲稱為「小學生」。
現在,小皮球終於受不了網友們的嘲諷,決定變強了,他變強的方法就是:買皮膚!
小皮球只會玩N個(5≤N≤150)英雄,因此,他也只准備給這N個英雄買皮膚,並且決定,以后只玩有皮膚的英雄。
這N個英雄中,第i個英雄有Ki 款皮膚,價格是每款 Ci個Q 幣(同一個英雄的皮膚價格相同)。
為了讓自己看起來高大上一些,小皮球決定給同學們展示一下自己的皮膚,展示的思路是這樣的:對於有皮膚的每一個英雄,隨便選一個皮膚給同學看。
比如,小皮球共有 5 個英雄,這 5 個英雄分別有0,0,3,2,4 款皮膚,那么,小皮球就有3×2×4=24 種展示的策略。
現在,小皮球希望自己的展示策略能夠至少達到M (M≤1017)種,請問,小皮球至少要花多少錢呢?
輸入
第一行,兩個整數N,M。
第二行,N個整數,表示每個英雄的皮膚數量Ki。
第三行,N 個整數,表示每個英雄皮膚的價格Ci 。
輸出
一個整數,表示小皮球達到目標最少的花費。
輸入樣例
3 24
4 4 4
2 2 2
輸出樣例
18
(1)編程思路。
展示方案達到M種作為背包的容量,每一個英雄都有一個皮膚數量、一個購買的Q幣數(花費),將每個英雄的皮膚看成物品,就是有限物品數量的多重背包問題。
設f[j]表示花費j個Q幣購買英雄皮膚可得到的最大展示方案數量,則狀態轉移方程為:
f[j]=max(f[j−p∗c[i]]∗p,f[j]),其中 p是當前英雄所選的皮膚數量,c[i]該皮膚的購買價格。
多重背包處理完后,用變量i從0~tot搜索f[i]的判斷,第1次遇到的 f[i]>=m的i值就是所求的最小花費。
(2)源程序。
#include <stdio.h> long long max(long long a,long long b) { return a>b?a:b; } long long f[250000]={0}; int main() { int n; long long m; scanf("%d%lld",&n,&m); int i,j,p; int k[155],c[155]; for (i=1;i<=n;i++) scanf("%d",&k[i]); int tot=0; // 花的總錢數 for (i=1;i<=n;i++) { scanf("%d",&c[i]); tot+=c[i]*k[i]; } f[0]=1; for (i=1;i<=n;i++) { for (j=tot;j>=0;j--) for (p=0;p<=k[i];p++) { if (j-c[i]*p>=0) f[j]=max(f[j],1L*f[j-c[i]*p]*p); } } for (i=0;i<=tot;i++) if (f[i]>=m) { printf("%d\n",i); break; } return 0; }
將上面的源程序提交給洛谷題庫P5365 [SNOI2017]英雄聯盟(https://www.luogu.com.cn/problem/P5365),測評結果為“Accepted”。
【例7】寶物篩選
問題描述
小F找到了王室的寶物庫,里面堆滿了無數價值連城的寶物。但是這里的寶物實在是太多了,小F的采集車似乎裝不下那么多寶物。看來小F只能含淚舍棄其中的一部分寶物了。
小F對庫里的寶物進行了整理,他發現每樣寶物都有一件或者多件。他粗略估算了下每樣寶物的價值,之后開始了寶物篩選工作:小F有一個最大載重為W的采集車,寶物庫里總共有n種寶物,每種寶物的價值為vi,重量為wi,每種寶物有mi件。小F希望在采集車不超載的前提下,選擇一些寶物裝進采集車,使得它們的價值和最大。
輸入
第一行為一個整數n(1≤n≤100)和W(0≤W≤40000),分別表示寶物種數和采集車的最大載重。
接下來n行每行三個整數vi,wi,mi。n≤∑mi ≤100000。
輸出
輸出僅一個整數,表示在采集車不超載的情況下收集的寶物的最大價值。
輸入樣例
4 20
3 9 3
5 9 1
9 4 2
8 1 3
輸出樣例
47
(1)編程思路1。
典型的多重背包問題,按前面介紹的采用一維數組用三重循環編寫源程序1。
(2)采用一維數組用三重循環編寫的源程序1。
#include <stdio.h> #include <string.h> int max(int a,int b) { return a>b?a:b; } int main() { int n,limw; scanf("%d%d",&n,&limw); int i,j,k; int f[40005]; memset(f,0,sizeof(f)); for (i=1;i<=n;i++) { int v,w,m; scanf("%d%d%d",&v,&w,&m); for (j=limw;j>=0;j--) for (k=0; k<=m && k*w<=j; k++) f[j]=max(f[j],f[j-k*w]+k*v); } printf("%d\n",f[limw]); return 0; }
將上面的源程序提交給洛谷題庫P1776 寶物篩選(https://www.luogu.com.cn/problem/P1776),測評結果為“Unaccepted”,其中測試點#4~測試點#10,均顯示“TLE”。
(3)編程思路2。
采用一維數組用三重循環編寫的源程序1顯示超時了,為了進行時間優化,采用二進制拆分優化法將多重背包轉換為0/1背包進行處理,編寫下面的源程序2。
(4)采用二進制拆分優化法編寫的源程序2。
#include <stdio.h> #include <string.h> int main() { int n,limw; scanf("%d%d",&n,&limw); int v[100005],w[100005]; int i,j; int cnt=0; for (i=1;i<=n;i++) { int a,b,c; scanf("%d%d%d",&a,&b,&c); for (j=1;j<=c;j<<=1) { v[++cnt]=j*a; w[cnt]=j*b; c-=j; } if (c) { v[++cnt]=a*c; w[cnt]=b*c; } } int f[40005]; memset(f,0,sizeof(f)); for (i=1;i<=cnt;i++) // 轉換為cnt種寶物 for (j=limw;j>=w[i];j--) if (f[j]<f[j-w[i]]+v[i]) f[j]=f[j-w[i]]+v[i]; printf("%d\n",f[limw]); return 0; }
將上面的源程序提交給洛谷題庫P1776 寶物篩選(https://www.luogu.com.cn/problem/P1776),測評結果為“Accepted”。
【例8】科技庄園
問題描述
Life種了一塊田,里面種了有一些桃樹。
Life對PFT說:“我給你一定的時間去摘桃,你必須在規定的時間之內回到我面前,否則你摘的桃都要歸我吃!”
PFT思考了一會,最終答應了!
由於PFT的數學不好!它並不知道怎樣才能在規定的時間獲得最大的價值,
由於PFT不是機器人,所以他的體力並不是無限的,他不想摘很多的桃以至體力為0,而白白把桃給Life。同時PFT每次只能摘一棵桃樹,每棵桃樹都可以摘K次(對於同一棵桃每次摘的桃數相同)。每次摘完后都要返回出發點(PFT一次拿不了很多)即Life的所在地(0,0),設試驗田左上角的桃樹坐標是(1,1)。
PFT每秒只能移動一個單位,每移動一個單位耗費體力1(摘取不花費時間和體力,但只限上下左右移動)。
輸入
第一行:四個數為N,M,TI,A(10≤N,M,TI,A≤100)分別表示試驗田的長和寬,Life給PFT的時間,和PFT的體力。
下面一個N行M列的矩陣桃田。表示每次每棵桃樹上能摘的桃數。
接下來N行M列的矩陣,表示每棵桃最多可以采摘的次數K。
輸出
一個數:PFT可以獲得的最大的桃個數。
輸入樣例
4 4 13 20
10 0 0 0
0 0 10 0
0 0 10 0
0 0 0 0
1 0 0 0
0 0 2 0
0 0 4 0
0 0 0 0
輸出樣例
10
(1)編程思路。
定義數組int dist[10005],num1[10005],num2[10005]; ,分別用於保存每顆桃樹到(0,0)的距離、樹上的桃子數、可采摘的次數。
在輸入后進行預處理,每塊桃樹田中把能摘到桃子的(桃子數量>0)並且可采摘次數>0桃樹相關信息分別保存到dist、num1和num2這三個數組中。
這樣,數組中的每顆桃樹可以看成一件物品,問題轉變為給定體力V、桃樹數量N(能摘到桃子的)、每顆桃樹最多被采摘K次,在這種情況下最多能摘多少桃子。就是一個典型的多重背包問題了。
背包容量為V,V受兩個條件約束,Life給PFT的時間TI和PFT的體力A。Life給PFT的時間可以轉換為體力,因為PFT每秒只能移動一個單位,每移動一個單位耗費體力1,因此給定時間TI就是可供PFT消耗的體力,另外PFT的體力是他實際擁有的體力,FPT最后回去時,體力不能為0,因此至少得留一格體力,這樣可確定背包容量V取TI和A-1中的較小值。
(2)源程序。
#include <stdio.h> long long max(long long a,long long b) { return a>b?a:b; } int main() { int n,m,x,y,v; scanf("%d%d%d%d",&n,&m,&x,&y); v=x<(y-1)?x:y-1; int i,j,k; int map[105][105]; long long f[105]={0}; for (i=1;i<=n;i++) for (j=1;j<=m;j++) scanf("%d",&map[i][j]); int dist[10005],num1[10005],num2[10005]; // 分別表示桃樹的距離、桃子數、可采摘的次數 int cnt=0; for (i=1;i<=n;i++) for (j=1;j<=m;j++) { int a; scanf("%d",&a); if (a>0 && map[i][j]>0) { dist[cnt]=2*(i+j); num1[cnt]=map[i][j]; num2[cnt]=a; cnt++; } } for (i=0;i<cnt;i++) for (j=v;j>=0;j--) for (k=1;k<=num2[i] && k*dist[i]<=j;k++) f[j]=max(f[j],f[j-k*dist[i]]+k*num1[i]); printf("%lld\n",f[v]); return 0; }
將上面的源程序提交給洛谷題庫P2760 科技庄園(https://www.luogu.com.cn/problem/P2760),測評結果為“Accepted”。
【例9】硬幣
問題描述
有面值分別為A1、A2、…、An的n種硬幣,每種硬幣各有C1、C2、…、Cn枚。用這些硬幣可以組合成多少種不同的不超過m的錢數。
輸入
輸入包含幾個測試用例。每個測試用例的第一行包含兩個整數n(1<=n<=100),m(m<=100000)。第二行包含2n個整數,表示A1,A2,…,An,C1,C2,…,Cn(1<=Ai<=100000,1<=Ci<=1000)。最后一個測試用例后跟兩個零。
輸出
對於每個測試用例,在一行上輸出答案。
輸入樣例
3 10
1 2 4 2 1 1
2 5
1 4 2 1
0 0
輸出樣例
8
4
(1)編程思路1。
n種硬幣,設第i種硬幣的面值為Ai,數量為Ci,將這些硬幣裝入容量為m的背包中,求這些硬幣能夠組成從1到m中的哪些數字。
多重背包問題,可采用二進制拆分優化的方法編寫如下的源程序1.
(2)源程序1。
#include <stdio.h> #include <string.h> int main() { int a[101], c[101],w[101]; int f[100005]; int n, m; while (scanf("%d%d",&n,&m) && (n||m)) { int i,j; for (i=1; i<=n; i++) scanf("%d",&a[i]); for (i=1; i<=n; i++) scanf("%d",&c[i]); memset(f,0,sizeof f); f[0] = 1; int ans = 0; for (i=1; i<=n; i++) { int k,cnt=0; for (j=1; j<=c[i]; j*=2) // 多重背包轉成0/1背包 { w[++cnt]=j*a[i]; c[i]-=j; } if (c[i]>0) { w[++cnt]=c[i]*a[i]; } for (j=1; j<=cnt; j++) for(k=m; k>=w[j]; k--) if (!f[k] && f[k -w[j]]) { f[k] = 1; ans++; } } printf("%d\n",ans); } return 0; }
將上面的源程序1提交給北大OJ題庫POJ 1742 Coins(http://poj.org/problem?id=1742),測評結果為“Time Limit Exceeded”,超時。
(3)編程思路2。
可以這樣來考慮問題。
對於第i種硬幣,如果A i ∗ C i≥m,相當於Ai取的個數沒有限制,即可以把第i種硬幣的數量視為無窮,作為一個完全背包來求解;否則,就作為一個多重背包來求解。
(4)源程序2。
#include <stdio.h> #include <string.h> int main() { int a[101], c[101],w[101]; int f[100005]; int n, m; while (scanf("%d%d",&n,&m) && (n||m)) { int i,j; for (i=1; i<=n; i++) scanf("%d",&a[i]); for (i=1; i<=n; i++) scanf("%d",&c[i]); memset(f,0,sizeof f); f[0] = 1; int ans = 0; for (i=1; i<=n; i++) { if (a[i] * c[i]>= m) { for (j=a[i]; j<=m; j++) // 完全背包 if (!f[j] && f[j - a[i]]) { f[j] = 1; ans++; } } else { int k,cnt=0; for (j=1; j<=c[i]; j*=2) // 多重背包轉成0/1背包 { w[++cnt]=j*a[i]; c[i]-=j; } if (c[i]>0) { w[++cnt]=c[i]*a[i]; } for (j=1; j<=cnt; j++) for(k=m; k>=w[j]; k--) if (!f[k] && f[k -w[j]]) { f[k] = 1; ans++; } } } printf("%d\n",ans); } return 0; }
將上面的源程序2提交給北大OJ題庫POJ 1742 Coins(http://poj.org/problem?id=1742),測評結果為“Accepted”。
(5)編程思路3。
也可以這樣考慮問題。
定義數組int f[100010],其中f[i]表示容量為i的背包是否可以裝滿,即錢數i是否可以構成。f[i]=1表示i可以構成。初始化時,將數組f全部設為0,f[ 0 ]設為1。
利用雙重循環,外循環i從0到n-1遍歷每種硬幣A[ i ],內循環 j 從A[i]開始往后遍歷到背包容量m,只要f[ j-A[i] ]==1(即表示錢數j-A[i]能夠構成),且f[j]==0(表示錢數j 尚未被構成),則錢數 j 是有可能構成的。
為什么說有可能呢?是因為能否構成錢數 j,還得看A[i]的數量是否達到上限C[i]。
為了記錄硬幣A[i]的使用數量,定義一個專門記錄個數的數組sum[M],在第一層循環內將數組sum[ ]初始化為0,一旦滿足f[j -A[i] ] && ! f[ j ] && num[ j - A[ i ] ]<C[i] ,則說明錢數j是可以構成的,則將f[ j ]的值置為1,再將sum[ j ]=num[ j - A[i ] ]+1 表示構成錢數j所對應的面值為A[ i ]的硬幣的使用數在錢數為 j-A[ i ]的基礎上加1。有了這個sum數組,可以保證硬幣A[i]的使用次數不會超過C[i]。
(6)源程序3。
#include <stdio.h> #include <string.h> int f[100010], sum[100010]; int main() { int n,m,i,j,cnt; int a[101],c[101]; while (scanf("%d%d",&n,&m) && n!=0 && m!=0) { for (i=0;i<n;i++) scanf("%d",&a[i]); for (i=0;i<n;i++) scanf("%d",&c[i]); memset(f, 0, sizeof(f)); f[0] = 1; cnt = 0; for (i=0; i<n; i++) { memset(sum, 0, sizeof(sum)); for (j=a[i]; j<= m; j++) { if (!f[j] && f[j - a[i]] && sum[j - a[i]] < c[i]) { f[j] = 1; sum[j] = sum[j-a[i]] + 1; cnt++; } } } printf("%d\n",cnt); } return 0; }
將上面的源程序3提交給北大OJ題庫POJ 1742 Coins(http://poj.org/problem?id=1742),測評結果為“Accepted”。
【例10】咖啡自動售貨機
問題描述
某咖啡自動售貨機只接收面值為1美分、5美分、10美分和25美分的硬幣。給定你手里擁有的這4種硬幣中每種硬幣的數量以及咖啡價格。請給出支付咖啡時必須使用的硬幣,要求你使用盡可能多的硬幣個數,並且不能找零。
輸入
輸入包括多組測試用例,每個測試用例用一行輸入。
每行輸入包含五個整數,每個數用一個空格分隔,描述一種需要解決的情況。第1個整數為P(1<=P<=10000),是以美分為單位的咖啡價格。接下來的四個整數,C1,C2,C3,C4(0<=Ci<=10000),是1美分、5美分、10美分和25美分的硬幣個數。輸入的最后一行包含五個零,作為測試用例的結束。
輸出
對於每種情況,都應該輸出一行,其中包含字符串“Throw in T1 cents、T2 nickels、T3 dimes和T4 quarters”,其中T1、T2、T3、T4是在使用盡可能多的硬幣的同時,應該使用適當價值的硬幣來支付咖啡。如沒有足夠的零錢來支付咖啡的價格,程序應該輸出“Charlie cannot buy coffee.”。
輸入樣例
12 5 3 1 2
16 0 0 0 1
0 0 0 0 0
輸出樣例
Throw in 2 cents, 2 nickels, 0 dimes, and 0 quarters.
Charlie cannot buy coffee.
(1)編程思路。
4種硬幣,面值分別為V[1]、V[2]、V[3]、V[4],每種硬幣的個數分別為C[1]、C[2]、C[3]、C[4]用這4種硬幣來組成咖啡的支付價格P。典型的多重背包問題。
本題在使用多重背包求解時,有兩個關鍵點。
1)在一般的背包問題里,有容量、價值,所要求的不是最大價值就是最小價值,而定義的f[i][j]表示裝有i件物品,容量為j的最大值為f[i][j],簡化為一維的f[j]表示的是在容量為j的時候最大價值為f[j]。在狀態轉移時,f[j]可以從f[j-v[i]]那邊得到值,但它並不需要去判斷在f[j-v[i]]前面是否有數可以組成f[j-v[i]],因此f[j-v[i]]為不為0,不影響最終結果。本題中,f[j]表示構成支付價格j時需要使用的最多硬幣數。在狀態轉移的時候要保證f[j-v[i]]>0(初始時,f[0]=1,其余元素全為0),這是因為題意是要求組成P分錢需要的最多硬幣數量,這樣要可以組成f[j],f[j-v[i]]必須要可以組成,否則就會出錯。同時,還要保證f[j-v[i]]+1>f[j](用了面值為V[i]的硬幣后,使用的硬幣數得多一些才行)且硬幣的使用個數不超過c[i](可參考上面例9的編程思路3)。
2)要求出各種硬幣使用多少個,需要記錄路徑,為此定義數組path[10010],在狀態由f[j-v[i]]到f[j]轉移時,記錄path[j]=j-v[i],這樣,j-path[j]=j-(j-v[i])=v[i],而v[i]正好是硬幣的面值,可以對相應面值的硬幣使用個數進行計數。
(2)源程序。
#include <stdio.h> #include <string.h> int main() { int f[10010],ans[10010],num[10010],path[10010]; int c[5]={0,1,5,10,25}; int t[5],p; while (scanf("%d%d%d%d%d",&p,&t[1],&t[2],&t[3],&t[4])&&(p||t[1]||t[2]||t[3]||t[4])) { memset(f,0,sizeof(f)); memset(ans,0,sizeof(ans)); memset(path,0,sizeof(path)); f[0]=1; int i,j; for (i=1;i<=4;i++) { memset(num,0,sizeof(num)); for (j=c[i];j<=p;j++) if (f[j-c[i]] && f[j-c[i]]+1>f[j] && num[j-c[i]]<t[i]) { f[j]=f[j-c[i]]+1; num[j]=num[j-c[i]]+1; path[j]=j-c[i]; } } if (f[p]>0) { i=p; while(i!=0) { ans[i-path[i]]++; i=path[i]; } printf("Throw in %d cents, %d nickels, %d dimes, and %d quarters.\n",ans[1],ans[5],ans[10],ans[25]); } else printf("Charlie cannot buy coffee.\n"); } return 0; }
將上面的源程序提交給北大OJ題庫POJ 1787 Charlie's Change(http://poj.org/problem?id=1787),可以Accepted。
練習題
1.P6771 [USACO05MAR]Space Elevator 太空電梯(https://www.luogu.com.cn/problem/P6771)

#include <stdio.h> #include <string.h> struct Node { int h,a,c; }; int max(int a,int b) { return a>b?a:b; } int main() { int k; scanf("%d",&k); struct Node block[405]; int f[40500]={0}; int i,j; for (i=1;i<=k;i++) { scanf("%d%d%d",&block[i].h,&block[i].a,&block[i].c); } for (i=1;i<k;i++) for (j=1;j<=k-i;j++) if (block[j].a>block[j+1].a) { struct Node temp; temp=block[j]; block[j]=block[j+1]; block[j+1]=temp; } for (i=1;i<=k;i++) { int n; for (n=1;n<=block[i].c;n++) //多重背包 { for (j=block[i].a;j>=block[i].h;j--) { f[j]=max(f[j],f[j-block[i].h]+block[i].h); } } } int ans=0; for (i=1;i<=block[k].a;i++) ans=max(ans,f[i]); printf("%d\n",ans); return 0; }

#include <stdio.h> #include <string.h> struct Node { int h,a,c; }; int max(int a,int b) { return a>b?a:b; } int main() { int k; scanf("%d",&k); struct Node block[405]; int f[40500]={0}; int i,j; for (i=1;i<=k;i++) { scanf("%d%d%d",&block[i].h,&block[i].a,&block[i].c); } for (i=1;i<k;i++) for (j=1;j<=k-i;j++) if (block[j].a>block[j+1].a) { struct Node temp; temp=block[j]; block[j]=block[j+1]; block[j+1]=temp; } for (i=1;i<=k;i++) { if (block[i].a>block[i].c*block[i].h) { int n; for (n=1;n<block[i].c;n*=2) { for (j=block[i].a;j>=block[i].h*n;j--) { f[j]=max(f[j],f[j-block[i].h*n]+block[i].h*n); } block[i].c-=n; } if (block[i].c>0) { for (j=block[i].a;j>=block[i].h*block[i].c;j--) f[j]=max(f[j],f[j-block[i].h*block[i].c]+block[i].h*block[i].c); } } else { for (j=block[i].h;j<=block[i].a;j++) { f[j]=max(f[j],f[j-block[i].h]+block[i].h); } } } int ans=0; for (i=1;i<=block[k].a;i++) ans=max(ans,f[i]); printf("%d\n",ans); return 0; }
2.POJ 1014 Dividing(http://poj.org/problem?id=1014)

#include <stdio.h> #define INF 100000000 int f[240005]; int max(int a,int b) { return a>b?a:b; } int main() { int test=1,c[7],w[1001],n[7]; while (1) { int i,j; for (i=1;i<=6;i++) scanf("%d",&n[i]); if (n[1]==0 && n[2]==0 && n[3]==0 && n[4]==0 && n[5]==0 && n[6]==0) break; int SumValue=0; for (i=1;i<=6;i++) { c[i] = i; SumValue+=i*n[i]; } printf("Collection #%d:\n",test++); if (SumValue%2) // 總價值為奇數,無法平分 { printf("Can't be divided.\n\n"); } else { int v = SumValue/2; for (i = 1; i <=v;i++) f[i]=-INF; f[0] = 0; for (i = 1; i <= 6; i++) { if (c[i] * n[i] >= v) //該種物品足以塞滿背包-->轉化為完全背包 { for (j= c[i]; j<= v; j++) f[j] = max(f[j], f[j-c[i]] + c[i]); } else { int k,cnt=0; for (j=1; j<=n[i]; j*=2) // 多重背包轉成0/1背包 { w[++cnt]=j*c[i]; n[i]-=j; } if (n[i]>0) { w[++cnt]=c[i]*n[i]; } for (j=1; j<=cnt; j++) for (k=v; k>=w[j]; k--) f[k] = max(f[k], f[k - w[j]] + w[j]); } } if(f[v] < 0) printf("Can't be divided.\n\n"); else printf("Can be divided.\n\n"); } } return 0; }
3.POJ1276 Cash Machine(http://poj.org/problem?id=1276)

#include <stdio.h> #include <string.h> int max(int a,int b) { if (a>b) return a; else return b; } int main() { int f[100005],w[1000]; int c,n; while (scanf("%d%d",&c,&n)!=EOF) { int cnt=0; memset(f,0,sizeof(f)); int i,j; for (i=1; i<=n; i++) { int k,d; scanf("%d%d",&k,&d); for (j=1; j<=k; j*=2) // 多重背包轉成0/1背包 { w[++cnt]=j*d; k-=j; } if (k>0) { w[++cnt]=k*d; } } for (i=1; i<=cnt; i++) // 01背包的求解 for(j=c; j>=w[i]; j--) f[j]=max(f[j],f[j-w[i]]+w[i]); printf("%d\n",f[c]); } return 0; }