1.2 正确处理多个测试用例
【1.1.1 Financial Management】仅给出了一个测试用例,该测试用例中的数据个数是已知的(12个月的收入),且运算十分简单(累加月收入,计算月平均值)。但在通常情况下,为了全面检验程序的正确性,大多数试题都要求测试多个测试用例,只有通过所有测试用例,程序才算正确。如果测试用例的个数或每个测试用例中数据的个数是预先确定的,则处理多个测试用例的循环结构比较简单;若测试用例的个数或每组测试用例中数据的个数未知,仅知测试用例内数据的结束标志和整个输入的结束标志,应如何处理呢?在数据量较大、所有测试用例都采用同一运算且数据范围已知的情况下,有无提高计算时效的办法呢?对于这两个问题,在本节中先给出两个实例。
【1.2.1 Doubles】
给出2~15个不同的正整数,计算这些数中有多少个数对满足一个数是另一个数的两倍。比如,有下列正整数
1 4 3 2 9 7 18 22
那么符合要求的数对有3个,因为2是1的两倍、4是2的两倍、18是9的两倍。
输入
输入包括多个测试用例。每个测试用例一行,给出2~15个两两不同且小于100的正整数。每一行的最后一个数是0,表示这一行的结束,这个数不属于那2~15个给定的正整数。输入的最后一行仅给出整数-1,这一行表示测试用例的输入结束,不用进行处理。
输出
对每个测试用例,输出一行,给出有多少对数满足其中一个数是另一个数的两倍。
试题来源:ACM Mid-Central USA 2003
在线测试:POJ 1552,ZOJ 1760,UVA 2787
试题解析
本题包含多个测试用例,因此需要循环处理每个测试用例,整个输入的结束标志是当前测试用例的第一个数是-1。在循环体内做两项工作:
1)通过一重循环读入当前测试用例的数组a,并累计数据元素个数n。当前测试用例的结束标志是读入数据0。
2)通过两重循环结构枚举a[]的所有数据对a[i]和a[j](0≤i<n-1,i+1≤j<n),判断a[i]和a[j]是否呈两倍关系(a[i]*2==a[j]||a[j]*2==a[i])。
参考程序
#include <iostream> //预编译命令 using namespace std; //使用 C++标准程序库中的所有标识符 int main() //主函数 { //主函数开始 int i, j, n, count, a[20]; //声明整型变量i、j、n、count和整型 //数组a cin>>a[0]; //输入第1个测试用例的首个数据 while(a[0]!=-1) //若输入未结束,则输入下一个测试用例 { n=1; //读入当前数据组 for( ; ; n++) { cin>>a[n]; if (a[n]==0) break; } count=0; //处理:计算当前测试用例中有多少数对满 //足一个数是另一个数的2倍 for (i=0; i<n-1; i++) //枚举所有数对 { for (j=i+1; j<n; j++) { if (a[i]*2==a[j] || a[j]*2==a[i])//若当前数对满足2倍关系,则累计 count++; } } cout<<count<<endl; //输出当前测试用例中满足2倍关系的数对 cin>>a[0]; //输入下一个测试用例的首个数据 } return 0; }
本题的测试用例数和测试用例长度都是未知的,其求解程序采用双重循环结构。
·外循环:枚举各组测试用例,结束标志为输入结束符(本题的输入结束符为-1)。
·内循环:输入和处理当前测试用例中的数据,输入的结束标志为测试用例的结束符(本题的测试用例以0为结束符)。
在处理多个测试用例的过程中,可能会遇到这样一种情况:数据量较大,所有测试用例都采用同一运算,并且数据范围已知。在这种情况下,为了提高计算效率,可以采用离线计算方法:预先计算出指定范围内的所有解,存入某个常量数组;以后每测试一个测试用例,直接从常量数组中引用相关数据就可以了,这样就避免了重复运算。
【1.2.2 Sum of Consecutive Prime Numbers】
一些正整数能够表示为一个或多个连续素数的和。给出一个正整数,有多少个这样的表示?例如,整数53有两个表示,即5+7+11+13+17和53;整数41有三个表示,即2+3+5+7+11+13、11+13+17和41;整数3只有一个表示,即3;整数20没有这样的表示。注意,加法操作数必须是连续的素数,因此,对于整数20,7+13和3+5+5+7都不是有效的表示。
请写一个程序,对于一个给出的正整数,程序给出连续素数的和的表示数。
输入
输入一个正整数序列,每个数一行,在2~10 000之间取值。输入以0表示结束。
输出
除了最后的0,输出的每一行对应输入的每一行。对于一个输入的正整数,输出的每一行给出连续素数的和的表示数。输出中没有其他字符。
试题来源:ACM Japan 2005
在线测试:POJ 2739,UVA 3399
试题解析
由于每个测试用例都要计算素数,且素数上限为10 000,因此:
1)首先,离线计算出[2..10001]内的所有素数,按照递增顺序存入数组prime[1..total]。
2)然后,依次处理每个测试用例:
设当前测试用例的输入为n,连续素数的和为cnt,n的表示数为ans。
采用双重循环计算n的表示数ans:
·外循环i:枚举所有可能的最小素数prime[i](for(int i=0;n>=prime[i];i++))。
·内循环j:枚举由prime[i]开始的连续素数的和cnt,条件是所有素数在prime[]中且cnt不大于n(for(int j=i;j<total & & cnt<n;j++)cnt+=prime[j])。内循环结束后,若cnt==n,则ans++。
外循环结束后得出的ans即问题解。
参考程序
#include <iostream> //预编译命令 using namespace std; //使用 C++标准程序库中的所有标识符 const int maxp = 2000, n = 10000; //设定素数表长和输入值的上限 int prime[maxp], total = 0; //素数表和表长初始化为0 bool isprime(int k) //判定k是否为素数 { for (int i = 0; i < total; i++) if (k % prime[i] == 0) return false; return true; } int main(void) //主函数 { for (int i = 2; i <= n; i++) // 预先建立素数表 if (isprime(i)) prime[total++] = i; prime[total] = n + 1; int m; cin >> m; //输入第1个正整数 while (m) { //循环,直到输入正整数0为止 int ans = 0; //和初始化为0 for (int i = 0; m >= prime[i]; i++) { // 枚举最小素数 int cnt = 0; //求连续素数的和 for (int j = i; j < total && cnt < m; j++) cnt += prime[j]; if (cnt == m) // 若和恰好等于m,则累计答案数 ++ans; } cout << ans << endl; //输出答案数 cin >> m; //输入下一个正整数 } return 0; }
所谓算法就是编程解决问题的方法。有些“输入–处理–输出”的计算题,尽管学过程序设计语言的读者能够解决,但“处理”这一环节的算法比较复杂,要求读者对于问题描述进行分析,推导出解题的算法。
【1.2.3 Game of Flying Circus】
反重力技术的发现改变了世界。反重力鞋(Grav鞋)的发明使人们能够在空中自由飞翔,从而催生了一项新的空中运动:“飞行马戏(Flying Circus)”。
参赛者穿着反重力鞋和飞行服进行比赛。比赛在一个特定的场地内进行,并要求参赛者在特定的时间内争取得分。比赛场地是一个边长为300米的正方形,正方形的四个角上都漂浮着浮标,这四个浮标按顺时针顺序编号为1、2、3、4,如图1.2-1所示。
图 1.2-1
两名选手将浮标#1作为比赛起点。比赛开始后,他们按顺时针顺序触碰四个浮标。(因为浮标#1是起点,所以他们要触碰的第一个浮标是浮标#2,此后,他们要按顺序触碰浮标#3、#4、和#1。)这里要注意,他们可以在比赛场地内自由飞行,甚至可以在正方形场地的中央飞行。
在以下两种情况下,选手可以得一分。
1)如果你比你的对手先触碰到浮标,你得一分。例如,在比赛开始后,如果对手比你先触碰了浮标#2,那么他得一分;而你触碰到浮标#2的时候,你就不会得分。还要注意,在触碰浮标#2之前,你不能触碰浮标#3或其他浮标。
2)不考虑浮标得分,而是靠格斗得分。如果你和对手在同一位置相遇,你可以和对手进行一场格斗,如胜利则得一分。考虑到游戏的平衡性,在浮标#2被触碰之前,两名选手不得格斗。
通常,有三种类型的选手:
1)Speeder:这类选手擅长高速运动,他们会通过触碰浮标来得分,尽量避免格斗。
2)Fighter:这类选手擅长格斗,他们会尽量通过和对手格斗来得分,因为Fighter的速度比Speeder慢,所以如果对手是一个Speeder,则Fighter很难通过触摸浮标来得分。
3)All-Rounder:综合了Fighter和Speeder的平衡型选手。
现在,在Asuka(All-Rounder选手)和Shion(Speeder选手)之间将进行一场训练赛。由于这场比赛只是一场训练赛,因此规则很简单:任何人触碰到浮标#1后,比赛结束。Shion是Speeder选手,他的策略非常简单:沿最短路径触碰浮标#2、#3、#4、#1。
Asuka擅长格斗,所以她和Shion格斗就会得1分,而对手在格斗之后会昏迷T秒。由于Asuka的速度比Shion慢,她决定在比赛中只和Shion格斗一次。本题设定,如果Asuka和Shion同时触碰浮标,则Asuka得分,并且Asuka还可以与Shion在浮标处格斗。在这种情况下,格斗发生在浮标被Asuka或Shion触碰之后。
Asuka的速度是V1米/秒,Shion的速度是V2米/秒。请问Asuka是否有赢的可能?
输入
输入的第一行给出整数t(0<t≤10000),然后给出t行,每行给出3个双精度变量T、V1和V2(0≤V1≤V2,T≥0),表示一个测试用例。
输出
如果存在Asuka赢得比赛的策略,则输出“Yes”,否则输出“No”。
提示
Asuka可以飞到连接浮标#2和浮标#3的边的中点,然后在那里等待Shion到来。当他们相遇时,Asuka和Shion格斗。此时,Shion会被击昏(这意味着Shion在100秒内不能移动),Asuka会飞回浮标#2,因为浮标#2已经被触碰了,她触碰浮标#2不会得分。但在那之后,她可以沿连接浮标的边飞到浮标#3、浮标#4、浮标#1,得3分。
试题来源:2015 ACM-ICPC Asia Shenyang Regional Contest
在线测试:HDOJ 5515,UVA 7244
试题解析
在Asuka和Shion之间进行的训练赛一共有5分可得:触碰4个浮标的4分和一场格斗的1分。
对于Asuka,要赢得比赛,可能有如下3种情况:
情况1:Asuka和Shion的速度一样(V1=V2),则Asuka和Shion同时到达浮标#2;然后Asuka和Shion进行一场格斗,Shion会被击昏;Asuka沿正方形场地的边到达浮标#3、浮标#4、浮标#1,赢得比赛。
情况2:Asuka的速度使得她在浮标#2和浮标#3之间的连线上的某点(未到浮标#3)和Shion相遇,即Shion沿正方形场地的边触碰浮标#2,然后沿连接浮标#2和浮标#3之间的连线向浮标#3飞行,Asuka则走直线,从浮标#1飞向该点。如图1.2-2所示。
Asuka和Shion在浮标#2与浮标#3间相遇的条件为。
设Asuka和Shion的相遇点与浮标#2的距离是x。在该点Asuka和Shion进行一场格斗,Shion会被击昏;然后Asuka沿直线飞到浮标#2,再飞向浮标#3和浮标#4。显然,相遇后,Asuka花费时间到达浮标#4,而Shion花费时间到达浮标#4。所以,如果Asuka能够先于Shion到达浮标#4,或者Asuka和Shion同时到达浮标#4,即,则Asuka获胜。
情况3:Asuka的速度使得她在浮标#3和浮标#4之间的连线上的某点(包括浮标#3,未到浮标#4)和Shion相遇,设该点与#4的距离为x,即Shion沿正方形场地的边已经触碰浮标#2和浮标#3;Asuka则走直线,从浮标#1飞向该点。如图1.2-3所示。在该点Asuka和Shion进行一场格斗,Shion会被击昏;然后Asuka沿直线飞到浮标#2,再沿正方形场地的边飞向浮标#3、浮标#4、浮标#1。也就是说,相遇后Asuka花费时间到达浮标#1,Shion花费时间到达浮标#1。如果Asuka能比Shion先到达浮标#1,或者Asuka和Shion同时到达浮标#1,则Asuka获胜。也就是说,如果,则Asuka获胜。
图 1.2-2
图 1.2-3
对于上述情况,如果Asuka能够获胜,则输出“Yes”,否则输出“No”。
参考程序
#include <bits/stdc++.h> using namespace std; typedef long long LL; typedef pair<int, int> PI; const int N=1e5; const double eps=1e-5; const LL mod=1e9+7; int main() { int t, ca=1; //测试用例编号初始化 scanf("%d", &t); //输入测试用例数 while(t--) //依次处理每个测试用例 { double t, v1, v2; // Shion格斗之后的昏迷时间为t,Asuka和 // Shion的速度分别为v1、v2 scanf("%lf%lf%lf", &t, &v1, &v2); //输入Shion格斗之后的昏迷时间 printf("Case #%d: ", ca++); //输出测试用例编号,计算下一个测试用例编号 //分析Asuka赢得比赛的第一种情况 if(v1==v2) { puts("Yes"); continue; } //分析Asuka赢得比赛的第二种情况 double tt1=300*sqrt(2.0)/v1; // 计算Asuka从浮标#1沿对角线至浮标#3所用 // 的时间 double tt2=600.0/v2; // 计算Shion走浮标#1-浮标#2-浮标#3所用 // 的时间 double v12=v1*v1, v22=v2*v2; // 计算两者速度的平方 double t1=300.0/v1; // 计算Asuka走浮标#1-浮标#4所用的时间t1 double t2=900.0/v2; // 计算Shion走浮标#1-浮标#2-浮标#3-浮标 // #4所用的时间 if(t1>=t2) // 若在两者都走连线情况下,Asuka未先到达浮标 // #4,则Asuka失败 { puts("No"); continue; } if(tt1<=tt2) // 若Asuka和Shion在浮标#2与浮标#3之间相 // 遇,则计算相遇点与浮标#2的距离x { double dt=(600*v12)*(600*v12)-4*(v12-v22)*(v12*90000-90000*v22); double x=(-600.0*v12+sqrt(dt))/2.0/(v12-v22); if((x+600)/v1<=t+(600-x)/v2) //若Asuka先于Shion到达浮标#4,则获胜 { puts("Yes"); continue; } } //分析Asuka赢得比赛的第三种情况 //计算Asuka和Shion在浮标#3与浮标#4之间的相遇点与浮标#4的距离x double dt=(1800*v12)*(1800*v12)-4*(v12-v22)*(v12*810000-90000*v22); double x=(1800.0*v12-sqrt(dt))/2.0/(v12-v22); //若Asuka比Shion先到浮标#1,则Asuka获胜;否则失败 if(sqrt((300.0-x)*(300.0-x)+90000.0)/v1+900.0/v1<=t+(300+ x)/v2) puts("Yes"); else puts("No"); } return 0; }