第3章NOIP 2015—2020普及组复试题解 从本章开始,一起看一下近几年的复试真题。考虑到书稿篇幅的问题, 本书并不主张对每一个题目开展深入的探讨,以免限制读者的思维。比赛的题目往往会比较灵活,有比较多的解题方法和途径,而在此仅提供一种可行的方法而已,希望读者尽展各自的思维,力求多角度、多方法求解,至于哪个方法是更好的,自然要依据算法评价准则及算法实现的难易度来综合评价。总之,本书的题解只求能起到一个抛砖引玉的作用。 3.1NOIP 2015普及组复试题解 3.1.1T1: 金币 【题目描述】 国王将金币作为工资,发放给忠诚的骑士。第一天,骑士收到一枚金币; 之后两天(第二天和第三天),每天收到两枚金币; 之后三天(第四、五、六天),每天收到三枚金币; 之后四天(第七、八、九、十天),每天收到四枚金币……这种工资发放模式会一直延续下去: 当连续N天每天收到N枚金币后,骑士会在之后的连续N+1天里,每天收到N+1枚金币。 请计算在前K天里,骑士一共获得了多少金币。 【输入】 输入文件只有1行,包含一个正整数K,表示发放金币的天数。 【输出】 输出文件只有1行,包含一个正整数,即骑士收到的金币数。 【样例输入】 6 【样例输出】 14 【解析】 利用循环语句模拟。 【参考代码】 #include <bits/stdc++.h> using namespace std; int k,mon,ans; int main(){ scanf("%d",&k); mon=1; while (k){ if (k>=mon){ ans+=mon*mon;k-=mon; }else{ ans+=mon*k;k=0; } mon++; } printf("%d\n",ans);return 0; } 3.1.2T2: 扫雷游戏 【题目描述】 扫雷游戏是一款十分经典的单机小游戏。在n行m列的雷区中有一些格子含有地雷(称为地雷格),其他格子不含地雷(称为非地雷格)。玩家翻开一个非地雷格时,该格将会出现一个数字——提示周围格子中有多少个是地雷格。游戏的目标是在不翻出任何地雷格的条件下,找出所有的非地雷格。 现在给出n行m列的雷区中的地雷分布,要求计算出每个非地雷格周围的地雷格数。 注: 一个格子的周围格子包括其上、下、左、右、左上、右上、左下、右下8个方向上与之直接相邻的格子。 【输入】 输入文件第一行是用一个空格隔开的两个整数n和m,分别表示雷区的行数和列数。 接下来n行,每行m个字符,描述了雷区中的地雷分布情况。字符'*'表示相应格子是地雷格,字符'?'表示相应格子是非地雷格。相邻字符之间无分隔符。 【输出】 输出文件包含n行,每行m个字符,描述整个雷区。用'*'表示地雷格,用周围的地雷个数表示非地雷格。相邻字符之间无分隔符。 【样例输入】 33 *?? ??? ?*? 【样例输出】 *10 221 1*1 【解析】 循环遍历每个'?',对于每个'?',统计其四周8个点'*'的数量。8个方向可以用数组预处理(参见代码),同时需要注意边界。 【参考代码】 #include <bits/stdc++.h> using namespace std; char s[105][105]; int n,m,a[105][105]; int dx[]={0,0,1,-1,1,1,-1,-1}; //8个方向 int dy[]={1,-1,0,0,1,-1,1,-1}; int main(){ scanf("%d%d",&n,&m); for (int i=1;i<=n;i++){//整行读入字符串 scanf("%s",s[i]+1); } for (int i=1;i<=n;i++){ for (int j=1;j<=m;j++){ if (s[i][j]=='*'){ printf("*"); } else{ int cnt=0; for (int k=0;k<8;k++){ int x=i+dx[k], y=j+dy[k]; if (x>=1 && x<=n && y>=1 && y<=m && s[x][y]=='*'){ cnt++; } } printf("%d",cnt); } } printf("\n"); } return 0; } 3.1.3T3: 求和 【题目描述】 一条狭长的纸带被均匀划分出了n个格子(图31),格子的编号为1~n。每个格子上都染了一种颜色colori(i用[1,m]区间中的一个整数表示),并且写了一个数字numberi。 图31样例输入数据的纸带 定义一种特殊的三元组(x,y,z),其中,x,y,z代表纸带上格子的编号,这里的三元组要求满足以下两个条件。 (1) x, y, z是整数, x<y<z, y-x=z-y; (2) colorx=colorz。 满足上述条件的三元组的分数规定为(x+z)×(numberx+numberz)。整个纸带的分数规定为所有满足条件的三元组的分数的和。这个分数可能会很大,只要输出整个纸带的分数除以10007所得的余数即可。 【输入】 第一行是用一个空格隔开的两个正整数n和m,n表示纸带上格子的个数,m表示纸带上颜色的种类数。 第二行有n个用空格隔开的正整数,第i个数字number表示纸带上编号为i的格子上面写的数字。 第三行有n个用空格隔开的正整数,第i个数字color表示纸带上编号为i的格子染的颜色。 【输出】 共一行,一个整数,表示所求的纸带分数除以10007所得的余数。 【样例输入】 62 553222 221121 【样例输出】 82 【解析】 同种颜色,下标都为奇数的可以两两产生分数,下标都为偶数的可以两两产生分数,所以在做题时,可以根据下标奇偶分为两组分别计算即可。 接下来就要列计算式了。 例如,对于奇数下标(共有k个)的某颜色(x1, num1),(x2, num2), …, (xk, numk),得分为: (x1+x2)×(num1+num2)+(x1+x3)×(num1×num3)+… +(x1+xk)×(num1×numk)+(x2+x3)×(num2+num3)+… +(x2+xk)×(num2×numk)+…+(x(k-1)+xk)×(num(k-1)+numk) 调整一下式子得到: (k-1)×x1×num1+x1×(num2+num3+…+numk)+ (k-1)×x2×num2+x2×(num1+num3+…+numk)+… (k-1)×xk×numk+xk×(num1+num2+…+num(k-1)) 将每行第一部分取一个xi×num调整到后面,就为: (k-2)×x1×num1+x1×(num1+num2+…+numk)+… (k-2)×x2×num2+x2×(num1+num2+…+numk)+… (k-2)×xk×numk+xk×(num1+num2+…+numk) 提取公因式后: (k-2)×(x1×num1+x2×num2+…+xk×numk)+ (x1+x2+…+xk)×(num1+num2+…+num) 化简到这里,基本上可以写程序了。 对于100%的数据,其实m也不是很大,可以用桶k[c]记录第color颜色的数量, sumxn[c]记录第color颜色下标i乘以数字num[i]的累加和,sumx[c]记录第color颜色下标i的累加,sumn[c]记录第color颜色数字num[i]的累加,统计出来之后,就可以直接运算出结果了。 【参考代码】 #include <bits/stdc++.h> using namespace std; const int M=100005; const int P=10007; int n,m,num[M],cor[M],ans; int k1[M],sumxn1[M],sumx1[M],sumn1[M]; int k2[M],sumxn2[M],sumx2[M],sumn2[M]; int main(){ scanf("%d%d",&n,&m); for (int i=1;i<=n;i++){ scanf("%d",&num[i]); } for (int i=1;i<=n;i++){ scanf("%d",&cor[i]); } for (int i=1;i<=n;i++){ if (i&1){//奇数的情况 int c=cor[i]; k1[c]++; sumxn1[c]=(sumxn1[c]+i%P*num[i]%P)%P; sumx1[c]=(sumx1[c]+i)%P; sumn1[c]=(sumn1[c]+num[i])%P; }else{//偶数的情况 int c=cor[i]; k2[c]++; sumxn2[c]=(sumxn2[c]+i%P*num[i]%P)%P; sumx2[c]=(sumx2[c]+i)%P; sumn2[c]=(sumn2[c]+num[i])%P; } } for (int i=1;i<=m;i++){ if (k1[i]>=2)ans=(ans+(k1[i]-2)*sumxn1[i]+sumx1[i]*sumn1[i])%P; if (k2[i]>=2)ans=(ans+(k2[i]-2)*sumxn2[i]+sumx2[i]*sumn2[i])%P; } printf("%d\n",ans); return 0; } 3.1.4T4: 推销员 【题目描述】 阿明是一名推销员,他奉命到螺丝街推销他们公司的产品。螺丝街是一条死胡同,出口与入口是同一个,街道的一侧是围墙,另一侧是住户。螺丝街一共有N家住户,第i家住户到入口的距离为Si米。由于同一栋房子里可以有多家住户,所以可能有多家住户与入口的距离相等。阿明会从入口进入,依次向螺丝街的X家住户推销产品,然后再原路走出去。 阿明每走1米就会积累1点疲劳值,向第i家住户推销产品会积累Ai点疲劳值。阿明是工作狂,他想知道,对于不同的X,在不走多余路的前提下,他最多可以积累多少点疲劳值。 【输入】 第一行有一个正整数N,表示螺丝街住户的数量。 接下来的一行有N个正整数,其中,第i个整数Si表示第i家住户到入口的距离。数据保证S1≤S2≤…≤Sn<10^8。 接下来的一行有N个正整数,其中,第i个整数Ai表示向第i户住户推销产品会积累的疲劳值。数据保证Ai<10^3。 【输出】 输出N行,每行一个正整数,第i行整数表示当X=i时,阿明最多积累的疲劳值。 【样例输入】 5 12345 12345 【样例输出】 15 19 22 24 25 【解析】 本题可以用贪心算法解决。题目希望疲劳值最大,可以按疲劳值从大到小排序,随着X的不断增大,希望可以有更多较大疲劳值的点加入,而距离只要在这些点中取最远距离。 这里可以得出一个结论: 选取的X个点中,至少有X-1个点的疲劳值是取最大的前X-1个值,剩下一个选取距离×2+疲劳值最大的点。 可以这么理解: 如果取疲劳值前X大点中的X-2个,剩下两个点选非前X大的值,那么剩下两个点中至少有一个点的距离是不用计算的,这个点换成疲劳值更大的点,会让答案更大一些。所以X个点中,至少有X-1个点的疲劳值是前X-1大的,剩下一个选择距离×2+疲劳值最大的点。答案要么选前i大疲劳值的点,要么选前i-1大疲劳值的点和距离×2+疲劳值最大的点。 【参考代码】 #include <bits/stdc++.h> using namespace std; int n,sum[100005],pre[100005],last[100005]; //sum[i]表示前i大疲劳值的前缀和 //pre[i]表示前i个点中距离*2的最大值 //last[i]表示点i~n中距离*2+疲劳值的最大值 struct node{ int d,v; //距离和疲劳值 }p[100010]; bool cmp(node x,node y) { return x.v>y.v; } int main(){ scanf("%d",&n); for (int i=1;i<=n;i++){ scanf("%d",&p[i].d); } for (int i=1;i<=n;i++){ scanf("%d",&p[i].v); } sort(p+1,p+1+n,cmp); //按疲劳值从大到小排序 for (int i=1;i<=n;i++){ sum[i]=sum[i-1]+p[i].v; } for (int i=1;i<=n;i++){ pre[i]=max(pre[i-1],2*p[i].d); //前i个中最大距离 } for (int i=n;i>=1;i--){ last[i]=max(last[i+1],2*p[i].d+p[i].v); //后i个中最大值 } for (int i=1;i<=n;i++){ printf("%d\n",max(sum[i]+pre[i],sum[i-1]+last[i])); } return 0; } 3.2NOIP 2016普及组复试题解 3.2.1T1: 买铅笔 【题目描述】 P老师需要去商店买n支铅笔作为小朋友们参加NOIP的礼物。她发现商店一共有 3种包装的铅笔,不同包装内的铅笔数量有可能不同,价格也有可能不同。公平起见,P老师决定只买同一种包装的铅笔。 商店不允许将铅笔的包装拆开,因此P老师可能需要购买超过n支铅笔才够给小朋友们发礼物。 现在P老师想知道,在商店每种包装的数量都足够的情况下,要买够至少n支铅笔最少需要花费多少钱。 【输入】 第一行包含一个正整数n,表示需要的铅笔数量。 接下来3行中,每行用两个正整数描述一种包装的铅笔: 其中,第1个整数表示这种包装内铅笔的数量,第2个整数表示这种包装的价格。 保证所有的7个数都是不超过10000的正整数。 【输出】 1个整数,表示P老师最少需要花费的钱。 【样例输入】 57 22 5030 3027 【样例输出】 54 【解析】 枚举每个包装,判断一下n是不是当前这个包装数量的整倍数,如果是就直接用n/包装数量×每个的价钱,如果不是,就用(n/包装数量+1)×单价,最后再判断最小值。 【参考代码】 #include <bits/stdc++.h> using namespace std; int main(){ int n,ans=100000000,t; scanf("%d",&n); for (int i=1;i<=3;i++){ int a,b; scanf("%d%d",&a,&b); t=((n-1)/a+1)*b; ans=min(ans,t); } printf("%d\n",ans); return 0; } 3.2.2T2: 回文日期 【题目描述】 在日常生活中,通过年、月、日这三个要素可以表示出一个唯一确定的日期。 牛牛习惯用8位数字表示一个日期,其中,前4位代表年份,接下来2位代表月份,最后2位代表日期。显然,一个日期只有一种表示方法,而两个不同的日期的表示方法不会相同。 牛牛认为,一个日期是回文的,当且仅当表示这个日期的8位数字是回文的。现在牛牛想知道: 在他指定的两个日期之间(包含这两个日期本身),有多少个真实存在的日期是回文的。 一个8位数字是回文的,当且仅当对于所有的i(1≤i≤8)从左向右数的第i个数字和第9-i个数字(即从右向左数的第i个数字)是相同的。 例如: 对于2016年11月19日,用8位数字20161119表示,它不是回文的。 对于2010年1月2日,用8位数字20100102表示,它是回文的。 对于2010年10月2日,用8位数字20101002表示,它不是回文的。 每一年中都有12个月份,其中,1、3、5、7、8、10、12月每个月有31天; 4、6、9、11月每个月有30天; 而对于2月,闰年时有29天,平年时有28天。 一个年份是闰年当且仅当它满足下列两种情况其中的一种: (1) 这个年份是4的整数倍,但不是100的整数倍。 (2) 这个年份是400的整数倍。 例如, 以下几个年份都是闰年: 2000、2012、2016。 以下几个年份是平年: 1900、2011、2014。 【输入】 两行,每行包括一个8位数字。 第一行表示牛牛指定的起始日期。 第二行表示牛牛指定的终止日期。 保证datei都是真实存在的日期,且年份部分一定为4位数字,且首位数字不为0。 保证date1一定不晚于date2。 【输出】 一个整数,表示在date1和date2之间有多少个日期是回文的。 【样例输入】 20110101 20111231 【样例输出】 1 【解析】 因为是回文,所以符合条件的日期满足后4位是前4位的逆序,可以枚举后4位,即枚举月份和日期,从而确定前4位,这样保证枚举的日期是回文日期,然后判断是否在题目给定的日期范围内即可。 【参考代码】 #include <bits/stdc++.h> using namespace std; int d1,d2,d,ans; int md[13]={0,31,29,31,30,31,30,31,31,30,31,30,31}; //预先设置每个月的天数,回文日期肯定是闰年 int main(){ scanf("%d%d",&d1,&d2); for (int i=1;i<=12;i++){ for (int j=1;j<=md[i];j++){ int y=(j%10)*1000+(j/10)*100+(i%10)*10+(i/10); d=y*10000+i*100+j; if (d1<=d && d<=d2){ ans++; } } } printf("%d",ans); return 0; } 3.2.3T3: 海港 【题目描述】 小K是一个海港的海关工作人员,每天都有许多船只到达海港,船上通常有很多来自不同国家的乘客。 小K对这些到达海港的船只非常感兴趣,他按照时间记录下了到达海港的每一艘船只情况; 对于第i艘到达的船,他记录了这艘船到达的时间ti (单位: s),船上的乘客数ki,以及每名乘客的国籍xi,1, xi,2,…,xi,k。 小K统计了n艘船的信息,希望你帮忙计算出以每一艘船到达时间为止的24h(24h=86400s)内所有乘船到达的乘客来自多少个不同的国家。 形式化地讲,你需要计算n条信息。对于输出的第i条信息,你需要统计满足ti-86400<tp≤ti的船只p,在所有的xp,j中,总共有多少个不同的数。 【输入】 第一行输入一个正整数n,表示小K统计了n艘船的信息。 接下来n行中,每行描述一艘船的信息: 前两个整数ti和ki分别表示这艘船到达海港的时间和船上的乘客数量,接下来ki个整数xi,j表示船上乘客的国籍。 保证输入的ti是递增的,单位是s; 表示从小K第一次上班开始计时,这艘船在第ti秒到达海港。 【输出】 输出n行,第i行输出一个整数表示第i艘船到达后的统计信息。 【样例输入】 3 144122 2223 1013 【样例输出】 3 4 4 【解析】 本题可以用队列+桶解决。每次输入直接把船上游客拆开用队列记录时间和国家,存入时如果发现这个国家编号都不与其他在队列中的国家编号相同,ans+1并记录国家编号出现次数(桶计数加1),然后每次从当前队首枚举过来,队首元素时间在一天外(超过86400s)就出队(桶计数减1),如果出队的国家编号都不与其他在队列中的国家编号相同,