顺序结构程序自上到下只执行 一遍,而分支结构中甚至有些语句可能一遍都执行不了。换句话说,为了让计算机执行大量 操作,必须编写大量的语句。能不能只编写少量语句,就让计算机做大量的工作呢?这就是 本章的主题。基本思路很简单:一条语句执行多次就可以了。但如何让这样的程序真正发挥 作用,可不是一件容易的事。
2.1 for循环
for循环的格式为:for(初始化;条件;调整)循环体
程序2-1 输出1,2,3,…,n的值
#include <stdio.h>
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
printf("%d\n",i);
}
return 0;
/*
下面的程序运行结果是什么?“!=”运算符表示“不相等”。
提示:请上机实验,不要凭主观感觉回答。
double i;
for(i=0;i!=10;i+=0.1)
printf("%.lf\n",i);
return 0;
结果:无限循环.浮点数进行小数运算的时由于精度问题,会有很小的误差,
然而用 = 或者 != 这样的运算符来比较,检测出这种误差的.所以导致结果的不正确.
多试一下,将循环条件改为 i != 0.1 或者 i != 0.2时,程序能够正常运行,得正常结果.
但是当i != 0.3时,就是无限循环.显然,程序中,这种不确定的错误是不应该存在,
在定义循环变量时,尽量采用int型及整数的加减
*/
}
程序2-2 输出所有形如aabb的4位完全平方数(即前两位数字相等,后两位数字也相等)。
分支和循环结合在一起时功能强大:下面枚举所有可能的aabb,然后判断它们是否为完 全平方数。注意,a的范围是1~9,但b可以是0。主程序如下:
for(int a = 1; a <= 9; a++)
for(int b = 0; b <= 9; b++)
if(aabb是完全平方数) printf("%d\n", aabb);
上面的程序并不完整——“aabb是完全平方数”是中文描述,而不是合法的C语言表达式,而aabb在C语言中也是另外一个变量,而不是把两个数字a和两个数字b拼在一起(C语言中的变量名可以由多个字母组成)。
思路一:如何判断n是否为完全平方数?第1章中用过“开平方”函 数,可以先求出其平方根,然后看它是否为整数,即用一个int型变量m存储sqrt(n)四舍五入后的整数,然后判断
m
2
m^2
m2是否等于n。函数floor(x)返回不超过x的最大整数。
程序2-2完整代码 7744问题(1)
//输出所有形如aabb的4位完全平方数(即前两位数字相等,后两位数字也相等)。
#include <stdio.h>
#include <math.h>
int main()
{
for(int a=1;a<=9;a++)
for(int b=0;b<=9;b++)
{
// if(aabb是完全平方数)
// printf("%d\n",aabb);
int n = a*1100+b*11;
int m = floor(sqrt(n)+0.5);
/*为了减小误差的影响,一般改成四舍五入,即floor(x+0.5)(2)。
如果难以理 解,可以想象成在数轴上把一个单位区间往左移动0.5个单位的距离。
floor(x)等于1的区间 为[1,2),
而floor(x+0.5)等于1的区间为[0.5,1.5)。
*/
if(m*m==n)printf("%d\n",n);
}
return 0;
}
你可能会问:可不可以这样写?if(sqrt(n)== floor(sqrt(n)))printf("%d\n",n);
,即直接判断sqrt(n)是否为整数。理论上当然没问题,但这样写不保险,因为浮点数的运算(和函数)有可能存在误差。
思路二:枚举平方根x,从而避免开平方操作。
程序2-3完整代码 7744问题(2)
#include<stdio.h>
int main()
{
for(int x = 1; ; x++){
int n = x * x;
if(n < 1000) continue;
if(n > 9999) break;
int hi = n / 100;
int lo = n % 100;
if(hi/10 == hi%10 && lo/10 == lo%10) printf("%d\n", n);
}
return 0;
}
此程序中的新知识是continue和break语句。continue是指跳回for循环的开始,执行调整语句并判断循环条件(即“直接进行下一次循环”),而break是指直接跳出循环。
这里的continue语句的作用是排除不足四位数的n,直接检查后面的数。当然,也可以直接从x=32开始枚举,但是continue可以帮助我们偷懒:不必求出循环的起始点。有了break, 连循环终点也不必指定——当n超过9999后会自动退出循环。注意,这里是“退出循环”而不 是“继续循环”(想一想,为什么),可以把break换成continue加以验证。
另外,注意到这里的for语句是“残缺”的:没有指定循环条件。事实上,3部分都是可以 省略的。没错,for(;;)就是一个死循环,如果不采取措施(如break),就永远不会结束。
2.2 while循环和do-while循环
while循环的格式为“while(条件)循环体;
程序2-4 3n+1问题:对于任意大于1的自然数n,若n为奇数,则将n变为3n+1,否则变为n的一半。 经过若干次这样的变换,一定会使n变为1。例如,3→10→5→16→8→4→2→1。
不难发现,程序完成的工作依然是重复性的:要么乘3加1,要么除以2,但和2.1的程序又不太一样:循环的次数是不确定的,而且n也不是“递增”式的循环。这样的情况很适合用while循环来实现。
程序2-4完整代码 3n+1问题(有bug)
#include <stdio.h>
int main()
{
int n, count = 0;
scanf("%d", &n);
while(n>1)
{
if(n%2==1)
n = 3 * n + 1;
else
n /= 2;
count++;
}
printf("%d\n",count);
printf("%d\n",n);
return 0;
}
这个程序是否正确?先来测试一下:输入“987654321”,看看结果是什么。很不幸,答 案等于1——这明显是错误的。题目中给出的范围是
n
≤
1
0
9
n≤10^9
n≤109,这个987654321是合法的输入数据。
在给n做变换的语句后加一条输出语句printf("%d\n",n),将很快找到问题的所在:第一次输出为-1332004332,它不大于1,所以循环终止。所以我们知道是乘法溢出了。
3n+1问题修改版
我们修改为long long版本的代码,它避开了对long long的输入输出,并且成功算出n=987654321时的答案为180。
程序2-5完整代码 3n+1问题
#include <stdio.h>
int main()
{
int n2,count=0;
scanf("%d",&n2);
long long n = n2;
while(n>1){
if(n%2==1)
n = 3 * n + 1;
else
n /= 2;
count++;
}
printf("%d\n",count);
printf("%d\n",n);
return 0;
}
程序2-6 近似计算:计算 π / 4 = 1 − 1 / 3 + 1 / 5 − 1 / 7 + . . . π/4=1-1/3+1/5-1/7+... π/4=1−1/3+1/5−1/7+... ,直到最后一项小于 1 0 − 6 10^-6 10−6
本题也是重复计算,因此可以用循环实现。但不同的是,只有算完一 项之后才知道它是否小于10-6。也就是说,循环终止判断是在计算之后,而不是计算之前。 这样的情况很适合使用do-while循环。
程序2-6完整代码 近似计算(使用for循环)
#include <stdio.h>
int main()
{
double sum=0;
int i;
for(i=0; ;i++)
{
double term = 1.0/(i*2+1);
if(i%2==0)
sum += term;
else
sum -= term;
if(term<1e-6)
break;
}
printf("%.6f\n",sum);
return 0;
}
程序2-6完整代码 近似计算(使用do-while循环)
#include <stdio.h>
int main()
{
double sum=0;
int n=1;
float sgn = 1;
do
{
sum += sgn/n;
n += 2;
sgn = -sgn;
}while(1.0/n>1e-6);
printf("%.6f\n",sum);
return 0;
}
do-while循环的格式为“do{循环体}while(条件);”,其中循环体至少执行一次,每次执行完循环体后判断条件,当条件满足时继续循环。
2.3 循环的代价
程序2-7 阶乘之和:输入n,计算 S = 1 ! + 2 ! + 3 ! + … + n ! S=1!+2!+3!+…+n! S=1!+2!+3!+…+n!的末6位(不含前导0)。 n ≤ 1 0 6 n≤10^6 n≤106,n!表示前n个正整数之积。
这个任务并不难,引入累加变量S之后,核心算法只有for(int i=1;i<=n;i++)S +=i!
。不过,C语言并没有阶乘运算符,所以这句话只是伪代码,而不是真正的代码。 事实上,还需要一次循环来计算i!,即for(int j=1;j<=i;j++)factorial*=j;
。
程序2-7完整代码 阶乘之和(1)
#include <stdio.h>
int main()
{
int i,n,S=0;
scanf("%d",&n);
for(i=1;i<=n;i++)
{
int j,temp=1;
for(j=1;j<=i;j++)
{
temp *= j;
}
S += temp;
}
printf("%d\n",S%1000000);
return 0;
}
下面来测试一下这个程序:n=100时,输出-961703。直觉告诉我们:乘法又溢出了。这个直觉很容易通过“输出中间变量”法得到验证,但若要解决这个问 题,还需要一点数学知识。
要计算只包含加法、减法和乘法的整数表达式除以正整数n的余数,可以在 每步计算之后对n取余,结果不变。
程序2-8完整代码 阶乘之和(2)
#include <stdio.h>
#include <time.h>
int main()
{
const int MOD=1000000;
int i,n,S=0;
scanf("%d",&n);
if(n>25) n=25;
for(i=1;i<=n;i++)
{
int j,temp=1;
for(j=1;j<=i;j++)
temp = temp*j%MOD;
S = (S+temp)%MOD;
}
printf("%d\n",S);
printf("Time used=%.2f\n",(double)clock()/CLOCKS_PER_SEC);
return 0;
}
25!末尾有6个0,所以从第5项开始,后面的所有项都不会影响和的末6位数字——只需要在程序的最前面加一条语句if(n>25)n=25;
,效率和溢出都将不存在问题。
2.4 算法竞赛中的输入输出框架
程序2-9 数据统计:输入一些整数,求出它们的最小值、最大值和平均值(保留3位小数)。输入保证这些数都是不超过1000的整数。
如果是先输入整数n,然后输入n个整数,相信读者能够写出程序。关键在于:整数的个 数是不确定的。下面直接给出程序:
程序2-9完整代码 数据统计(有bug)
#include <stdio.h>
int main()
{
int x,n=0,min,max,s=0;
while(scanf("%d",&x)==1)//scanf返回的值是成功输入的变量个数,当输入结束时,scanf函数无法再次读取x,将返回0
{
s += x;
if(x<min)
min = x;
if(x>max)
max = x; //max可能一开始就等于其他很大的值,无法更新为比它小的值
//解决方法:1、定义一个很大的常数,如INF=10000000,然后令max=-INF,min=INF
// 2、先读取第一个整数x,然后令max=min=x
n++;
}
printf("%d %d %.3f\n",min,max,(double)s/n);
return 0;
}
下面进行测试。输入“2 8 3 5 1 7 3 6”,按Enter键,但未显示结果。难道程序速度太慢? 其实程序正在等待输入。还记得scanf的输入格式吗?空格、TAB和回车符都是无关紧要的, 所以按Enter键并不意味着输入的结束。那如何才能告诉程序输入结束了呢?
在Windows下,输入完毕后先按Enter键,再按Ctrl+Z键,最后再按Enter 键,即可结束输入。在Linux下,输入完毕后按Ctrl+D键即可结束输入。
输入终于结束了,但输出却是“1 2293624 4.375”。这个2293624是从何而来?当用-O2编 译(读者可阅读附录A了解-O2)后答案变成了1 10 4.375,和刚才不一样!换句话说,这个 程序的运行结果是不确定的。在读者自己的机器上,答案甚至可能和上述两个都不同。
根据“输出中间结果”的方法,读者不难验证下面的结论:变量max在一开始就等于 2293624(或者10),自然无法更新为比它小的8。修改方法已经注明在程序中。
上面的程序并不是很方便:每次测试都要手动输入许多数。尽管可以用前面讲的管道的 方法,但数据只是保存在命令行中,仍然不够方便。
注:一个好的方法是用文件——把输入数据保存在文件中,输出数据也保存在文件中。这 样,只要事先把输入数据保存在文件中,就不必每次重新输入了;数据输出在文件中也避免 了“输出太多,一卷屏前面的就看不见了”这样的尴尬,运行结束后,慢慢浏览输出文件即 可。如果有标准答案文件,还可以进行文件比较(9),而无须编程人员逐个检查输出是否正确。事实上,几乎所有算法竞赛的输入数据和标准答案都是保存在文件中的。
使用文件最简单的方法是使用输入输出重定向,只需在main函数的入口处加入以下两条
语句:
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
上述语句将使得scanf从文件input.txt读入,printf写入文件output.txt。事实上,不只是scanf 和printf,所有读键盘输入、写屏幕输出的函数都将改用文件。尽管这样做很方便,并不是所 有算法竞赛都允许用程序读写文件。甚至有的竞赛允许访问文件,但不允许用freopen这样的 重定向方式读写文件。
程序2-10完整代码 数据统计(重定向版)
#define LOCAL
#include <stdio.h>
#define INF 1000000000
int main()
{
#ifdef LOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif
int x,n=0,min=INF,max=-INF,s=0;
while(scanf("%d",&x)==1)
{
s += x;
if(x<min)
min = x;
if(x>max)
max = x;
// printf("%d %d %.3f\n",x,min,max); 用于调试输出中间信息
n++;
}
printf("%d %d %.3f\n",min,max,(double)s/n);
return 0;
}
重定向的部分被写在了#ifdef和#endif中。其含义是:只有定义了符号LOCAL,才编译两条freopen语句。 输出中间结果的printf语句写在了注释中——它在最后版本的程序中不应该出现,但是又舍不得删除它(万一发现了新的bug,需要再次用它输出中间信息)。将其注释的好处是:一旦需要时,把注释符去掉即可。
如果比赛要求用文件输入输出,但禁止用重定向的方式,又当如何呢?程序如下:
程序2-11完整代码 数据统计(fopen版)
#include <stdio.h>
#define INF 1000000000
int main()
{
FILE *fin,*fout;
fin = fopen("data.in","rb");
fout = fopen("data.out","wb");
int x,n=0,max=-INF,min=INF,s=0;
while(fscanf(fin,"%d",&x)==1) //注意是fscanf
{
s += x;
if(x<min)
min = x;
if(x>max)
max = x;
n++;
}
fprintf(fout,"%d %d %.3f\n",min,max,(double)s/n);
fclose(fin);//关闭文件
fclose(fout);
return 0;
}
虽然新内容不少,但也很直观:先声明变量fin和fout(暂且不用考虑FILE*),把scanf改 成fscanf,第一个参数为fin;把printf改成fprintf,第一个参数为fout,最后执行fclose,关闭两个文件。
程序2-12 数据统计II:输入一些整数,求出它们的最小值、最大值和平均值(保留3位小数)。输入保证这些 数都是不超过1000的整数。输入包含多组数据,每组数据第一行是整数个数n,第二行是n个整数。n=0为输入结束 标记,程序应当忽略这组数据。相邻两组数据之间应输出一个空行。
本题和例题2-5本质相同,但是输入输出方式有了一定的变化。由于这样的格式在算法 竞赛中非常常见,这里直接给出代码:
程序2-12完整代码 数据统计II(有bug,修改办法已在代码注释里)
#include <stdio.h>
#define INF 100000000
int main()
{
int x,n=0,min=INF,max=-INF,s=0,kcase=0;
while(scanf("%d",&n)==1 && n)//鲁棒性,程序能自动处理错误,防止真实数据没有以0结尾情况出现
{
int s = 0;
//此处是重置min,max的值,如果不重置,第三组数据输出后会出现错误,min,max仍然是上次的值
//int min=INF,max=-INF;
for(int i=0;i<n;i++){
scanf("%d",&x);
s += x;
if(x<min) min=x;
if(x>max) max=x;
}
if(kcase) printf("\n");
printf("Case %d: %d %d %.3f\n",++kcase,min,max,(double)s/n);
}
return 0;
}
首先是输入循环。题目说了n=0为输入标记,为什么还要判断scanf的返回值呢?答案是为了鲁棒性(robustness)。
算法竞赛中题目的输入输出是人设计的,难免会出错。有时会出现题目指明以n=0为结束标 记而真实数据忘记以n=0结尾的情形。虽然比赛中途往往会修改这一错误,但在ACM/ICPC 等时间紧迫的比赛中,如果程序能自动处理好有瑕疵的数据,会节约大量不必要的时间浪费。
接下来是找bug时间。上面的程序对于样例输入输出可以得到正确的结果,但它真的是 正确的吗?在样例输入的最后增加第3组数据:10,会看到这样的输出:
Case 3:-4 10 0.000
相信读者已经意识到问题出在哪里了:min和max没有“重置”,仍然是上个数据结束后的值。
2.5 注解与习题
习题2-1 水仙花数:输出100~999中的所有水仙花数。若3位数ABC满足 A B C = A 3 + B 3 + C 3 ABC=A^3+B^3+C^3 ABC=A3+B3+C3,则称其为水仙花 数。例如 153 = 1 3 + 5 3 + 3 3 153=1^3+5^3+3^3 153=13+53+33,所以153是水仙花数。
#include <stdio.h>
int main()
{
int A,B,C;
for(int i=100;i<1000;i++){
A = i/100;
B = i/10%10;
C = i%10;
if(i==A*A*A+B*B*B+C*C*C)
printf("%d是水仙花数\n",i);
}
}
习题2-2 韩信点兵:相传韩信才智过人,从不直接清点自己军队的人数,只要让士兵先后以三人一排、五人 一排、七人一排地变换队形,而他每次只掠一眼队伍的排尾就知道总人数了。输入包含多组 数据,每组数据包含3个非负整数a,b,c,表示每种队形排尾的人数(a<3,b<5,c< 7),输出总人数的最小值(或报告无解)。已知总人数不小于10,不超过100。输入到文件结束为止。
非文件版:
#include <stdio.h>
int main()
{
int x,a=0,b=0,c=0,kcase=0;
scanf("%d%d%d",&a,&b,&c);
for(int i=10;i<=100;i++){
if(i%3==a&&i%5==b&&i%7==c){
printf("Case %d: %d\n",++kcase,i);
}
else if(i==101){
printf("Case %d: No answer\n",++kcase);
}
}
return 0;
}
文件版:
#define LOCAL
#include <stdio.h>
int main()
{
//#ifdef LOCAL
//
//#endif // LOCAL
FILE *fin, *fout;
fin = fopen("data.in.txt", "rb");
fout = fopen("data.out.txt", "wb");
int x, S3 = 0, S5 = 0, S7 = 0, count = 0, n = 0;
while (n < 3)//借此循环读取第一组数
{
int tip = fscanf(fin, "%d", &x);
if (tip != 1)break;//判断文件里是否还有数据遗存
if (n == 0)S3 = x;//读入
else if (n == 1)S5 = x;
else if (n == 2) {
S7 = x;
for (int i = 10; i <= 101; i++) {//找到最小值并输出
if (i % 3 == S3 && i % 5 == S5 && i % 7 == S7 && i <= 100) {
printf("Case %d: %d\n", ++count, i);
//fprintf(fout,"Case %d: %d\n", count, i);//输出不换行
n = -1;//这样会产生无限循环
break;
}
else if (i == 101) {
printf("Case %d: No answer\n", ++count);
//fprintf(fout,"Case %d: No answer\n", count);
n = -1;
break;//可去掉
}
}
//break;
}
n++;//n循环后需要重置
}
//fprintf(fout, "Case %d: = %d\n", count, S3);
fclose(fin);
fclose(fout);
return 0;
}
习题2-3 倒三角形:
输入正整数n≤20,输出一个n层的倒三角形。例如,n=5时输出如下:
#########
#######
#####
###
#
#include <stdio.h>
int main()
{
int n;
scanf("%d",&n);
for(int i=n;i>0;i--)
{
for(int j=0;j<n-i;j++){
printf(" ");
}
for(int k=0;k<2*i-1;k++){
printf("#");
}
printf("\n");
}
}
习题2-4 子序列的和:输入两个正整数n<m< 1 0 6 10^6 106,输出 1 / n 2 + 1 / ( n + 1 ) 2 + . . . + 1 / m 2 1/n^2+1/(n+1)^2+...+1/m^2 1/n2+1/(n+1)2+...+1/m2 ,保留5位小数。输入包含多组数据, 结束标记为n=m=0。提示:本题有陷阱。
#include <stdio.h>
int main()
{
int n,m,temp;
double sum = 0;
scanf("%d%d",&n,&m);
if(n>m)
{
temp = n;
n = m;
m = temp;
}
for(int i=n;i<=m;i++){
sum += 1.0/i/i;
}
printf("%.5lf",sum);
}
习题2-5 分数化小数:输入正整数a,b,c,输出a/b的小数形式,精确到小数点后c位。 a , b ≤ 1 0 6 a,b≤10^6 a,b≤106,c≤100。输 入包含多组数据,结束标记为a=b=c=0。
#include <stdio.h>
int main()
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
printf("%.*lf\n",c,(double)a/b);
return 0;
}
习题2-6 排列:用1,2,3,…,9组成3个三位数abc,def和ghi,每个数字恰好使用一次,要 求abc:def:ghi=1:2:3。按照“abc def ghi”的格式输出所有解,每行一个解。
#include <stdio.h>
#include <stdlib.h>
void result(int num, int *result_add, int *result_mul)
{
int i, j, k;
i = num / 100; //百位
j = num / 10 % 10; //十位
k = num % 10; //个位
*result_add += i + j + k; //分解出来的位数相加
*result_mul *= i * j * k; //相乘
}
/* 整体思路:i 最小只能是123, 最大只能是333(因为最大数字只能是999)
第2个地方的实现, 我们可以用数学的方法来实现
1~9加起来真能是45, 1~9乘起来只能是362880
所以我们可以将前面的i, j, k分别分解出来的9位数字相加, 相乘,
看最后的结果是不是45,362880 */
int main()
{
int i, j, k;
int result_add, result_mul;
for(i = 123; i <=333; i++)
{
j = i * 2;
k = i * 3;
result_add = 0;
result_mul = 1;
result(i,&result_add,&result_mul);
result(j,&result_add,&result_mul);
result(k,&result_add,&result_mul);
if(result_add == 45 && result_mul == 362880)
printf("%d %d %d\n", i, j, k);
}
return 0;
}
2.6 小结
循环的出现让程序逻辑复杂了许多。在很多情况下,仔细研究程序的执行流程能够很好地帮助理解算法,特别是“当前行”和变量的改变。有些变量是特别值得关注的,如计数器、 累加器,以及“当前最小/最大值”这样的中间变量。很多时候,用printf输出一些关键的中间变量能有效地帮助读者了解程序执行过程、发现错误,就像本章中多次使用的一样。