背包问题(2):0/1背包
0/1背包是最基本的背包问题,其基本特点是:每种物品仅有一件,可以选择放或不放,即每个物品最多只能放一次。
0/1背包问题的一般描述为:有N个物品,第i个物品的重量与价值分别为W[i]与P[i]。背包容量为V,试问在每个物品最多使用一次(物品必须保持完整)的情况下,如何让背包装入的物品具有更大的价值总和。
其一般解题思路为:
设f[i][j]表示容量为j时放入前i个物品得到的最大价值。
对于第i件物品,有两种选择,即放或者不放。
如果不放,则f[i][j]=f[i-1][j];
如果放,则f[i][j]=f[i-1][j-W[i]]+P[i]
因此有状态转移方程:f[i][j]=max (f[i-1][j], f[i-1][j-W[i]]+P[i])。
编写代码时,一般可以写成如下的循环。
for (i=1;i<=N;i++) // 对每件物品进行处理
for (j=1;j<=V;j++)
{
if (j-W[i]<0)
f[i][j]=f[i-1][j];
else
{
f[i][j]=max(f[i-1][j],f[i-1][j-W[i]]+P[i]);
}
}
最优值为f[N][V]。
由上面的循环过程可知,循环过程中的f[i][j]仅与f[i-1][j]或f[i-1][j-W[i]]相关,因此可以将存储空间由二维数组压缩成一维数组,即
f[j]=max(f[j], f[j−W[i]]+P[i])
在具体递推处理时,需要采用逆推的方法进行计算最优值。一般写成如下的循环。
for (i=1;i<=N;i++) // 对每件物品进行处理
for (j=V;j>=W[i];j--) // 逆推计算最优值
{
f[j]=max(f[j],f[j-W[i]]+P[i]);
}
对于背包问题,数组f的初始化也需要注意。对于不同的问题,初始化值一般不同。
一般在求最优解的背包问题题目中,有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。这两种问法的实现方法在初始化的时候就有所不同。
如果是要求“恰好装满背包”,那么在初始化时,除了f[0]为0,其它元素f[1]~f[V]均初始化为-∞,这样就可以保证最终得到的f[V]是一种恰好装满背包的最优解。
如果并没有要求必须把背包装满,而是只希望获得的价值尽量大,初始化时应该将f[0]~f[V]全部设为0。
为什么要这样初始化呢?
初始化f数组事实上就是在没有任何物品放入背包时的合法状态。如果要求背包“恰好装满”,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时f的值也就全部为0了。
0/1背包问题是最基本的背包问题,它包含了背包问题中设计状态、确定状态转移方程的最基本思想,另外,其他类型的背包问题往往也可以转换成0/1背包问题求解。因此需要认真体会并掌握。
【例1】采药
问题描述
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入
第一行有2个整数 T(1≤T≤1000)和M(1≤M≤100),用一个空格隔开,T代表总共能够用来采药的时间,M 代表山洞里的草药的数目。
接下来的 M行每行包括两个在1到 100 之间(包括 1和100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出
输出在规定的时间内可以采到的草药的最大总价值。
输入样例
70 3
71 100
69 1
1 2
输出样例
3
(1)编程思路。
这是一道典型的0/1背包问题,把采摘每株草药的时间看做标准模型中的重量,把规定的时间看做载重为T的背包,这样问题和基本模型就一样了。
分别采用二维数组和一维数组的方法编写源程序如下。
(2)采用二维数组编写的源程序1。
#include <stdio.h> int max(int a,int b) { if (a>b) return a; else return b; } int main() { int f[101][1001]={0}; int w[101],v[101]; int t,m; scanf("%d%d",&t,&m); int i,j; for (i=1;i<=m;i++) { scanf("%d%d",&w[i],&v[i]); } for (i=1;i<=m;i++) for (j=1;j<=t;j++) { if (j-w[i]<0) f[i][j]=f[i-1][j]; else { f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]); } } printf("%d\n",f[m][t]); return 0; }
(3)采用一维数组编写的源程序2。
#include <stdio.h> int max(int a,int b) { if (a>b) return a; else return b; } int main() { int f[1001]={0}; int w[101],v[101]; int t,m; scanf("%d%d",&t,&m); int i,j; for (i=1;i<=m;i++) { scanf("%d%d",&w[i],&v[i]); } for (i=1;i<=m;i++) for (j=t;j>=w[i];j--) { f[j]=max(f[j],f[j-w[i]]+v[i]); } printf("%d\n",f[t]); return 0; }
将上面的源程序提交给洛谷题库P1048 [NOIP2005 普及组] 采药(https://www.luogu.com.cn/problem/P1048),测评结果为“Accepted”。
【例2】背包问题
问题描述
有N件物品和一个容量为M的背包。第i件物品的重量是Wi,价值是Di。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
输入
第一行:物品个数N (1≤N≤3,402)和背包大小M(1≤M≤12,880)。
第二行至第 N+1 行:第i 个物品的重量 Wi(1≤Wi≤400)和价值Di(1≤Di≤100)。
输出
输出一行最大价值。
输入样例
4 6
1 4
2 6
3 12
2 7
输出样例
23
(1)编程思路。
最基本的0/1背包问题,可以按前面介绍的模板写成一维数组的方式,也可以写成二维数组的方式。
(2)采用一维数组编写的源程序1。
#include <stdio.h> int max(int a,int b) { if (a>b) return a; else return b; } int main() { int f[12881]={0}; int n,m; scanf("%d%d",&n,&m); int c[3500],w[3500]; int i,j; for (i=1;i<=n;i++) { scanf("%d%d",&c[i],&w[i]); } for (i=1;i<=n;i++) for (j=m;j>=c[i];j--) { f[j]=max(f[j],f[j-c[i]]+w[i]); } printf("%d\n",f[m]); return 0; }
将上面的源程序1提交给洛谷题库P2871 [USACO07DEC]Charm Bracelet S(https://www.luogu.com.cn/problem/P2871),测评结果为“Accepted”。
(3)采用二维数组编写的源程序2。
#include <stdio.h> int max(int a,int b) { if (a>b) return a; else return b; } int f[3410][12881]={0}; int main() { int n,m; scanf("%d%d",&n,&m); int d[3410],w[3410]; int i,j; for (i=1;i<=n;i++) { scanf("%d%d",&w[i],&d[i]); } for (i=1;i<=n;i++) // 对每件物品进行处理 for (j=1;j<=m;j++) { if (j-w[i]<0) f[i][j]=f[i-1][j]; else { f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+d[i]); } } printf("%d\n",f[n][m]); return 0; }
将上面的源程序2提交给洛谷题库P2871 [USACO07DEC]Charm Bracelet S(https://www.luogu.com.cn/problem/P2871),测评结果为“Unaccepted”,其中测试点#2和测试点#10,显示“MLE”。
【例3】装箱问题
题目描述
有一个箱子容量为V(正整数,0<=V<=20000),同时有n个物品(0<n<=30,每个物品有一个体积(正整数)。
要求n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。
输入格式
1个整数,表示箱子容量
1个整数,表示有n个物品
接下来n行,分别表示这n个物品的各自体积。
输出格式
1个整数,表示箱子剩余空间。
输入样例
24
6
8
3
12
7
9
7
输出样例
0
(1)编程思路1。
本题也是典型的0/1背包问题,并且是0/1背包的简化版,把箱子看成背包,容量看成载重量,每个物品的体积看成重量,剩余空间最小也就是尽量装满背包。于是这个问题便成了:
有一个载重量为V的背包,有N个物品,尽量多装物品,使背包尽量的重。
定义数组f[20001],其中元素f[i]表示重量i可否构成。
状态转移方程为: f[j]=f[j-w[i]] { f[j-w[i]]=true}
初始时,f[0]=1,什么也不装,重量0肯定可以构成。其余元素全部为0。
最终的解就是v-x (x<=n 且f[x]=true 且 f[x+1..n]=false)。
(2)源程序1。
#include <stdio.h> int main() { int f[20001]={0}; int v,n; scanf("%d%d",&v,&n); int w[31]; int i,j; for (i=1;i<=n;i++) { scanf("%d",&w[i]); } f[0]=1; for (i=1;i<=n;i++) for (j=v;j>=w[i];j--) { if (f[j-w[i]]==1) f[j]=1; } for (i=v;i>0;i--) if (f[i]==1) break; printf("%d\n",v-i); return 0; }
(3)编程思路2。
也可以这样解决问题。
定义数组f[20001],其中元素f[j]表示载重量为j的背包可装载的最大重量。
显然对于每个物品i而言,
要么不装,f[j]=f[j],
要么装, f[j]=f[j-w[i]]+w[i]
取二者的较大值。
因此,状态转移方程为:f[j]=max(f[j],f[j-w[i]]+w[i])
这个状态转移方程可以这样理解:要载重为j的背包空出w[i](j-w[i])的空间且装上第i个物品,比不装获得的价值大时,就装上它。
初始时,数组f的元素值全部为0。各背包全部为空。
最终的解就是v-f[v]。
(4)源程序2。
#include <stdio.h> int max(int a,int b) { if (a>b) return a; else return b; } int main() { int f[20001]={0}; int v,n; scanf("%d%d",&v,&n); int w[31]; int i,j; for (i=1;i<=n;i++) { scanf("%d",&w[i]); } for (i=1;i<=n;i++) for (j=v;j>=w[i];j--) { f[j]=max(f[j],f[j-w[i]]+w[i]); } printf("%d\n",v-f[v]); return 0; }
将上面的源程序提交给洛谷题库P1049 [NOIP2001 普及组] 装箱问题(https://www.luogu.com.cn/problem/P1049),测评结果为“Accepted”。
【例4】数字组合
问题描述
有n个正整数,找出其中和为t(t也是正整数)的可能的组合方式。如:
n=5,5个数分别为1,2,3,4,5,t=5;
那么可能的组合有5=1+4和5=2+3和5=5三种组合方式。
输入
输入的第一行是两个正整数n和t,用空格隔开,其中1<=n<=20,表示正整数的个数,t为要求的和(1<=t<=1000)
接下来的一行是n个正整数,用空格隔开。
输出
和为t的不同的组合方式的数目。
输入样例
5 5
1 2 3 4 5
输出样例
3
(1)编程思路。
将输入的和值t看成背包的总容量,输入的每个正整数看成一件物品,其重量就是正整数本身,就是一个简单的0/1背包问题。
设f[i]表示构成和值i的不同组合方式的数目,初始时,f[0]=1(背包中什么正整数没放,肯定是0,和值0肯定有1种组合方式),其余元素全部为0。
显然,对于正整数a,如果加入背包,则有f[j]=f[j]+f[j-a]。
(2)源程序。
#include <stdio.h> #include <string.h> int main() { int n,t; scanf("%d%d",&n,&t); int i,j; int f[1005]={0}; f[0]=1; for (i=1;i<=n;++i) { int a; scanf("%d",&a); for (j=t;j>=a;j--) f[j]+=f[j-a]; } printf("%d\n",f[t]); return 0; }
【例5】积木城堡
题目描述
XC的儿子小XC最喜欢玩的游戏用积木垒漂亮的城堡。城堡是用一些立方体的积木垒成的,城堡的每一层是一块积木。小XC是一个比他爸爸XC还聪明的孩子,他发现垒城堡的时候,如果下面的积木比上面的积木大,那么城堡便不容易倒。所以他在垒城堡的时候总是遵循这样的规则。
小XC想把自己垒的城堡送给幼儿园里漂亮的女孩子们,这样可以增加他的好感度。为了公平起见,他决定把送给每个女孩子一样高的城堡,这样可以避免女孩子们为了获得更漂亮的城堡而引起争执。可是他发现自己在垒城堡的时候并没有预先考虑到这一点。所以他现在要改造城堡。由于他没有多余的积木了,他灵机一动,想出了一个巧妙的改造方案。他决定从每一个城堡中挪去一些积木,使得最终每座城堡都一样高。为了使他的城堡更雄伟,他觉得应该使最后的城堡都尽可能的高。
任务:
请你帮助小XC编一个程序,根据他垒的所有城堡的信息,决定应该移去哪些积木才能获得最佳的效果。
输入
第一行是一个整数N(N<=100),表示一共有N座城堡。以下N行每行是一系列非负整数,用一个空格分隔,按从下往上的顺序依次给出一座城堡中所有积木的棱长。用-1结束。一座城堡中的积木不超过100块,每块积木的棱长不超过100。
输出
一个整数,表示最后城堡的最大可能的高度。如果找不到合适的方案,则输出0。
输入样例
2
2 1 –1
3 2 1 -1
输出样例
3
(1)编程思路。
塔好积木再拿走就相当于当初搭的时候没选拿走的积木。这样一转化,问题就清楚了。把积木可搭建的最大高度看成背包的载重,每块积木的高度就是物品的重量。也就是用给定的物品装指定的包,使每个包装的物品一样多,且在符合条件的前提下尽量多。
这样就变成典型的0/1背包问题了。对于每一个城堡求一次,最终找到每一个城堡都可达到的最大高度即可。
定义二维数组int f[101][10001],其中元素f[i][j]=1表示第i座城堡高度为j可搭出,f[i][j]=0表示第i座城堡高度为j不能搭出。
状态转移方程为: f[i][j]=f[i][j-a[k]] { f[i][j-a[k]]=true} 其中a[k]表示第i座城堡所用的第k块积木的棱长。
初始时,f[i][0]=1,每座城堡高度为0肯定可以搭出来。
从所有城堡高度的最小值开始枚举,若对于某一高度值h,所有的f[1][h]~f[n][h]均为1,则h就是最后城堡的最大可能的高度。
(2)源程序。
#include <stdio.h> int f[101][10001]={0}; int main(void) { int n; scanf("%d",&n); int i,j,k; int a[101]; int min=10001; for (i=1;i<=n;i++) { int cnt=1; int sum=0; int x; while (1) { scanf("%d",&x); if (x==-1) break; sum+=x; a[cnt++]=x; } if (min>sum) min=sum; f[i][0]=1; for (j=1;j<cnt;j++) for (k=sum;k>=a[j];k--) { if (f[i][k-a[j]]==1) f[i][k]=1; } } for (i=min;i>0;i--) { for (j=1;j<=n;j++) if (f[j][i]==0) break; if (j>n) break; } printf("%d\n",i); return 0; }
将上面的源程序提交给洛谷题库P1504 积木城堡(https://www.luogu.com.cn/problem/P1504),测评结果为“Accepted”。
【例6】凑零钱
问题描述
韩梅梅喜欢满宇宙到处逛街。现在她逛到了一家火星店里,发现这家店有个特别的规矩:你可以用任何星球的硬币付钱,但是绝不找零,当然也不能欠债。韩梅梅手边有 10000枚来自各个星球的硬币,需要请你帮她盘算一下,是否可能精确凑出要付的款额。
输入
输入第一行给出两个正整数:N(≤10000)是硬币的总个数,M(≤100)是韩梅梅要付的款额。第二行给出 N 枚硬币的正整数面值。数字间以空格分隔。
输出
在一行中输出硬币的面值 V1≤V2≤⋯≤Vk,满足条件 V1 +V2 +...+Vk =M。数字间以 1 个空格分隔,行首尾不得有多余空格。若解不唯一,则输出最小序列。若无解,则输出 No Solution。
注:我们说序列{ A[1],A[2],⋯ }比{ B[1],B[2],⋯ }“小”,是指存在 k≥1 使得 A[i]=B[i] 对所有 i<k 成立,并且 A[k]<B[k]。
输入样例
8 9
5 9 8 7 2 3 4 1
输出样例
1 3 5
(1)编程思路。
将要付的款额M看成背包的总容量,将N枚物品的面值看成装入背包中物品的重量,本题实际上就是看用这N枚硬币能否装满容量为M的背包,若能装满,输出字典序最小的装载方案。
定义数组int f[105];其中f[i]表示容量为i的背包是否能装满,初始时f[0]=1(肯定存在背包中什么不装的方案),其余f[i]全为0。
显然,若 f[j-a[i]]=1,容量为j-a[i]的背包可以装满,则加入面值为a[i]的硬币后,容量为j的背包也可装满,即f[j]=1。
为了记录路径,定义数组int path[10005][105];,该数组记录当前状态是由哪个状态转移过来的,初始值也全为0。
若f[j-a[i]]=1,则一定可得到f[j]=1,则记录f[i][j]=1,表示容量为j的背包是加入了面值为a[i]的硬币。
因为装入的方案可能有多种,为了输出字典序最小的方案,显然应该用面值尽可能小的硬币来装。为此,将保存面值的数组a先按从大到小的顺序排序,这样进行0/1背包时,物品的加入是按面值大的先加入背包,面值小的后加入背包,这样后面加入的更小的面值方案会覆盖前面加入的面值较大的方案,从而实现输出字典序最小的硬币方案。
(2)源程序。
#include <stdio.h> #include <string.h> #include <algorithm> using namespace std; int cmp(int a,int b) { return a>b; } int path[10005][105]; int main() { int n,m; scanf("%d %d",&n,&m); int f[105],a[10005]; int i,j; for (i=1;i<=n;i++) scanf("%d",&a[i]); memset(f,0,sizeof(f)); memset(path,0,sizeof(path)); f[0]=1; sort(a+1,a+n+1,cmp); for (i=1;i<=n;i++) { for (j=m;j>=a[i];j--) { if (f[j-a[i]]==1) { f[j]=1; path[i][j]=1; } } } if (f[m]>0) { i=n , j = m; int flag = 0; while (i>0 && j>=0) { if (path[i][j]) // 当前状态可选 { if (flag==0) { printf("%d",a[i]); flag = 1;} else printf(" %d",a[i]); j -= a[i]; } i--; } printf("\n"); } else printf("No Solution\n"); return 0; }
【例7】烹调方案
题目描述
gw希望能在T时间内做出最美味的食物,可使用的食材一共有n件,每件食材有三个属性,ai,bi和ci,如果在t时刻完成第i样食材则得到ai-t*bi的美味指数,用第i件食材做饭要花去ci的时间。
众所周知,gw的厨艺不怎么样,所以他需要你设计烹调方案使得美味指数最大。
输入
第一行是两个正整数T和n(1<=n<=50),表示控制时间和食材个数。
下面一行n个整数,ai
下面一行n个整数,bi
下面一行n个整数,ci。所有数字均小于100,000
输出
输出最大美味指数
输入样例
74 1
502
2
47
输出样例
408
(1)编程思路。
一般的背包问题中,每个物品的重量和价值是固定的,因此物品的装入顺序对结果不产生影响。
在本题中,因为每种食材的加工完成时刻t不同,其得到的美味指数不一样,美味指数与时刻t存在线性的关系。因此,需要考虑每种食材的加工顺序。
考虑相邻的两个食材x,y。假设现在已经耗费p的时间,若先加工食材x,再加工y,得到的美味指数为:
a[x]-(p+c[x])*b[x]+a[y]-(p+c[x]+c[y])*b[y] (式①)
若先加工食材x,再加工y,得到的美味指数为:
a[y]-(p+c[y])*b[y]+a[x]-(p+c[y]+c[x])*b[x] (式②)
对这两个式子化简,得到①>②的条件是 c[x]*b[y]<c[y]*b[x]。
只要满足上面这个条件,则对于食材对(x,y),x在y之前进行加工得到的美味指数一定最大。
定义结构体数组
struct node
{
long long a, b, c;
} m[51];
来保存输入的各食材数据。
将结构体数组按成员分量c/b从小到大的顺序排列后,按数组中的顺序依次加工食材(也就是依次加入背包),就是简单的0/1背包问题了。
(2)源程序。
#include <stdio.h> long long max(long long a,long long b) { return a>b?a:b; } struct node { long long a, b, c; }; int main() { int t,n; scanf("%d%d",&t,&n); struct node m[51]; int i,j; for (i = 1; i <= n; i++) scanf("%lld",&m[i].a); for (i = 1; i <= n; i++) scanf("%lld",&m[i].b); for (i = 1; i <= n; i++) scanf("%lld",&m[i].c); for (i=1;i<n;i++) for (j=1;j<=n-i;j++) if (m[j].c*m[j+1].b>m[j+1].c*m[j].b) { struct node temp; temp=m[j]; m[j]=m[j+1]; m[j+1]=temp; } long long f[100001]={0}; long long ans=0; for (i = 1; i <= n; i++) for (j = t; j>= m[i].c; j--) { f[j] = max(f[j], f[j-m[i].c] + m[i].a - j*m[i].b); ans = max(f[j], ans); } printf("%lld\n",ans); return 0; }
将上面的源程序提交给洛谷题库P1417 烹调方案(https://www.luogu.com.cn/problem/P1417),测评结果为“Accepted”。
【例8】多米诺骨牌
问题描述
多米诺骨牌由上下2个方块组成,每个方块中有1∼6个点。现有排成行的上方块中点数之和记为S1,下方块中点数之和记为S2,它们的差为|S1-S2 |。
如图,S1=6+1+1+1=9,S2=1+5+3+2=11, |S1-S2|=2。
每个多米诺骨牌可以旋转180°,使得上下两个方块互换位置。请你计算最少旋转多少次才能使多米诺骨牌上下2行点数之差达到最小。
对于图中的例子,只要将最后一个多米诺骨牌旋转180°,即可使上下2行点数之差为0。
输入
输入文件的第一行是一个正整数 n (1≤n≤1000),表示多米诺骨牌数。接下来的n行表示n 个多米诺骨牌的点数。每行有两个用空格隔开的正整数,表示多米诺骨牌上下方块中的点数 a和b,且1≤a,b≤6。
输出
输出文件仅一行,包含一个整数。表示求得的最小旋转次数。
输入样例
4
6 1
1 5
1 3
1 2
输出样例
1
(1)编程思路。
不管骨牌这样旋转变换,其上下两行数字和s1+s2=sum,得到的总和sum是不会变的。
题目要求的是上下两行数字和最小差值情况下的最小交换次数。直接表示差值,可能有正有负,不太方便。我们可以转化一下,保存某一行的数字和(例如上面的第1行的数字和S1),由于每次旋转交换只是把上下两个数交换,所有骨牌上下两行数的总和sum是不变的,这样差值就容易求出来了,|sum-s1-s1|就是所求上下两行数字和的差值了。
设 f[i][j] 表示前i个数字,第一行的数字和是j时,最小的交换次数。初始值所有f[i][j]都是无穷大(本题中可以直接设置为300000,就足够大了),f[1][a[1]]=0(第1张骨牌第1行的和值为自身,显然不用交换),f[1][b[1]]=1(第1张骨牌第1行的和值为下面的数字,需要旋转180°,交换1次)。(数组a[]和b[]分别保存第一行和第二行各骨牌上面的数字)
由于n张骨牌的点数和最大可能为6*n,因此可以将背包容量看成6*n。之后从第2张骨牌开始,每张骨牌加入背包中,考虑状态转移
if (j-a[i] >= 0) f[i][j] = min(f[i][j], f[i-1][j-a[i]]); // 当前不交换
if (j-b[i] >= 0) f[i][j] = min(f[i][j], f[i-1][j-b[i]]+1); // 当前交换
最后再枚举一下前n个骨牌第一行的和f[n][i],找出使绝对值abs(sum-i-i)最小的f[n][i]就是所求的最小交换次数。
(2)源程序。
#include <stdio.h> int min(int a,int b) { return a<b?a:b; } int abs(int a) { return a>0?a:-a; } int a[1005],b[1005],f[1005][6005]; int main() { int n; scanf("%d",&n); int i,j; int sum=0; for (i=1;i<=n;i++) { scanf("%d%d",&a[i],&b[i]); sum+=a[i]+b[i]; } for (i=1;i<=n;i++) for (j=0;j<=6*n;j++) f[i][j]=300000; f[1][b[1]]=1; f[1][a[1]]=0; for (i=2;i<=n;i++) { for (j=0;j<=6*n;j++) { if(j-a[i]>=0) f[i][j]=min(f[i][j],f[i-1][j-a[i]]); if(j-b[i]>=0) f[i][j]=min(f[i][j],f[i-1][j-b[i]]+1); } } int ans=300000,diff=300000; for (i=0;i<=sum;i++) { if (f[n][i]!=300000) { if (abs(sum-i-i)<diff) { diff=abs(sum-i-i); ans=f[n][i]; } else if (abs(sum-i-i)==diff) ans=min(ans,f[n][i]); } } printf("%d\n",ans); }
将上面的源程序提交给洛谷题库P1282 多米诺骨牌(https://www.luogu.com.cn/problem/P1282),测评结果为“Accepted”。
【例9】豪华游轮
问题描述
有一条豪华游轮(其实就是条小木船),这种船可以执行4种指令:
right X : 其中X是一个1到719的整数,这个命令使得船顺时针转动X度。
left X : 其中X是一个1到719的整数,这个命令使得船逆时针转动X度。
forward X : 其中X是一个整数(1到1000),使得船向正前方前进X的距离。
backward X : 其中X是一个整数(1到1000),使得船向正后方前进X的距离。
随意的写出了n个命令,找出一个种排列命令的方法,使得船最终到达的位置距离起点尽可能的远。
输入
第一行一个整数n(1 <= n <= 50),表示给出的命令数。
接下来n行,每行表示一个命令。
输出
一个浮点数,能够走的最远的距离,四舍五入到6位小数。
输入样例
3
forward 100
backward 100
left 90
输出样例
141.421356
(1)编程思路。
这种船可以执行的4种指令可以分成3种情况:向前走、向后走、旋转一定的角度(或者顺时针、或者逆时针)。
为了能够走最远的距离,显然应该把所有向前走的指令全部合并起来,一直向前走得到一条边sumf,把所有向后走的指令也全部合并起来,一直向后走也得到另一条边sumb,再将所有的旋转指令有机组合起来,通过它们的旋转组合,形成两条边sumf与sumb的夹角,这个夹角越接近180度,夹角所对应的第3条边(也是船走的距离)就越大。
因此本题实际是一个变形的0/1背包问题。
设f[i][j]表示执行前i条旋转指令后,夹角为j是否可得到,初始值除f[0][0]=1外,其余全部为0。
之后按0/1背包的处理方法,将各旋转指令作为一个个的物品加入容量为360的背包中,指令中的旋转角度ang[i]看成是物品的重量,则有状态转移方式为:
若f[i-1][j]=1,则f[i][j]=1 (第i条指令不加入背包中)
f[i][(j+ang[i]+720)%360]=1 (第i条指令加入背包中)
按照0/1背包处理完后,然后遍历f[cnt][i] (cnt为旋转指令的条数)求得离180最近的角度,再用余弦公式计算出第3条边就可以了。
在这里我们只需要通过让加入背包的旋转指令使得旋转的度数尽量接近180°就可以了,而没有加入背包的旋转指令只需要在走完后,再执行没有加入背包的旋转指令,在原地旋转一通,并不影响走的总距离。
(2)源程序。
#include <stdio.h> #include <math.h> # define PI 3.1415926535 int min(int a,int b) { return a<b?a:b; } int abs(int a) { return a>0?a:-a; } int main() { int n; scanf("%d",&n); int i,j; int sumf=0,sumb=0; // 向前走的总距离和向后走的总距离 int ang[55]; int cnt=0; for (i=1;i<=n;i++) { int x; char ch[11]; scanf("%s%d",ch,&x); if (ch[0]=='f') sumf+=x; if (ch[0]=='b') sumb+=x; if (ch[0]=='r') ang[++cnt]=x; if (ch[0]=='l') ang[++cnt]=-x; } int f[105][405]={0}; f[0][0]=1; for (i=1;i<=cnt;i++) for (j=0;j<360;j++) if (f[i-1][j]) { f[i][j]=1; f[i][(j+ang[i]+720)%360]=1; } int p=180; for (i=0;i<360;i++) if (f[cnt][i]) p=min(p,abs(i-180)); double ans=sqrt(sumf*sumf+sumb*sumb+2*sumb*sumf*cos(p*PI/180)); printf("%.6f\n",ans); return 0; }
将上面的源程序提交给洛谷题库P2625 豪华游轮(https://www.luogu.com.cn/problem/P2625),测评结果为“Accepted”。
【例10】奶牛博览会
问题描述
奶牛想证明它们是聪明而风趣的。为此,贝西筹备了一个奶牛博览会,她已经对N头奶牛进行了面试,确定了每头奶牛的智商和情商。
贝西有权选择让哪些奶牛参加展览。由于负的智商或情商会造成负面效果,所以贝西不希望出展奶牛的智商之和小于零,或情商之和小于零。满足这两个条件下,她希望出展奶牛的智商与情商之和越大越好,请帮助贝西求出这个最大值。
输入
第一行:单个整数N,1≤N≤100。
第二行到第 N+1行:第 i+1行有两个整数:Si和Fi,表示第i头奶牛的智商和情商,− 1000≤Si,Fi ≤1000。
输出
输出单个整数:表示情商与智商和的最大值。贝西可以不让任何奶牛参加展览,如果这样做是最好的,输出0。
输入样例
5
-5 7
8 -6
6 -3
2 1
-8 -5
输出样例
8
(1)编程思路。
本题可以用0/1背包模型求解,把智商当成“物品体积”,情商当成“物品价值”来解决问题。
但由于题目中,智商和情商均可以为负数,因此需要对负数进行处理。
由题目可知,最多100头牛,每头牛的智商在-1000~1000之间,即智商和(背包容量)在-100000至100000之间,由于数组下标不能为负数,考虑将原点0右移100000,即用下标0~200000来对应这些智商和,下标范围0~99999来对应负数,下标100000对应为0,下标范围100000~200000来对应正数。
套用0/1背包模型时要注意,对于正数逆序求解(这是0/1背包模型的基本套路),例如
for (j=200000;j>=s[i];j--)
f[j]=max(f[j],f[j-s[i]]+f[i]);
但对于负数,由于j-s[i]>j,因此不能逆向推,采用正向求解。例如
for(int j=0;j<=200000+s[i];j++)
f[j]=max(f[j],f[j-s[i]]+f[i]);
最后,找到下标范围为100000~200000(对应智商不为负数)内,i+f[i]-100000(f[i]>0)的最大值就是问题的答案。
(2)源程序。
#include <stdio.h> #include <string.h> int max(int a,int b) { return a>b?a:b; } int f[200005]; int main() { int n; scanf("%d",&n); int s[105],f[105]; int i,j; for (i=1;i<=n;i++) scanf("%d%d",&s[i],&f[i]); memset(f,-0x3f,sizeof(f)); f[100000]=0; for (i=1;i<=n;i++) { if(s[i]>0) for (j=200000;j>=s[i];j--) f[j]=max(f[j],f[j-s[i]]+f[i]); else for(int j=0;j<=200000+s[i];j++) f[j]=max(f[j],f[j-s[i]]+f[i]); } int ans=0; for (i=100000;i<=200000;i++) if(f[i]>=0) ans=max(ans,i+f[i]-100000); printf("%d\n",ans); return 0; }
将上面的源程序提交给北大OJ题库POJ 2184 Cow Exhibition(http://poj.org/problem?id=2184),可以Accepted。
练习题
1.P1164 小A点菜(https://www.luogu.com.cn/problem/P1164)
#include <stdio.h> #include <string.h> int main() { int n,m; scanf("%d%d",&n,&m); int i,j; int f[10001]={0}; f[0]=1; for (i=1;i<=n;++i) { int a; scanf("%d",&a); for (j=m;j>=a;j--) f[j]+=f[j-a]; } printf("%d\n",f[m]); return 0; }
2.P1877 [HAOI2012]音量调节(https://www.luogu.com.cn/problem/P1877)
#include <stdio.h> int main(void) { int c[1001]; int f[51][1001]={0}; // f[i][j]=1表示第i首歌是否能为音量j int n,begin,max; scanf("%d%d%d",&n,&begin,&max); f[0][begin]=1; int i,j; for (i=1;i<=n;i++) scanf("%d",&c[i]); for (i=1;i<=n;i++) { for (j=0;j<=max;j++) { if (f[i-1][j]) { if(j+c[i]<=max) f[i][j+c[i]]=1; if(j-c[i]>=0) f[i][j-c[i]]=1; } } } for (i=max;i>=0;i--) { if (f[n][i]==1) { printf("%d\n",i); return 0; } } printf("-1\n"); return 0; }
3.P1926 小书童——刷题大军(https://www.luogu.com.cn/problem/P1926)
#include <stdio.h> int main() { int n,m,k,r; scanf("%d%d%d%d",&n,&m,&k,&r); int t1[11],t2[11],s[11]; int i,j; for (i=1;i<=n;i++) scanf("%d",&t1[i]); for (i=1;i<=m;i++) scanf("%d",&t2[i]); for (i=1;i<=m;i++) scanf("%d",&s[i]); int f[151]={0}; f[r]=0; for (i=1;i<=m;i++) { for (j=r;j>=t2[i];j--) { if (f[j]<f[j-t2[i]]+s[i]) f[j]=f[j-t2[i]]+s[i]; } } for (i=0;i<=r;i++) if (f[i]>=k) break; int res=r-i; int cnt=0; for (i=1;i<n;i++) for (j=1;j<=n-i;j++) if (t1[j]>t1[j+1]) { int t=t1[j]; t1[j]=t1[j+1]; t1[j+1]=t; } for (i=1;i<=n;i++) { if (t1[i]<=res) { cnt++; res-=t1[i]; } else break; } printf("%d\n",cnt); return 0; }
4.P2370 yyy2015c01 的U盘(https://www.luogu.com.cn/problem/P2370)
#include <stdio.h> int max(int a,int b) { return a>b?a:b; } struct Node { int w,v; }; struct Node a[1005]; int main() { int n,p,s; scanf("%d%d%d",&n,&p,&s); int i,j; for (i=1;i<=n;i++) scanf("%d%d",&a[i].w,&a[i].v); int f[1005]={0}; for (i=1;i<n;i++) for (j=1;j<=n-i;j++) if (a[j].w>a[j+1].w) { struct Node temp; temp=a[j]; a[j]=a[j+1]; a[j+1]=temp; } for (i=1;i<=n;i++) { for (j=s;j>=a[i].w;j--) { f[j]=max(f[j],f[j-a[i].w]+a[i].v); if (f[j]>=p) { printf("%d\n",a[i].w); return 0; } } } printf("No Solution!\n"); return 0; }
5.P2392 kkksc03考前临时抱佛脚(https://www.luogu.com.cn/problem/P2392)
#include <stdio.h> #include <string.h> int max(int a,int b) { return a>b?a:b; } int main() { int s[5]; int i,j,k; for (i=1;i<=4;i++) scanf("%d",&s[i]); int opt[1201],time[21]; int ans=0; for (i=1;i<=4;i++) { int sum=0; for (j=1;j<=s[i];j++) { scanf("%d",&time[j]); sum+=time[j]; } memset(opt,0,sizeof(opt)); for (j=1;j<=s[i];j++) for (k=sum/2;k>=time[j];k--) opt[k]=max(opt[k],opt[k-time[j]]+time[j]); ans+=sum-opt[sum/2]; } printf("%d\n",ans); return 0; }
6.POJ 1745 Divisibility(http://poj.org/problem?id=1745)
// 设 f[i][j]表示前i个数通过一系列加减运算后余数能否为j。 // 初始化f为0, 然后f[1][a[1]%k] = 1 // if(f[i - 1][j]) // f[i][(j + a[i])%k] = 1; // f[i][(j - a[i])%k] = 1; (i=2~n,j=0~k-1) #include <stdio.h> #define maxn 10005 int f[maxn][105]={0}; int main() { int n,k,i,j,a[maxn]; scanf("%d%d",&n,&k); for (i=1; i<=n;i++) { scanf("%d",&a[i]); a[i]%=k; } f[1][((a[1] % k) + k) % k] = 1; for (i = 2; i <= n; i++) { for (j = 0; j<k; j++) { if (f[i-1][j]) { f[i][((j + a[i]) % k + k) % k] = 1; f[i][((j - a[i]) % k + k) % k] = 1; } } } if (f[n][0]) printf("Divisible\n"); else printf("Not divisible\n"); return 0; }
7.POJ 1837 Balance(http://poj.org/problem?id=1837)
// 一个天平左右臂各长为15,给出天平上C个挂钩的位置,再给出G个砝码的重量, // 问有多少种方法能使这个天平保持平衡。 // 设f[i][j]表示把前i个物品全部挂上时使天平达到平衡度为j的方案数, // 则状态转移方程为: f[i][j+w[i]*c[k]]+=f[i-1][j] ; // 将g个挂钩挂上的极限值:15*25*20==7500 // 那么在有负数的情况下是-7500~~7500,以0为平衡点 // 可以将平衡点往右移7500个单位,范围就是0~~15000 。这样就好处理多了 #include <stdio.h> #include <string.h> int f[21][15005]; int main() { int c,g,i,j,k,w[25],a[25]; scanf("%d%d",&c,&g); for (i=1;i<=c;i++) scanf("%d",&w[i]); for (i=1;i<=g;i++) scanf("%d",&a[i]); memset(f,0,sizeof(f)); f[0][7500]=1; for (i=1;i<=g;i++) for (j=0;j<=15000;j++) { if (f[i-1][j]!=0) for (k=1;k<=c;k++) f[i][j+a[i]*w[k]]+=f[i-1][j]; } printf("%d\n",f[g][7500]); return 0; }
8.POJ 3459 Projects(http://poj.org/problem?id=3459)
#include <stdio.h> #include <string.h> int max(int a,int b) { return a>b?a:b; } int main() { int f[110][110]; // f[i][j]表示前i个工程,分配j个人完成的最大期望利润 int p[110][110]; int cost[110][110]; int t; scanf("%d",&t); while (t--) { int m,n,c; scanf("%d%d%d",&m,&n,&c); int i,j,k; for (i=1;i<=m;i++) for (j=1;j<=n+2;j++) scanf("%d",&p[i][j]); for (i=1;i<=m;i++) for (j=0;j<=n;j++) cost[i][j]=p[i][j]*(p[i][n+1]-c*j)-(100-p[i][j])*p[i][n+2]; for (i=1;i<=n;i++) f[0][i]=-100*c*i; for (i=1;i<=m;i++) f[i][0]=-1*p[i][n+2]*100+f[i-1][0]; for (i=1;i<=m;i++) for (j=1;j<=n;j++) { f[i][j]=-0x7f7f7f7f; for (k=0;k<=j;k++) f[i][j]=max(f[i][j],f[i-1][j-k]+cost[i][k]); } int ans=-0x7f7f7f7f; for (i=0;i<=n;i++) if (ans<f[m][i]) ans=f[m][i]; printf("%d\n",ans); for (i=0;i<=n;i++) if (f[m][i]==ans) printf("%d ",i); printf("\n"); } return 0; }
9.POJ 3624 Charm Bracelet(http://poj.org/problem?id=3624)
#include <stdio.h> int max(int a,int b) { if (a>b) return a; else return b; } int main() { int opt[12885]={0}; int w[3405],d[3405]; int n,m; scanf("%d%d",&n,&m); int i,j; for (i=1;i<=n;i++) { scanf("%d%d",&w[i],&d[i]); } for (i=1;i<=n;i++) for (j=m;j>=w[i];j--) { opt[j]=max(opt[j],opt[j-w[i]]+d[i]); } printf("%d\n",opt[m]); return 0; }
10.POJ 3628 Bookshelf 2(http://poj.org/problem?id=3628)
#include <stdio.h> int max(int a,int b) { if (a>b) return a; else return b; } int f[20000005]={0}; int main() { int h[25]; int n,b; scanf("%d%d",&n,&b); int i,j; int sum=0; for (i=1;i<=n;i++) { scanf("%d",&h[i]); sum+=h[i]; } for (i=1;i<=n;i++) for (j=sum;j>=h[i];j--) { f[j]=max(f[j],f[j-h[i]]+h[i]); } int ans; for (i=1;i<=sum;i++) if (f[i]>=b) { ans=f[i]-b; break; } printf("%d\n",ans); return 0; }