贪心算法(二)
例6:排队打水问题
有N 个人排队到R 个水龙头去打水,他们装满水桶的时间为T1,T2,…,Tn 为整数且各不相等,应如何安排他们的打水顺序才能使他们花费的时间最少?
分析:由于排队时,越靠前面的计算的次数越多,显然越小的排在越前面得出的结果越小(可以用数学方法简单证明,这里就不再赘述),所以这道题可以用贪心法解答,基本步骤:
(1)将输入的时间按从小到大排序;
(2)将排序后的时间按顺序依次放入每个水龙头的队列中;
(3)统计,输出答案。
【样例输入】
4 2 {4人打水,2个水龙头}
2 6 4 5 {每个打水时间}
【样例输出】
23 {总共花费时间}
参考程序主要框架如下:
1 对数组a排序;
2 Fillchar(S,Sizeof(S),0);
3 J:=0; Min:=0;
4 For I:=1 To N Do {用贪心法求解}
5 Begin
6 Inc(J);
7 If J=R+1 Then J:=1;
8 S[J]:=S[J]+A[I];
9 Min:=Min+S[J];
10 End;
11 Writeln(Min); {输出解答}
例7:均分纸牌(NOIP2002)
有 N 堆纸牌,编号分别为 1,2,…, N。每堆上有若干张,但纸牌总数必为 N 的倍数。可以在任一堆上取若干张纸牌,然后移动。
移牌规则为:在编号为 1 堆上取的纸牌,只能移到编号为 2 的堆上;在编号为 N 的堆上取的纸牌,只能移到编号为 N-1 的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。
现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。
例如 N=4,4 堆纸牌数分别为:
① 9 ② 8 ③ 17 ④ 6
移动3 次可达到目的:
从 ③ 取 4 张牌放到 ④ (9 8 13 10) -> 从 ③ 取 3 张牌放到 ②(9 11 10 10)-> 从 ② 取 1 张牌放到①(10 10 10 10)。
[输 入]:
键盘输入文件名。文件格式:
N(N 堆纸牌,1 <= N <= 100)
A1 A2 … An (N 堆纸牌,每堆纸牌初始数,l<= Ai <=10000)
[输 出]:
输出至屏幕。格式为:所有堆均达到相等时的最少移动次数。
【样例输入】(a.in)
4
9 8 17 6
【样例输出】(a.out)
3
【算法分析】
如果你想到把每堆牌的张数减去平均张数,题目就变成移动正数,加到负数中,使大家都变成0,那就意味着成功了一半!拿例题来说,平均张数为10,原张数9,8,17,6,变为-1,-2,7,-4,其中没有为0 的数,我们从左边出发:要使第1 堆的牌数-1 变为0,只须将-1 张牌移到它的右边(第2 堆)-2中;结果是-1 变为0,-2 变为-3,各堆牌张数变为0,-3,7,-4;同理:要使第2 堆变为0,只需将-3移到它的右边(第3堆)中去,各堆牌张数变为0,0,4,-4;要使第3 堆变为0,只需将第3 堆中的4 移到它的右边(第4 堆)中去,结果为0,0,0,0,完成任务。每移动1 次牌,步数加1。也许你要问,负数张牌怎么移,不违反题意吗?其实从第i 堆移动-m 张牌到第i+1 堆,等价于从第i+1 堆移动m 张牌到第i 堆,步数是一样的。
如果张数中本来就有为0 的,怎么办呢?如0,-1,-5,6,还是从左算起(从右算起也完全一样),第1 堆是0,无需移牌,余下与上相同;再比如-1,-2,3,10,-4,-6,从左算起,第1 次移动的结果为0,-3,3,10,-4,-6;第2 次移动的结果为0,0,0,10,-4,-6,现在第3 堆已经变为0 了,可节省1 步,余下继续。
参考程序主要框架如下:
1 ave:=0;step:=0;
2 for i:=1 to n do
3 begin
4 read(a[i]); inc(ave,a[i]); {读入各堆牌张数,求总张数ave}
5 end;
6 ave:=ave div n; {求牌的平均张数ave}
7 for i:=1 to n do a[i]:=a[i]-ave; {每堆牌的张数减去平均数}
8 i:=1;j:=n;
9 while (a[i]=0) and (i<n) do inc(i); {过滤左边的0}
10 while (a[j]=0) and (j>1) do dec(j); {过滤右边的0}
11 while (i<j) do
12 begin
13 inc(a[i+1],a[i]); {将第i 堆牌移到第i+1 堆中去}
14 a[i]:=0; {第i 堆牌移走后变为0}
15 inc(step); {移牌步数计数}
16 inc(i); {对下一堆牌进行循环操作}
17 while (a[i]=0) and (i<j) do inc(i);{过滤移牌过程中产生的0}
18 end;
19 writeln(step);
点评:基本题(较易) 本题有3 点比较关键:一是善于将每堆牌数减去平均数,简化了问题;二是要过滤掉0(不是所有的0,如-2,3,0,-1 中的0 是不能过滤的);三是负数张牌也可以移动,这是辩证法(关键中的关键)。
例8:拦截导弹问题。(NOIP1999T1的第一问)
某国为了防御敌国的导弹袭击,开发出一种导弹拦截系统,但是这种拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭,由于该系统还在试用阶段。所以一套系统有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度不大于30000 的正整数)。计算要拦截所有导弹最小需要配备多少套这种导弹拦截系统。
● 输入数据
导弹数n和n颗依次飞来的高度(1≤n≤1000).
● 输出数据
要拦截所有导弹最小配备的系统数k。
【算法分析】
按照题意,被一套系统拦截的所有导弹中,最后一枚导弹的高度最低。设:k为当前配备的系统数;L[k]为被第k套系统拦截的最后一枚导弹的高度,简称系统k的最低高度(1≤k≤n)。我们首先设导弹1被系统1所拦截(k←1,L[k]←导弹1的高度)。然后依次分析导弹2,……,导弹n的高度。
若导弹I的高度高于所有系统的最低高度,则断定导弹I不能被这些系统所拦截,应增设一套系统来拦截导弹I(k←k+1,L[k]←导弹i的高度);若导弹I低于某些系统的最低高度,那么导弹I均可被这些系统所拦截。究竟选择哪个系统拦截可使得配备的系统数最少,我们不妨采用贪心策略,选择其中最低高度最小(即导弹I的高度与系统最低高度最接近)的一套系统P(L[p]=min{L[j]|L[j]>导弹I 的高度};L[p]←导弹I 的高度)(i≤j≤k)。这样可使得一套系统拦截的导弹数尽可能增多。
依次类推,直至分析了n枚导弹的高度为止。此时得出的k便为应配备的最少系统数。
参考程序主要框架如下:
1 k:=1;L[1]:=导弹1 的高度;
2 for i:=2 to n do
3 begin
4 p:=0;
5 for j:=1 to k do
6 if (L[j]>=导弹I 的高度)and((p=0)or(L[j]<L[p])) then p:=j;
7 if p=0 then begin k:=k+1;L[k]:= 导弹I 的高度; end //导弹I不能被拦截
8 else L[p]:= 导弹I 的高度;
9 end;
10 输出应配备的最少系统数K。
应用贪心法的关键——正确性证明
1、反证法
用贪心的策略,依次构造出一个解S1,假设最优解S2不同于S1,可以证明是矛盾的,从而得出S1就是最优解。
例9、最大整数(max.???)
此题为NOIP1998提高组第2题联接数
[问题描述]
设有n(n<=20)个正整数(longint范围以内),将它们联接成一排,组成一个最大的多位整数。
例如:n=3时,3个整数13,312,343联接成的最大整数为:34331213。
又如:n=4时,4个整数7,13,4,246联接成的最大整数为:7424613。
[输入格式]
输入文件共两行,第一行一个整数n,第二为n个正整数,每个数之间用一个空格隔开。
[输出格式]
输出文件仅一行,表示联接成的多位整数。
[算法分析]
本例因为涉及将两个自然数连接起来的问题,采用字符串来处理比较方便。首先我们自然会想到大的字符串应该排在前面,因为如果A与B是两个由数字字符构成的字符串且A>B,一般情况下有:A+B>B+A,但是当A=B+C时,按字符串的大小定义有A>B,些时有可能出现A+B<B+A的情况,如A=‘121’,B=‘12’,则A+B=‘12112’,B+A=‘12121’,A+B<B+A。为了解决这个问题,我们根据题意引进另一种字符串比较办法,将A+B与B+A相比较,如果前者大于后者,则认为A>B,按这一定义将所有的数字字符串从大到小排序后连接起来所得到的数字字符串即是问题的解。排序时先将所有字符串中的最大值选出来存在数组的第一个元素中,再从第二至最后一个元素中最大的字符串选出来存在数组的第二个元素中,直到从最后二个元素中选出最大的字符串存在数组的倒数第二个元素中为止。
补充说明:按本题定义的字符串的大小定义是有序的,即如果A+B≥B+A,B+C≥C+B,则一定有A+C≥C+A。证明方法如下:
引理:记nA为n个字符串A按字符串加法运算规则相加之和,则由A+B≥B+A可推导出nA+mB≥mB+nA,其中m,n为任意的自然数。用反证法可证明反过来也成立。
设la为字符串A的长度,lb为字符串B的长度,lc为字符串C的长度,再设n=lb*lc,m=la*lc,k=la*lb,则nA,mB,kC三个字符串等长,根据引理有:nA+mB≥mB+nA,mB+kC≥kC+mB,从而得到nA≥mB≥kC,所以nA+kC≥kC+nA,A+C≥C+A。
要使n个字符串拼接起来后得到一个最大的字符串和式,则一定要将按上述定义最大的字符串放在第一个,否则必可通过将最大的字符串与它左侧的字符串交换得到更大的字符串和式。
2、 构造法
根据描述的算法,用贪心的策略,依次构造出一个解,可证明一定是合法的解。即用贪心法找可行解。
例10、取火柴游戏(match.???)
[问题描述]
输入k及k个整数n1,n2,……,nk,表示有k堆火柴棒,第i堆火柴棒的根数为ni;接着便是你和计算机对弈游戏,取的规则如下:每次可以从一堆中取走若干根火柴,也可以一堆全部取走,但不允许跨堆取,也不允许不取。
谁取走最后一根火柴为谁胜利。
例如:k=2,n1=n2=2,A代表你,P代表计算机,若决定A先取:
A:(2,2)→(1,2) {从一堆中取一根}
P:(1,2)→(1,1) {从另一堆中取一根}
A:(1,1)→(1,0)
P:(1,0)→(0,0) {P胜利}
如果决定A后取:
P:(2,2)→(2,0)
A:(2,0)→(0,0) {A胜利}
又如k=3,n1=1,n2=2,n3=3,A决定后取:
P:(1,2,3)→(0,2,3)
A:(0,2,3)→(0,2,2)
A已将游戏归结为(2,2)的情况,不管P如何取A都必胜。
编一个程序,在给出初始状态之后,判断是先取必胜还是先取必败,如果是先取必胜,请输出第一次该如何取。如果是先取必败,则输出“lose”。
[输入输出样例1]
match.in
3
3 6 9
match.out
4 3 {表示第一次从第3堆取4个出来,必胜}
3 6 5 {第一次取后的状态}
[输入输出样例2]
match.in
4
15 22 19 10
match.out
lose {先取必败}
[算法分析]
从问题的描述分析,可以将问题中的n堆火柴棒抽象为n个非负整数,而每取一次火柴棒可抽象为使其中的一个自然数变小,当所有的数都变为0时,游戏结束,最后一次取火柴棒的人为胜方。
当n较小,且n堆火柴棒也都较小时,可使用递推的方法来处理这个问题,具体做法是从终了状态(全零)反推出初始状态的值是先取必胜还是先取必败,因为某一状态的值可以从它的所有的取一次后的下一状态得到,如果某状态的所有的下一状态都为先取必败,则这一状态为先取必胜,否则为先取必败。
但当k和ni都很大时,上述方法很难行的通,这时为了解决这个问题,首先引进关于n个非负整数的奇偶状态的定义,如果把n个非负整数都化成二进制数,然后对n个二进制数按位相加(不进行进位),若每一位相加的结果都为偶数,则称这n个非负整数的状态为偶状态,否则称之为奇状态。可以证明:任何一个偶状态在某一个数变小后一定成为奇状态,而对任何一个奇状态,必定可以通过将某一个数的值变小,使得改变后的状态成为偶状态。前一种情况是显然的,因为一个数变小以后其对应的二进制数至少有一位发生改变,这一位的改变就破坏了原来的偶状态。后一种情况可以通过构造的方法来证明,首先对任何一个奇状态,从高位向低位寻找到第一位按位加之和为奇数的二进制位,设这一位为第k位,则n个数的对应的二进制数中至少存在一个数,其第k位为1,将这个二进制数的第k位变成0,则所有二进制数的第k位上的数字之和就变成了偶数。然后再对这个数的比k位低的所有位作如下调整:如果所有二进制数在该位按位加之和为偶数,则不改变该位的值,否则改变该数在该位的值,若原来的值为0,则改为1,若原来的值为1,则改为0,这样就构造出了一个偶状态,并且被改变的那个数一定变小了,因为这个数被改变的所有二进制位中的最高位从1变成了0。
如n=3时,三堆火柴棒的数量分别为3,6,9,则3=(0011)2,6=(0110)2,9=(1001)2,最高位之和为1,其中9对应的二进制数的最高位为1,将其变为0,次高位之和也是1,9对应的二进制数的次高位为0,根据证明过程将其变为1,最后二位数字之和均为偶数,无需作任何改变,这样9=(1001)2被变成了(0101)2=5,显然,3=(0011)2,6=(0110)2,5=(0101)2是一个偶状态。
有了前面的分析,一种贪心算法就出来了。程序中用n个包含16个元素的数组(线性表)来存放对每个非负整数对应的二进制数,如b[i,0]存放第i个数的最低位,n个数的状态取决于它们对应的二进制数的各位数字之和的奇偶性,而各位数字之和的奇偶性只需用0和1来表示,0表示偶,1表示奇。最后的状态(全0)为偶状态,所以开始状态为偶状态时,先取必败,因为先取后局面变成了奇状态,后取方一定可将字取成偶状态,直至取光为止。反之则先取必胜。
例11、雇佣计划(employ.???)
[问题描述]
一位管理项目的经理想要确定每个月需要的工人,他当然知道每月所需的最少工人数。当他雇佣或解雇一个工人时,会有一些额外支出。一旦一个工人被雇佣,即使他不工作,他也将得到工资。这位经理知道雇佣一个工人的费用,解雇一个工人的费用和一个工人的工资。现他在考虑一个问题:为了把项目的费用控制在最低,他将每月雇佣或解雇多少个工人。
[输入格式]
输入文件含有三行。第一行为月数n(不超过12)。第二行含雇佣一个工人的费用,一个工人的工资和解雇一个工人的费用(≤100)。第三行含n个数,分别表示每月最少需要的工人数(≤1000)。每个数据之间有一个空格隔开。
[输出格式]
输出文件仅一行,表示项目的最小总费用。
[输入样例]
3
4 5 6
10 9 11
[输出样例]
199
[算法分析]
我们从第一个月开始,逐月计算现有工人数,先给这些工人发放工资。如果雇佣了新工人,则必须给新上岗人员发放雇佣费用;如果削减了部分工人,则必须给下岗人员发放解雇费用。当月发放的工资+雇佣(或解雇)费用构成了一个月的总费用。我们从第一个月开始,逐月累计项目总费用,直至计算出n个月的总费用为止。问题是怎样将这笔费用控制在最低?设mincost表示最小费用和,初始时mincost=0;now表示现有工人数,初始时now=0;min[i]表示第i个月所需要的最少工人数(1≤i≤n);n表示月数;f表示解雇费用;s表示工资;h表示雇佣费用。则我们需要解决下面的两个问题:
1、怎样在当月工人数不足的情况下确定雇佣方案
如果第i个月的所需最少人数min[i]大于现有工人数now,则需要雇佣工人。为了尽可能少地减少雇佣费用,我们不妨雇佣(min[i]-now)个工人,使得第i个月的工人数正好为min[i];如果min[i]=now,则维持现有工人数now不变。
算法思想如下:
1 if min[i]>now then begin
2 mincost:=mincost+h*(min[i]-now);now:=min[i];
3 end;
4 mincost:=mincost+now*s; { 显然,这样的雇佣费用支出是最节约的 }
2、怎样在当月工人数多余的情况下确定解雇方案
如果现有工人数now大于第i个月最少需要的工人数min[i],则需要解雇一部分工人,最佳方案是否一定是解雇(now-min[i])个工人呢?不是的。例如对于示例中的数据,依上述方法计算:
第1个月雇佣10人:mincost=mincost+h*(min[i]-now)+s*min[i]
=0+4*10+5*10
=90 now=10
第2个月解雇1人,使得现有工人数为9人:
mincost=mincost+f*(now-min[2])+s*min[2]
=90+6*1+5*9
=141 now=9
第3个月雇佣2人,使得现有工人数为11人:
mincost=mincost+h*(min[3]-now)+s*min[3]
=141+4*2+5*11
=204 now=11
显然,该雇佣计划的总费用(204)并不是最少的,因为如果第2个月不解雇1人,仍维持10人,第3个月再雇佣1人,这种情况下的总费用为(4*10+5*10)+(5*10)+(4*1+5*11)=90+50+59=199,即为样例输出。由此看出,解雇的人数应比(now-min[i])少一些,那么少多少呢?我们采取这样的贪心策略去确定:尽可能少地解雇工人,并且在工资支出合理的前提下尽可能使现有工人数维持在一个最长时间内,以减少雇佣和解雇的额外支出。
算法思想如下:
1 在min[i]~min[n]间按最少需要人数递增的顺序,将月份排成y1,……,yn-i+1;
2 for j:=n-i downto 1 do
3 if (min[yj]<now) and (f+h*ord(yj+1<=n)<s*(yj+1-i))4 then begin
5 mincost:=mincost+f*(now-min[yj]);now:=min[yj];
6 and
7 mincost:=mincost+s*now;
我们从i=1开始,依上述办法逐月累计费用总和,直至i=n为止。此时的费用总和最少。例如对于示例中的数据,依上述方法计算:
第1个月雇佣10人,费用总和为90;
第2个月维持现有人数不变,费用总和为90+5*10=140;
第3个月雇佣1人,工人数增至11,费用总和为140+5*11-14=199。
显然,这个答案与样本输出一致。结合上述分析得出本题总的算法框架如下:
1 mincost:=0;now:=0; {初始化}
2 for i:=1 to n do
3 begin
4 if min[i]>now
5 then 在现有人数不足情况下确定第i个月的雇佣方案并累计最小费用mincost;
6 else 在现有人数多余情况下确定第i个月的解雇方案并累计最小费用mincost;
7 end;
8 输出项目的最小费用mincost;
3、 调整法
用贪心的策略,依次构造出一个解S1。假设最优解S2不同于S1,找出不同之处,在不破坏最优性的前提下,逐步调整S2,最终使其变为S1。从而S1也是最优解。
例12、排队接水(water.???)
COGS_1152可做
[问题描述]
有n个人在一个水龙头前排队接水,假如每个人接水的时间为Ti,请编程找出一种这n个人排队的顺序,使得n个人的平均等待时间最小。
[问题输入]
输入文件共两行,第一行为n;第二行分别表示第1个人到第n个人每人的接水时间T1,T2,……,Tn,每个数据之间有1个空格。
[问题输出]
输出文件有两行,第一行为一种排队顺序,即1到n的一种排列;第二行为这种排列方案下的平均等待时间(输出结果精确到小数点后两位)。
[输入样例]
10
56 12 1 99 1000 234 33 55 99 812
[输出样例]
3 2 7 8 1 4 9 6 10 5
291.90