第十届全国青少年信息学奥林匹克联赛
(NOIP2004)复赛提高组解题报告
(提高组 3小时完成)
一、津津的储蓄计划(Save.pas/dpr/c/cpp).
【问题描述】
津津的零花钱一直都是自己管理。每个月的月初妈妈给津津300元钱,津津会预算这个月的花销,并且总能做到实际花销和预算的相同。
为了让津津学习如何储蓄,妈妈提出,津津可以随时把整百的钱存在她那里,到了年末她会加上20%还给津津。因此津津制定了一个储蓄计划:每个月的月初,在得到妈妈给的零花钱后,如果她预计到这个月的月末手中还会有多于100元或恰好100元,她就会把整百的钱存在妈妈那里,剩余的钱留在自己手中。
例如11月初津津手中还有83元,妈妈给了津津300元。津津预计11月的花销是180元,那么她就会在妈妈那里存200元,自己留下183元。到了11月月末,津津手中会剩下3元钱。
津津发现这个储蓄计划的主要风险是,存在妈妈那里的钱在年末之前不能取出。有可能在某个月的月初,津津手中的钱加上这个月妈妈给的钱,不够这个月的原定预算。如果出现这种情况,津津将不得不在这个月省吃俭用,压缩预算。
现在请你根据2004年1月到12月每个月津津的预算,判断会不会出现这种情况。如果不会,计算到2004年年末,妈妈将津津平常存的钱加上20%还给津津之后,津津手中会有多少钱。
【输入文件】
输入文件save.in包括12行数据,每行包含一个小于350的非负整数,分别表示1月到12月津津的预算。
【输出文件】
输出文件save.out包括一行,这一行只包含一个整数。如果储蓄计划实施过程中出现某个月钱不够用的情况,输出-X,X表示出现这种情况的第一个月;否则输出到2004年年末津津手中会有多少钱。
【样例输入1】
290
230
280
200
300
170
340
50
90
80
200
60
【样例输出1】
-7
【样例输入2】
290
230
280
200
300
170
330
50
90
80
200
60
【样例输出2】
1580
【分析】本题实在没什么好说的。
【程序清单】
program save;
var a,b,c,d,e:integer;i:byte;
begin
assign(input,'save.in');
reset(input);
assign(output, 'save.out');
rewrite(output);
a:=0;b:=0;c:=0;d:=0;e:=0;
for i:=1 to 12 do begin
read(input,a);{a表示每月预算}
b:=300+e;{b表示月初手中的钱,e表示上月月底手中余下的钱}
c:=b-a;{c表示到月底花销后剩余的钱}
if c<0 then begin
write(output,'-',i);
close(output);
exit;
end;
e:=c mod 100;{e表示月底手中余下的钱}
d:=d+c-e; {d表示存到妈妈那里的钱}
end;
close(input);
d:=d div 10*12+e;
write(output,d);
close(output);
end.
【点评】超简单,算是一颗定心丸吧。
二、 合并果子 (fruit.pas/dpr/c/cpp)
【问题描述】
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。
【输入文件】
输入文件fruit.in包括两行,第一行是一个整数n(1<=n<=10000),表示果子的种类数。第二行包含n个整数,用空格分隔,第i个整数ai(1<=ai<=20000)是第i种果子的数目。
【输出文件】
输出文件fruit.out包括一行,这一行只包含一个整数,也就是最小的体力耗费值。输入数据保证这个值小于231。
【样例输入】
3
1 2 9
【样例输出】
15
【数据规模】
对于30%的数据,保证有n<=1000:
对于50%的数据,保证有n<=5000;
对于全部的数据,保证有n<=10000。
【分析】用贪心法。先将果子数排序,取其中最小的两堆合并,得到一个新堆;再排序,再取其中最小的两堆合并……直到只剩一堆。为尽快出解,排序的速度显得格外重要,可用快速排序算法。
【程序清单】
program fruit;
var n,i,j,k,min,mid,max:word;
a:array[1..10000]of longint;{存放各堆果子数}
p,temp:longint;
procedure QuickSort(l, r:Integer);{对数组a快速排序(升序)}
var i,j,x,y:integer;
begin
i:=l; j:=r; x:=a[(l+r) DIV 2];
repeat
while a[i]<x do inc(i);
while x<a[j] do dec(j);
if i<=j then begin
y:=a[i]; a[i]:=a[j]; a[j]:=y;
inc(i);dec(j);
end;
until i>j;
if l<j then QuickSort(l,j);
if i<r then QuickSort(i,r);
end;
begin
assign(input,'fruit.in');
reset(input);
assign(output, 'fruit.out');
rewrite(output);
readln(input,n);
for i:=1 to n do read(input,a[i]);
close(input);
QuickSort(1,n);{用快速排序法对果子堆按升序排列}
p:=0;
for i:=1 to n-2 do begin
inc(a[i+1],a[i]);{把第i堆合并到第i+1堆}
inc(p,a[i+1]);{累加合并所耗体力值}
{用二分法将刚合并得到的新堆插入到相应位置,使整个序列再次成为升序序列}
min:=i+2;max:=n;
while min<=max do begin
mid:=(min+max) div 2;
if a[mid]=a[i+1] then begin
j:=mid;break;
end;
if a[mid]<a[i+1] then min:=mid+1 else max:=mid-1;
end;
if min>max then j:=min;
temp:=a[i+1];
for k:=i+1 to j-2 do a[k]:=a[k+1];
a[j-1]:=temp;
end;
p:=p+a[n]+a[n-1];{合并最后两堆}
write(output,p);
close(output);
end.
【点评】“石子合并”的翻新,有意去掉了“相邻两堆才能合并”的条件,降低了难度,于是“动态规划”也就降为“贪心”了。
三、 合唱队形 (chorus.pas/dpr/c/cpp)
【问题描述】
N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。
合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2…,K,他们的身高分别为T1,T2,…,TK, 则他们的身高满足T1<...<Ti,Ti>Ti+1>…>TK(1<=i<=K)。
你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
【输入文件】
输入文件chorus.in的第一行是一个整数N(2<=N<=100),表示同学的总数。第二行有n个整数,用空格分隔,第i个整数Ti(130<=Ti<=230)是第i位同学的身高(厘米)。
【输出文件】
输出文件chorus.out包括一行,这一行只包含一个整数,就是最少需要几位同学出列。
【样例输入】
8
186 186 150 200 160 130 197 220
【样例输出】
4
【数据规模】
对于50%的数据,保证有n<=20;
对于全部的数据,保证有n<=100。
【分析】可枚举第i(i=1,2,…,N)位同学为“最高峰”,求出此时的最长合唱队形,比较N次枚举结果,取其中最长合唱队形。
【程序清单】
program chorus;
var
i,j,k,n,Max,MaxL,MaxR,Max1:byte;
T,b:array[1..100]of byte;
begin
assign(input, 'chorus.in');
reset(input);
assign(output, 'chorus.out');
rewrite(output);
readln(input,n);{读取人数}
for i:=1 to n do read(input,T[i]);{读取身高}
close(input);
Max:=0;{ Max记录最长合唱队形的长度}
for i:=1 to n do begin{枚举第i个同学为“最高峰”}
for j:=1 to n do b[j]:=1;{数组b记录本人在合唱队形中的长度}
for j:=i-1 downto 1 do{左边的同学要成为严格升序列}
if T[j]<T[i] then begin
Max1:=b[i];
for k:=j+1 to i do
if (T[k]>T[j]) and (b[k]>Max1) then Max1:=b[k];
b[j]:=Max1+1;
end;
MaxL:=1;
for j:=1 to i-1 do
if b[j]>MaxL then MaxL:=b[j];{计算左边升序列的最大长度}
for j:=i+1 to n do{右边的同学要成为严格降序列}
if T[j]<T[i] then begin
Max1:=b[i];
for k:=i+1 to j-1 do
if (T[k]>T[j]) and (b[k]>Max1) then Max1:=b[k];
b[j]:=Max1+1;
end;
MaxR:=1;
for j:=i+1 to n do
if b[j]>MaxR then MaxR:=b[j]; {计算右边降序列的最大长度}
if MaxL+MaxR-1>Max then Max:=MaxL+MaxR-1;{计算合唱队形的最大长度}
end;
write(output,n-Max);
close(output);
end.
【点评】“最长不下降序列”的翻新,只是队形上略加变化,但仍未能有效提升本题的难度。
四、虫食算 (alpha.pas/dpr/c/cpp)
所谓虫食算,就是原先的算式中有一部分被虫子啃掉了,需要我们根据剩下的数字来判定被啃掉的字母。来看一个简单的例子:
43#98650#45
+ 8468#6633
44445506978
其中#号代表被虫子啃掉的数字。根据算式,我们很容易判断:第一行的两个数字分别是5和3,第二行的数字是5。
现在,我们对问题做两个限制:
首先,我们只考虑加法的虫食算。这里的加法是N进制加法,算式中三个数都有N位,允许有前导的0。
其次,虫子把所有的数都啃光了,我们只知道哪些数字是相同的,我们将相同的数字用相同的字母表示,不同的数字用不同的字母表示。如果这个算式是N进制的,我们就取英文字母表午的前N个大写字母来表示这个算式中的0到N-1这N个不同的数字:但是这N个字母并不一定顺序地代表0到N-1)。输入数据保证N个字母分别至少出现一次。
BADC
+ CBDA
DCCC
上面的算式是一个4进制的算式。很显然,我们只要让ABCD分别代表0123,便可以让这个式子成立了。你的任务是,对于给定的N进制加法算式,求出N个不同的字母分别代表的数字,使得该加法算式成立。输入数据保证有且仅有一组解,
【输入文件】
输入文件alpha.in包含4行。第一行有一个正整数N(N<=26),后面的3行每行有一个由大写字母组成的字符串,分别代表两个加数以及和。这3个字符串左右两端都没有空格,从高位到低位,并且恰好有N位。
【输出文件】
输出文件alpha.out包含一行。在这一行中,应当包含唯一的那组解。解是这样表示的:输出N个数字,分别表示A,B,C……所代表的数字,相邻的两个数字用一个空格隔开,不能有多余的空格。
【样例输入】
5
ABCED
BDACE
EBBAA
【样例输出】
1 0 3 4 2
【数据规模】
对于30%的数据,保证有N<=10;
对于50%的数据,保证有N<=15;
对于全部的数据,保证有N<=26。
【分析】基本算法是搜索加剪枝。可能解集为n!种,必须施以强有力的剪枝,才可能在1秒内出解。
拿样例来说,如果按常规思想,对序列ABCDE的取值进行全排列,可选升序:
01234,01243,…,43201,43210;
也可选降序:
43210,43201,…,01243,01234
则很难实施优化,往往只能逐一判断它是否是解,绝大部分数据必会超时(但可通过第1,2,9组数据)。
我们还是来研究一下加法算式:
如果从低位出发,最低位上的算式是D+E=A 或者 D+E=N+A;可以写成一个统一的法则:
x[1]=(D+E+x[0]) div N,其中x[1]表示第1位(最低位)加法所产生的向高位(第2位)的进位;x[0]=0;
A=(D+E+x[0]) mod N,此式必须成立,则当前序列才有可能是解
一般地,有:
x[i]=(value[str[1,N-i+1]]+value[str[2,N-i+1]]+x[i-1]) div N;(i=1,2,…,N)
其中x[i]表示第i位加法产生的进位;str1,str2,str3分别为算式的被加数、加数、和数三个字符串
判断法则:value[str[3,N-i+1]]=(value[str[1,N-i+1]]+value[str[2,N-i+1]]+x[i-1]) mod N
问题是:如果判断法则不成立,怎样剪枝?
显然,若判断法则不成立,应立即更改value[str[3,N-i+1]], value[str[1,N-i+1]], value[str[2,N-i+1]]中的某一个值再试。
拿样例来说,对序列ABCDE的值01234,显然判断法则不成立(A<>D+E且A<>(D+E) mod 5),必须更改A,D,E之值,若改D或E的值,效率太低(因为D,E的位置在序列的最后);若改A的值,则极可能漏掉真正解!
办法只有一个,让先出场的字母(如ADE)排在前列!
于是,从低位出发,依次记下出场字母的顺序,对样例是DEACB,若采用升序,则DEACB之值首先为01234,此时,判断法则A=D+E不成立,应更改A的值(A是本轮牵涉到的最后一个字母,用一个指针point(别害怕“指针”,它其实就是一个整数)指示当前字母的位置);在A的值(当前是2)的右边找到比它略大的值作为其新值(新值为3),其余数作升序排列,则形成一个新的可能解:01324;显然这个新的可能解也不满足判断法则A=D+E,再产生下一个可能解:01423;仍不行,此时A的值为4,其右边已经没有比它大的数了,只好让指针左移一位,即point:=point-1,现在开始调整字母E的值,产生新可能解为:02134;现在指针又回到字母A的位置,仍不是解;下一个:02314;不是解,下一个:02413,不行;03124;03214;03412;04123;04213;04312;10234;10324;10423;12034;12304,现在判断法则成立了!
再做第2位上的加法。由于x[1]=0,所以判断法则是E+C=A或E+C=N+A;显然12304不是解;下一个:12403,重新回到第一位的判断,不行;下一个:13024;不行;14023,(第一位的)判断法则成立了!x[1]=1, 再做第2位上的加法,判断法则是E+C+1=A或E+C+1=N+A,不行;14203;14302;……直到42130,它满足:第1位:D+E=N+A;x[1]=1;第2位:E+C+1=N+A,x[2]=1;第3位:C+A+1=N+B,x[3]=1;第4位:B+D+1=N+B,x[4]=1;第5位:A+B+1=E,x[5]=0;成功的标志就是x[n]=0(当然之前赋初值为非0值!)
类似地,可以从高位入手对字母出场顺序进行排列;在产生解的序列时也可以采用降序,所以本题可以形成高位升序、高位降序、低位升序、低位降序4种方案。很难说,哪一种更优,这要取决于具体的测试数据。如果测试数据的结果离某种方案的始发序列近,则这个方案出解自然快。
【程序清单】(低位降序)
program alpha;
var n,i,j,no,temp:byte;ch:char;
value:array['A'..'Z']of byte;{存放各个字母的取值}
x:array[0..26]of 0..1;{存放各位上加法产生的进位}
alp:array[1..26]of char;{存放出场顺序的字母}
num:array[1..26]of byte;{存放各位当前字母的顺序号,即被剪去的枝的根}
num0:array['A'..'Z']of byte;{存放字母的出场顺序号}
set1:set of 'A'..'Z';
str:array[1..3]of string;
procedure next(point:byte);{产生下一个可能解,point是当前字母的顺序号}
var i,j,k,temp,min1,min2:shortint;
begin
min1:=-1;
for i:=point+1 to n do begin{在当前字母右边寻找比它的值略小的字母}
if (value[alp[i]]<value[alp[point]]) and (value[alp[i]]>min1)
then begin min1:=value[alp[i]];min2:=i;end;
end;
if min1=-1 then begin{若当前字母右边已经没有哪个字母的值比它更小}
if point>1 then begin
next(point-1);{以当前字母的左边的字母为新的当前字母,产生新的序列}
exit;end
else halt;{若当前字母已经是第一个出场的字母,则所有可能序列搜索完毕,程序终止}
end
else begin{产生下一个序列}
temp:=value[alp[point]];{当前位置上的值略微减小}
value[alp[point]]:=min1;
value[alp[min2]]:=temp;
for j:=point+1 to n-1 do{余下的位置按降序重排}
for k:=j+1 to n do
if value[alp[j]]<value[alp[k]] then begin
temp:=value[alp[j]];
value[alp[j]]:=value[alp[k]];
value[alp[k]]:=temp;
end;
end;
end;
begin
assign(input,'alpha.in');
reset(input);
assign(output,'alpha.out');
rewrite(output);
readln(input,n);
for i:=1 to 3 do readln(input,str[i]);
close(input);
