[算法竞赛入门]第二章_循环结构程序设计
第2章 循环结构程序设计
【学习内容相关章节】
2.1for循环 2.2循环结构程序设计 2.3文件操作
2.4小结与习题
【学习目标】
(1)掌握for循环的使用方法;
(2)掌握while循环的使用方法;
(3)学会使用计算器和累加器;
(4)学会用输出中间结果的方法调试;
(5)学会用计时函数测试程序效率;
(6)学会用重定向的方式读写文件;
(7)学会fopen的方式读写文件;
(8)了解算法竞赛对文件读写方式和命名的严格性;
(9)记住变量在赋值之前的值是不确定的;
(10)学会使用条件编译指示构建本地运行环境。
【学习要求】
掌握for循环的使用方法;掌握while循环的使用方法;掌握几个常用的文件操作库函数fopen、fclose、fprintf、fscanf等。
【学习内容提要】
在有些程序中,需要反复执行某些语句。将n条相同的语句简单地复制会使程序变得不合理的冗长,因此高级语言中提供了支持程序重复执行某一段程序的循环控制语句。相关的语句有:for、while、do while、break、continue等。
既可以从文件中读取数据, 也可以向文件中写入数据。读写文件之前,首先要打开文件。读写文件结束后,要关闭文件。C/C++提供了一系列库函数,声明于stdio.h中,用于进行文件操作。这里介绍其中几个常用的文件操作库函数fopen、fclose、fprintf、fscanf等。
【学习重点、难点】
学习重点:
(1)掌握for循环的使用方法;
(2)掌握while循环的使用方法;
(3)掌握文件有关操作;
(4)条件编译。
学习难点:
(1)掌握for循环的使用方法;
(2)掌握while循环的使用方法;
【课时安排(共2学时)】
2.1for循环 2.2循环结构程序设计 2.3文件操作 2.4小结与习题
2.1 for循环
请用for循环实现输入正整数n,打印1,2,3,…,10,每个占用一行。程序如下:
程序2-1 输出1,2,3,…,n的值
#include <stdio.h>
int main(){
int i, n;
scanf("%d", &n);
for(i = 1; i <= n; i++)
printf("%d\n", i);
return 0;
}
提示2-1:for循环的格式:for(初始化;条件;调整) 循环体;
提示2-2:尽管for循环反复执行相同的语句,但这些语句每次的执行效果往往不同。
提示2-3:编写程序时,要特别留意“当前行”的跳转和变量的改变。
有了for循环,可以解决一些简单的问题。
例2-1 aabb。
输出所有形如aabb的四位完全平方数(即前两位数字相等,后两位数字也相等)。
【分析】
分支和循环结合在一起时威力特别强大:可以枚举所有可能的aabb,然后判断它们是否是完全平方数。注意,a的范围是1~9,但b可以是0。主程序如下:
for(a = 1; a <= 9; a++)
for(b = 0; b <= 9; b++)
if(aabb是完全平方数) printf("%d\n", aabb);
注意:(1)上面不是真正的程序,把这样的代码称为伪代码(psedocode)。
(2)上面用到了循环的嵌套:for循环的循环体自身又是一个循环。
提示2-4:不拘一格的使用伪代码来思考和描述算法是一种值得推荐的做法。
提示2-5:把伪代码改写成代码时,一般先选择较为容易的任务来完成。
程序2-2 7744问题(1)
#include <stdio.h>
#include <math.h>
int main(){
int a, b, n;
double m;
for(a = 1; a <= 9; a++)
for(b = 0; b <= 9; b++) {
n = a*1100 + b*11;
m = sqrt(n);
if(floor(m+0.5) == m) printf("%d\n", n);
}
return 0;
}
说明:函数floor(x)返回x的整数部分,使用floor(m+0.5)==m的原因是浮点数的运算(和函数)有可能存在误差。
提示2-6:浮点运算可能存在误差。在进行浮点数比较时,应考虑到浮点误差。
对于四位完全平方数的还有另一个思路就是枚举平方根x,从而避免开平方操作。
程序2-3 7744问题(2)
#include <stdio.h>
int main(){
int x, n, hi, lo;
for(x = 1; ; x++) {
n = x * x;
if(n < 1000) continue;
if(n > 9999) break;
hi = n / 100;
lo = n % 100;
if(hi/10 == hi%10 && lo/10 == lo%10) printf("%d\n", n);
}
return 0;
}
说明:本程序中用到break和continue语句。break是指直接跳出本层循环,continue是指结束本次循环,但不跳出本层循环,进入下一次循环。
2.2 循环结构程序设计
例2-2 3n+1问题
猜想:对于任意大于1的自然数,若n为奇数,则将n变为3n+1,否则变为n的一半。经过若干次这样的变换,一定会使n变为1。例如3→10→5→16→8→4→2→1。
输入n,输出变换的次数。n≤109。
样例输入:3
样例输出:7
【分析】
从3n+1问题可以看出,n也不是“递增”式的循环,且循环次数也不确定,这种情况非常适合用while循环来实现。
程序2-4 3n+1问题
#include <stdio.h>
int main(){
int n, count = 0;
scanf("%d", &n);
while(n > 1) {
if(n % 2 == 1) n = n*3+1;
else n /= 2;
count++;
}
printf("%d\n", count);
return 0;
}
提示2-7:while循环的格式为:“while(条件) 循环体;”。
从程序2-4中可得,while循环与for循环可以相互转化。在本程序中的count++,相当于一个计算器,这个功能在编程中会经常遇到。
提示2-8:当需要统计某种事物的个数时,可以用一个变量来充当计算器。
提示2-9:不要忘记测试。一个看上去正确的程序可能隐含错误。
提示2-10:在观察无法找出错误时,可以用“输出中间结果”的方法查错。
例2-3 阶乘之和。
输入n,计算S=1!+2!+3!+…+n!的末6位(不含前导0)。n≤106。这里,n!表示前n个正整数之积。
样例输入:10
样例输出:37913
【分析】
用S变量来累加阶乘之和。核心算法只有一句话:for(i=1;i<=n;i++) S+=i!。在C语言中没有阶乘运算符,所以还需要一个循环来求i!: for(j=1;j<=i;j++) factorial*=j;。
代码如下:
程序2-5 阶乘之和(1)
#include <stdio.h>
int main(){
int i, j, n, S = 0;
scanf("%d", &n);
for(i = 1; i <= n; i++){
int factorial = 1;
for(j = 1; j <= i; j++)
factorial *= j;
S += factorial;
}
printf("%d\n", S % 1000000);
return 0;
}
注意:每执行一次循环体,都要重新声明一次累乘器factorial,并初始化为1(因为是乘积,所以初始化为1,要是初始化为0,则循环后的factorial=i!=0)。
提示2-11:在循环体开始处定义的变量,每次执行循环体时会重新声明并初始化。
提示2-12:要计算只包含加法、减法和乘法的整数表达式除以正整数n的余数,可以在每步计算之后对n取余,结果不变。
程序2-6 阶乘之和(2)
#include <stdio.h>
#include <time.h>
int main(){
const int MOD = 1000000;
int i, j, n, S = 0;
scanf("%d", &n);
for(i = 1; i <= n; i++){
int factorial = 1;
for(j = 1; j <= i; j++)
factorial = (factorial * j % MOD);
S = (S + factorial) % MOD;
}
printf("%d\n", S);
printf("Time used = %.2lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
说明:(1)这个程序真正的特别之处在于计时函数clock()的使用。该函数返回程序目前为止运行的时间,以毫秒为单位。这样,在程序结束之前调用它,便可获得整个程序的运行时间。这个时间除以常数CLOCKS_PER_SEC之后得到的值以“秒”为单位。
(2)在VC++6.0中time.h下宏定义的常量CLOCKS_PER_SEC,其值为1000。VC++6.0中该符号常量定义如下: #define CLOCKS_PER_SEC 1000
对于CLOCKS_PER_SEC,它用来表示一秒钟会有多少个时钟计时单元,时钟计时单元的长度为1毫秒,clock()/CLOCKS_PER_SEC就是将毫秒转化为秒。
提示2-13:可以使用time.h和clock()函数获得程序运行时间。常数CLOCKS_PER_SEC与操作系统有关,请不要直接使用clock()的返回值,而应总是除以CLOCKS_PER_SEC。
提示2-14:很多程序的运行时间与规模n存在着近似的简单关系。可以通过计时函数来发现或验证这一关系。
本节的两个例题展示了计数器、累加器的使用和循环结构程序设计中最常见的两个问题:算术运算溢出和程序效率低下。另外,本节中介绍的两个工具——输出中间结果和计时函数,都有是相当实用的。
2.3 文件操作
例2-4 数据统计。
输入一些整数,求出它们的最小值、最大值和平均值(保留3位小数)。输入保证这些数都是不超过1000的整数。
样例输入:2 8 3 5 1 7 3 6
样例输出:1 8 4.375
【分析】
如果是先输入整数n,然后输入n个整数,相信能写出程序。关键在于:整数的个数是不确定的。下面给出程序:
程序2-7 数据统计(错误)
#include <stdio.h>
int main(){
int x, n = 0, min, max, s = 0;
while(scanf("%d", &x) == 1) {
s += x;
if(x < min) min = x;
if(x > max) max = x;
n++;
}
printf("%d %d %.3lf\n", min, max, (double)s/n);
return 0;
}
说明:(1)scanf函数的返回值是成功输入的变量个数。当输入结束时,scanf无法再次读取x,将返回0。
(2)在测试时,输入2 8 3 5 1 7 3 6,按Enter键后,没有输出结果,所以此时按Enter键并不意味着输入的结束。
提示2-15:在Windows下,输入完毕后先按Enter键,再按Ctrl+Z键,最后再按Enter键,即可结束键入。在Linux下,输入完毕后按Ctrl+D键即可结束输入。
通过提示2-15,输入可以结束了。但是此程序的运行结果是不确定的。
提示2-16:变量在未赋值之前的值是不确定的。特别地,它不一定等于0。
解决上面程序的运行结果不对的方法是在变量使用前赋初值。由于min保存的是最小值,它的初值应该是一个很大的数;反过来,max的初值应该是一个很小的数。一种方法是定义一个很大的常数,如INF=1000000000,然后让max=INF,而min=-INF,而另一种方法是先读取第一个整数x,然后令max=min=x。
另一个好的方法是用文件——把输入数据保存在文件中,输出数据也保存在文件中。事实上,几乎所有算法竞赛的输入数据和标准答案都是保存在文件中的。
使用文件最简单的方法是使用输入输出重定向,只需要在main函数的入口处加入以下两条语句:
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
它将使得scanf从文件input.txt读入,printf写入文件output.txt。但是并不是所有算法竞赛都允许用程序读写文件。甚至有的竞赛允许访问文件,但不允许freopen这样的重定向方式读写文件。请参赛之前仔细阅读文件读写的相关规定。
提示2-17:请在比赛之前了解文件读写的相关规定:是标准输入输出(也称标准I/O,即直接读键盘、写屏幕),还是文件输入输出?如果是文件输入输出,是否禁止用重定向方式访问文件?
多年来,无数选手因文件相关问题丢掉了大量的得分。一个普适的原则是:详细阅读比赛规定,并严格遵守。
例如,如果题目规定程序名称为test,输入文件名为test.in,输出文件名为test.out,就是不要犯以下错误。
错误1:程序存为t1.c(应该改成test.c)。
错误2:从input.txt读取数据(应该从test.in读取)。
错误3:从tset.in读到数据(拼写错误,应该从test.in读取)。
错误4:数据写到test.ans(扩展名错误,应该是test.out)。
错误5:数据写到c:\contest\test.out(不能加路径,哪怕是相对路径。文件名应该只有8个字符:test.out)。
提示2-18:在算法竞赛中,选手应严格遵守比赛的文件名规定,包括程序文件名和输入输出文件名。不要弄错大小写,不要拼错文件名,不要使用绝对路径或相以路径。
利用文件是一种好的自我测试方法,但如果比赛要求采用标准输入输出,就必须在自我测试完毕之后删除重定向语句。
下面来介绍一种方法可以在本机测试时用文件重定向,但一旦提交到比赛,就自动“删除”重定向语句。代码如下:
程序2-8 数据统计(重定向版)
#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("x = %d, min = %d, max = %d\n", x, min, max);
*/
n++;
}
printf("%d %d %.3lf\n", min, max, (double)s/n);
return 0;
}
上面是一份典型的比赛代码,包含了几个特别的地方:
(1)重定向的部分被写在了#ifdef和#endif中。它的含义是:只有定义了符号LOCAL,才编译两条freopen语句。
(2)输出中间结果的printf语句写在注释中——它在最后版本的程序中不应该出现,但是又舍不得删除它(万一发现了新的bug,需要再次用它输出中间信息)。把它注释化的好处是:一旦需要的时候,把注释符去掉即可。
上面的代码在程序首部就定义了符号LOCAL,因此在本机测试时使用重定向方式读写文件。如果比赛要求读写标准输入输出,只需在提交之前删除#define LOCAL即可。一个更重好的方法是在编译选项而不是程序里定义这个LOCAL符号,这样在提交之前不需要修改程序。
如果比赛要求用文件输入输出,但禁止用重定向的方式,程序如下:
程序2-9 数据统计(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, min = INF, max = -INF, s = 0;
while(fscanf(fin, "%d", &x) == 1) {
s += x;
if(x < min) min = x;
if(x > max) max = x;
n++;
}
fprintf(fout, "%d %d %.3lf\n", min, max, (double)s/n);
fclose(fin);
fclose(fout);
return 0;
}
说明:(1)本程序先声明变量fin和fout,把scanf改成fscanf,第一个参数为fin;把printf改成fprintf,第一个参数为fout,最后执行fclose,关闭两个文件。
(2)重定向和fopen的区别。重定向的方法写起来简单、自然,但是不能同时读写文件和标准输入输出;fopen的写法稍显繁琐,但是灵活性比较大。如果想把fopen版的程序改成读写标准输入输出,只需赋值fin=stdin;fout=stdout;即可,不要想调用fopen和fclose。
2.4 小结与习题
2.4.1 输出技巧
首先是输出技巧。通过对程序2-1进行小小的改动来实现输出2,4,6,8,…,2n,每个一行。为了方便,现把程序复制如下:
#include <stdio.h>
int main()
{
int i, n;
scanf("%d", &n);
for(i = 1; i <= n; i++)
printf("%d\n", i);
return 0;
}
任务1:修改第7行,不修改第6行。
解答:
修改第7行的如下:
printf("%d\n", 2i);
任务2:修改第6行,不修改第7行。
解答:
修改第6行的如下:
for(i = 2; i <= 2n; i+=2)
2.4.2 浮点数陷阱
“!=”运算符表示“不相等”,则下面的程序的运行结果是什么?
#include <stdio.h>
int main()
{
double i;
for(i=0;i!=10;i+=0.1)
printf("%.1lf\n",i);
return 0;
}
说明:对于i可以达到10.0,但永远不会与10相等,所以for循环是一个死循环。对于float和dobule类型的数据不能直接用 ==和!=来比较大小,即不要测试精确的浮点型数值,需要用精度比较,来测试一个可接受的数值范围。课本P85页有这样的说明。例如,
if(fabs(sales_tax-0.065)<=0.0001)
....
2.4.3 64位整数
题目:输入正整数n,统计它的正因子个数。n≤1012。例如,n=30时,输出应该为8。
【分析】
(1)如果i是n的约数,则n/i也是n的约数。所以可以从1 枚举到。
(2)n太大(n≤1012),超过了int类型的表示范围(-231~231-1,比-2×29~2×29略宽)。
(3)有一种比int更大的类型,称为long long,它的表示范围是-263~263-1,比-1019~-1019略窄。在VC6.0中(没有long long数据类型),如果要定义一个long long数据类型的变量a(64位整数)应该定义如下:
__int64 a;
输入输出的时候用 %I64d,如:
scanf("%I64d",&a);
printf("%I64d",a);
本题的程序如下:
#include <stdio.h>
#include <math.h>
int main()
{
__int64 n, /* 注意int64之前是两个下划线(_)*/
x,count=0; /* 假设x是n的因子,count为计数器 */
scanf("%I64d",&n);
for(x=1;x<=(__int64)sqrt(n);x++){
if(n%x == 0) count += 2; /* 之所以+2,是因为x和n/x都是n的因子 */
if(n/x == x) count = count-1;
}
printf("%I64d\n", count);
return 0;
}
2.4.4 C++中的输入输出
“A+B”问题:输入若干对整数,输出每对之和。通过经典的“A+B”问题,来用C++语言来研究输入输出。
(1)方法一
#include <cstdio> //功能和C中的stdio.h很接近,但有些许不同
using namespace std;
int main()
{
int a,b;
while(scanf("%d%d", &a, &b)==2) printf("%d\n", a+b);
return 0;
}
上面的C++程序很像C程序。唯一的区别是原来的stdio.h变成了cstdio,并且多了一条语句using namespace std。这是一个普遍规律——要在C++程序中使用C语言头文件,请去掉扩展名.h,并在最前面加上小字母c。例如,stdio.h在C++中的新名字是cstdio。另外,第一行中以//开头的是C++特有的“单行注释”,它和C中的传统注释(/和/)可以混合使用。
(2)方法二(不用记住%d、%lf等恼人的占位符)
#include <iostream> //头文件iostream包含了对输入输出流的定义
using namespace std;
int main(){
int a,b;
while(cin>>a>>b) cout<<a+b<<"\n";
return 0;
}
下面用输入输出文件流来修改方法二,程序如下:
#include <fstream>
using namespace std;
ifstream fin("aplusb.in");
ofstream fout("aplusb.out");
int main(){
int a, b;
while(fin>>a>>b) fout<<a+b<< "\n";
return 0;
}
说明:(1)在C++中,将数据从一个对象到另一个对象的流动抽象称为“流”。文件流是以外存文件为输入输出对象的数据流。输出文件流是从内存流向外存文件的数据,输入文件流是从外存文件流向内存的数据。每一个文件流都有一个内存缓冲区与之对应。
在C++的I/0类库中定义了几种文件类,专门用于对磁盘文件的输入输出操作,有3个用于文件操作的文件类:
①ifstream(input file stream)类,它是从istream类派生的。用来支持从磁盘文件的输入,即从硬盘到内存。
可以用下面的方法建立一个输入文件流对象:
ifstream fin("aplusb.in");
其中fout为ofstream类(输出文件流类)的对象。
②ofstream(output file stream)类,它是从ostream类派生的。用来支持向磁盘文件的输出,即从内存到硬盘。
可以用下面的方法建立一个输出文件流对象:
ofstream fout("aplusb.out");
其中fout为ofstream类(输出文件流类)的对象。
③fstream类,它是从iostream类派生的,用来支持对磁盘文件的输入输出。
(2)文件流与文件的概念的区别在于文件流不是由若干个文件组成的流,文件流本身不是文件,而只是以文件为输入输出对象的流,若要对磁盘文件输入输出,就必须通过文件流来实现。
(3)文件流中的两个重要的运算符:
①析取器(>>)从流中输入数据。比如本程序中有一个输入流(fin),fin>>a>>b表示从输入流中读取两个指定类型(即变量a和b的类型)的数据。
②插入器(<<)向流中输出数据。比如本程序中有一个输出流(fout),fout<<a+b<< "\n";表示把a+b的值和换行字符("\n")输出到文件输出流fout中。
如果想再次使用cin和cout,是否要逐个把程序中的所有fin和fout替换为cin和cout?不用这么麻烦,只需要把fin和fout的声明语句去掉,并加上这样两行即可:
#define fin cin
#define fout cout
2.4.5 ACM题目中的输入输出
1.ACM题目特点
由于ACM竞赛题目的输入数据和输出数据一般有多组(不定),并且格式多种多样,所以,如何处理题目的输入输出是对大家的一项最基本的要求。这也是困扰初学者的一大问题。经常有刚接触ACM在线测试系统的同学抱怨:“为什么我在OJ(Online Judge)上连简单的A+B也通不过?”
先给一个竞赛样例:
题目名称:A+B for Input-Output Practice (I)
链接地址:http://acm.hdu.edu.cn/showproblem.php?pid=1089
Time Limit: 1 Seconds Memory Limit:32768K
Problem Description
Your task is to Calculate a + b.
Too easy?! Of course! I specially designed the problem for acm beginners.
You must have found that some problems have the same titles with this one, yes, all these problems were designed for the same aim.
Input
The input will consist of a series of pairs of integers a and b, separated by a space, one pair of integers per line.
Output
For each pair of input integers a and b you should output the sum of a and b in one line, and with one line of output for each line in input.
Sample Input
1 5
10 20
Sample Output
6
30
初学者很常见的一种写法:
#include <stdio.h>
void main()
{
int a,b;
scanf(“%d %d”,&a,&b);
printf(“%d”,a+b);
}
但上述代码提交上述到OJ上不能通过,是什么原因呢?这就是下面需要解决的问题。下面来分类进行介绍。
2.基本输入
(1)第一类输入
输入不说明有多少个Input Block,以EOF为结束标志,例如上面的例子。
源代码如下:
#include <stdio.h>
int main()
{
int a,b;
while(scanf("%d %d",&a, &b) != EOF) printf("%d\n",a+b);
}
本类问题的输入方案为:
①C语法
while(scanf("%d %d",&a, &b) != EOF)
{
....
}
②C++语法
while( cin >> a >> b )
{
....
}
说明:
①scanf函数返回值就是读出的变量个数,如:scanf("%d %d", &a, &b ); ,
如果只有一个整数输入,返回值是1;如果有两个整数输入,返回值是2;如果一个都没有,则返回值是-1。
②EOF是一个预定义的常量,等于-1。
(2)第二类输入
输入一开始就会说有n个Input Block,下面接着是输入n个Input Block。
参见:HDOJ_1090(http://acm.hdu.edu.cn/showproblem.php?pid=1090)。
源代码如下:
#include <stdio.h>
int main()
{
int n,i,a,b;
scanf("%d",&n);
for(i=0;i<n;i++)
{
scanf("%d %d",&a, &b);
printf("%d\n",a+b);
}
}
本类问题的输入方案为:
①C语法
scanf("%d",&n);
for( i=0 ; i<n ; i++ )
{
....
}
②C++语法
cin >> n;
for( i=0 ; i<n ; i++ )
{
....
}
(3)第三类输入
输入不说明有多少个Input Block,但以某个特殊输入为结束标志。
参见:HDOJ_1091(http://acm.hdu.edu.cn/showproblem.php?pid=1091)。
源代码如下:
#include <stdio.h>
int main()
{
int a,b;
while(scanf("%d %d",&a, &b) &&(a!=0 && b!=0))
printf("%d\n",a+b);
}
上面程序有什么问题?
本类问题的输入方案为:
①C语法
while(scanf(“%d”,&n) !=EOF && n!=0 )
{
....
}
②C++语法
while( cin >> n && n != 0 )
{
....
}
(4)第四类输入
以上几种情况的组合,可以参照如下网页,进行上机实验。
①http://acm.hdu.edu.cn/showproblem.php?pid=1092
②http://acm.hdu.edu.cn/showproblem.php?pid=1093
③http://acm.hdu.edu.cn/showproblem.php?pid=1094
(5)第五类输入
输入一整行的字符串。
参见:HDOJ_1048(http://acm.hdu.edu.cn/showproblem.php?pid=1048)。
本类问题的输入方案为:
①C语法
char buf[20];
gets(buf);
②C++语法
如果用string buf;来保存,则用语句getline( cin , buf ); 。
如果用char buf[ 255 ]; 来保存,则用语句 cin.getline( buf, 255 );。
说明:①scanf("%s%s",str1,str2),在多个字符串之间用一个或多个空格分隔;
②若使用gets函数,应为gets(str1); gets(str2); 字符串之间用回车符作分隔。
③通常情况下,接受短字符用scanf函数,接受长字符用gets函数。
④getchar函数每次只接受一个字符,经常c=getchar()这样来使用。
⑤cin.getline的用法
getline 是一个函数,它可以接受用户的输入的字符,直到已达指定个数,或者用户输入了特定的字符。它的函数声明形式(函数原型)如下:
istream& getline(char line[], int size, char endchar='\n');
不用管它的返回类型,来关心它的三个参数:
char line[]就是一个字符数组,用户输入的内容将存入在该数组内。
int size表示最多接受几个字符?用户超过size的输入都将不被接受。
char endchar表示当用户输入endchar指定的字符时,自动结束。默认是回车符。
结合后两个参数,getline可以方便地实现:用户最多输入指定个数的字符,如果超过,则仅指定个数的前面字符有效,如果没有超过,则用户可以通过回车来结束输入。
char name[4];
cin.getline(name,4,'\n');
由于endchar 默认已经是 '\n',所以后面那行也可以写成:cin.getline(name,4);。
思考:以下题目属于哪一类输入?
http://acm.hdu.edu.cn/showproblem.php?pid=1018
http://acm.hdu.edu.cn/showproblem.php?pid=1013
3.基本输出
(1)第一类输出
一个Input Block对应一个Output Block,Output Block之间没有空行。
参见:HDOJ_1089(http://acm.hziee.edu.cn/showproblem.php?pid=1089)。
本类问题的输出方案为:
①C语法
{
....
printf("%d\n",ans);
}
②C++语法
{
...
cout << ans << endl;
}
(2)第二类输出
一个Input Block对应一个Output Block,每个Output Block之后都有空行。
参见:HDOJ_1095(http://acm.hdu.edu.cn/showproblem.php?pid=1095)。
源代码如下:
#include <stdio.h>
int main()
{
int a,b;
while(scanf("%d %d",&a, &b) != EOF) printf("%d\n\n",a+b);
}
解决办法如下:
①C语法
{
....
printf("%d\n\n",ans);
}
②C++语法
{
...
cout << ans << endl << endl;
}
(3)第三类输出
一个Input Block对应一个Output Block,Output Block之间有空行。
参见:HDOJ_1096(http://acm.hdu.edu.cn/showproblem.php?pid=1096)。
源代码如下:
#include <stdio.h>
int main()
{
int icase,n,i,j,a,sum;
scanf("%d",&icase);
for(i=0;i<icase;i++)
{
sum=0;
scanf("%d",&n);
for(j=0;j<n;j++)
{
scanf("%d",&a);
sum+=a;
}
if(i<icase-1)
printf("%d\n\n",sum);
else
printf("%d\n",sum);
}
}
解决办法如下:
①C语法
for (k=0;k<count;k++)
{
while (…)
{
printf("%d\n",result);
}
if(k!=count-1) printf("\n");
}
②C++语法
类似,输出语句换一下即可。
思考:
以下题目属于哪一类输出?
http://acm.hdu.edu.cn/showproblem.php?pid=1016
http://acm.hdu.edu.cn/showproblem.php?pid=1017
4.初学者常见问题
(1)编译错误
①Main函数必须返回int类型(正式比赛);
②不要在for语句中定义类型;
③如果__int64不支持,可以用long long代替;
④使用了汉语的标点符号;
⑤itoa不是一个标准的C函数,它能将整数转换为字符串,是Windows特有的,如果写跨平台的程序,使用与ANSI标准兼容的sprintf()函数。
int num = 100;
char str[25];
sprintf(str, "%d" , num);
⑥另外,拷贝程序容易产生错误。
(2)C语言处理“混合数据”的问题
参见:HDOJ_1170(http://acm.hdu.edu.cn/showproblem.php?pid=1170)。
常见代码:
……
scanf("%d\n",&icase);
for (i=0;i<icase;i++)
{
scanf("%c%d%d",&opera,&num1,&num2);
……
}
……
(3)printf和cout混用的问题
以下的程序输出什么?
#include <stdio.h>
#include <iostream.h>
int main()
{
int j=0;
for(j=0;j<5;j++)
{
cout<<"j=";
printf("%d\n",j);
}
return 0;
}
程序输出为
0
1
2
3
j=j=j=j=j=。
为什么?因为一个带缓冲输出(cout),而一个不带缓冲输出(printf)。
5.OJ上提交代码后出现的提示
在OJ上提交代码后,会弹出处理页面,然后单击处理页面上的Status(状态)链接,会弹出状态页。
状态页面显示了Run ID、Submit time、Judge Status(评测状态)、Problem ID、Language、Run time(时间开销)、Run memory(内存开销)、User Name等几个信息。Judge Status(评测状态)一般有以下情况。
Accepted:恭喜,通过。
Presentation Error:输出格式错误。
Wrong Answer:答案错误。
Runtime Error:程序运行时发生错误,多为数组访问越界。
Time Limit Exceeded:超内存错误,程序运行使用的内存超过限制的内存用量。
Compile Error:编译错误,源代码中有语法错误。
6.学习方式
(1)通过练习->总结->练习->总结->……的过程,使得自己编程水平得到提高;
(2)可以在http://acm.hdu.edu.cn或其它网站上多多练习;
(3)可以去杭电ACM论坛或其它ACM论坛上,与别人分享经验;
(4)有问题可以在google、baidu上去搜索。
2.4.6 小结
现在对本章介绍的循环总结如下:
(1)介绍了在编程中的一些经验,如使用计算器、累加器、以及“当前最小/最大值”这样的中间变量。使用printf输出一些关键的中间变量能有效地帮助我们了解程序执行过程、发现错误。
(2)遇到问题需要自己分析和设计,可以先写出“伪代码”,然后转换成程序。伪代码是为了让思路更清晰,突出主要矛盾。
(3)编写好程序后,测试显得相当重要。如前面的例子中几乎都有陷阱:运算结果溢出、运算时间过长等。对于海量的输入输出问题可以通过文件得到缓解。
(4)再次强调:编程不是看书看会的,也不是听课听会的,而是练会的。
2.4.7 上机练习
注意,本章的题目需用文件输入输出。如果题目代号为abc,那么输入文件为abc.in,输出文件为abc.out。如果对文件操作不熟练,请尽量把fopen、freopen两种方法都尝试一下。
习题2-1 位数(digit)
输入一个不超过109的正整数,输出它的位数。例如12735的位数是5。请不要使用任何数学函数,只用四则运算和循环语句实现。
【分析】
采取的策略是不断地整除10,检查商是否大于10。
程序如下:
#include <stdio.h>
void main() {
unsigned long n; /* 无符号的长整型数,占4字节存储空间 */
int digit=1; /* 位数 */
scanf("%ld",&n);
for (;n>=10;) {
n=n/10;
digit=digit+1;
}
printf("%d",digit);
return;
}
习题2-2 水仙花数(daffodil)
输出100~999中的所有水仙花数。若3位数ABC=A2+B2+C2,则称其为水仙花数。例如153=13+53+33,所以153是水仙花数。
【分析】
采取的策略是穷举100~999之内的所有数据,检查是否满足题目的要求。
程序如下:
#include <stdio.h>
void main() {
int n,
a,b,c; /* n的百、十、个位 */
for (n=100; n<=999; n=n+1){
a = n / 100;
b = (n-a*100)/10; /* 也可以写成 b=(n%100)/10; */
c = n % 10;
if (n==a*a*a+b*b*b+c*c*c) /* 算术运算的优先级要高于逻辑运算 */
printf("%d\n",n);
}
return;
}
习题2-3 韩信点兵(hanxi)
相传韩信才智过人,从不直接清点自己军队的人数,只要让士兵先后以三人一排、五人一排、七人一排地变换队形,而他每次只掠一眼队伍的排尾就知道总人数了。输入3个非负整数a,b,c,表示每种队形排尾的人数(a<3,b<5,c<7),输出总人数的最小值(或报告无解)。已知总人数不小于10,不超过100。
样例输入:2 1 6
样例输出:41
样例输入:2 1 3
样例输出:No answer
【分析】
采取的策略是用穷举法解题;总人数(10,100]。
程序如下:
#include <stdio.h>
void main () {
int a, /*三人一排,余a人*/
b, /*五人一排,余b人*/
c, /*七人一排,余c人*/
sum; /*总人数*/
scanf("%d%d%d",&a,&b,&c);
for (sum=10; sum<=100; sum=sum+1) {
if ((sum%3==a) &&(sum%5==b) &&(sum%7==c)) {
printf("%d",sum);
break;
}
}
if (sum>100) printf("No answer");
return;
}
习题2-4 倒三角形(triangle)
输入正整数n≤20,输出一个n层的倒三角形。例如n=5时输出如下:
#########
#######
#####
###
#
【分析】
该图形一共有n行,这次要考虑每行中,先输出若干个空格,所以,其外循环为:
for( i=1;i<=n;i++){
输出若干个空格
输出若干#
换行
}
下面列出在第i行,其空格数,#字符数与i的关系为:
行i 空格数 #字符数
1 0 2n-1
2 1 2n-3
…
i i-1 2n-2i+1
…
n-1 n-2 3
n n-1 1
即第i行的空格数为i-1个,#字符个数为2n-2i+1。#字符个数为2n-2i+1是如何得来的呢?如果将行从第n行记为第1行,第n-1行记为第2行,…,则原第i行现在记为第n-i+1行。现在行号为第1行的#字符的个数为1,现在行号为第2行的#字符的个数为3=2*2-1,所以原第i行现在记为第n-i+1行,输出的#字符的个数为2(n-i+1)-1=2n-2i+1。
所以在第i行输出空格和输出#字符的内循环分别为:
for(j=1;j<=i-1;j++)
printf(" ");
for(j=1;j<=2n-2i+1;j++)
printf("%c",'#');
合起来,构成一个完整程序。
程序如下:
#include <stdio.h>
void main () {
int n, /* 输出n行; n<=20 */
i, /* 打印第i行 */
j;
scanf("%d",&n);
for (i=1; i<=n; i=i+1) {
/* 在第i行,打印(i-1)个空格 */
for (j=1; j<=i-1; j=j+1) printf(" ");
/* 在第i行,打印(2*n-2*i+1)个# */
for (j=1; j<=(2*n-2*i+1); j=j+1) printf("#");
printf("\n"); /* 输出结束后换行,否则所有的#号在同一行输出 */
}
return;
}
引申1:用for循环编程画出下列图形:
M
MM
MMM
MMMM
MMMMM
MMMMMM
MMMMMMM
MMMMMMMM
MMMMMMMMM
MMMMMMMMMM
【分析】
该图形一共有10行,每一行增加一个字符,所以,应循环10次,每次输出一行,其循环模式为:
for( i=1;i<=10;i++){
输出第i行
换行
}
“输出第i行”是在for循环中的一个小循环。每次执行“输出第i行”,其长度都是不一样的,但长度的变化正好与循环变量i同步,故可以依赖于i。注意到第i行的M字符数与i的关系。
行 i M数
1 1 1
2 2 2
3 3 3
…
10 10 10
所以,可以得到“输出第i行”的循环为:
for(j=1;j<=i;j++) printf(“%c”,’M’)
完整的程序如下:
#include <stdio.h>
main(){
int i,j;
for(i=1;i<=10;i++){
for(j=1;j<=i;j++)
printf("%c",'M');
printf("\n");
}
}
说明:对付这种字符图形,一般用两重循环,外循环遍历所有行,内循环遍历行中每个字符。
引申2:输出下列图形:
MMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMM
MMMMMMMMMMMMM
MMMMMMMMMMM
MMMMMMMMM
MMMMMMM
MMMMM
MMM
M
【分析】
该图形一共有10行,这次要考虑每行中,先输出若干个空格,所以,其外循环为:
for( i=1;i<=10;i++){
输出若干个空格
输出若干M
换行
}
下面列出在第i行,其空格数,M数与i的关系为:
行 i M数
1 0 19
2 1 17
3 2 15
4 3 13
…
10 9 1
即第i行的空格数为i-1个,M个数为21-2i。即在第i行输出空格和输出M字符的内循环分别为:
for(j=1;j<=i-1;j++)
printf(“ ”);
for(k=1;k<=21-2i;k++)
printf(“%c”,’M’);
合起来,构成一个完整程序。
完整程序如下:
#include <stdio.h>
int main()
{
int i,j,k;
for(i=1;i<=10;i++)
{
for(j=1;j<=i-1;j++)
printf(" ");
for(k=1;k<=21-2*i;k++)
printf("%c",'M');
printf("\n");
}
return 0;
}
引申3:输出下列图形:
A
ABC
ABCDE
ABCDEFG
ABCDEFGHI
ABCDEFGHIJK
ABCDEFGHIJKLM
ABCDEFGHIJKLMNO
ABCDEFGHIJKLMNOPQ
ABCDEFGHIJKLMNOPQRS
【分析】
该图形一共有10行,这次要考虑每行中,先输出若干个空格,所以,其外循环为:
for( i=1;i<=10;i++){
输出若干个空格
输出若干字符
换行
}
如果要输出A起头依序的n(n<27)个字母,可以为:
for(ch='A';ch<'A'+n;++ch)
printf("%c",ch);
下面分析每一行中的空格数与字符数与第i行之间的关系:
行 i M数
1 9 1
2 8 3
3 7 5
4 6 7
…
10 0 19
即第i行的空格数据为10-i个,字符数为2i-1。因此,输出空格数和字符数的内循环分别为:
for(j=1;j<=10-i;++j) /输出空格数/
printf(" ");
for(ch='A';ch<'A'+2*i-1;++ch)
printf("%c",ch);
合起来,构成一个完整程序。
完整程序如下:
#include <stdio.h>
int main()
{ int i,j;
char ch;
for(i=1;i<=10;i++)
{
for(j=1;j<=10-i;++j) /*输出空格数*/
printf(" ");
for(ch='A';ch<'A'+2*i-1;++ch) /*输出字符*/
printf("%c",ch);
printf("\n");
}
return 0;
}
引申4:输出下列图形:
*
***
*****
*******
*****
***
*
上面的图形可以分成两部分:
(1)
*
***
*****
*******
与引申3的分析一样,输出空格数和字符数的内循环分别为:
for(j=1;j<=4-i;++j) /*输出空格数*/
printf(" ");
for(k=1;k<=2*i-1;++k) /*输出“*”号*/
printf("*");
(2)
*****
****
与引申2的分析一样,输出空格数和字符数的内循环分别为:
for(j=1;j<=i;++j) /*输出空格数*/
printf(" ");
for(k=1;k<=7-2*i;++k) /*输出“*”号*/
printf("*");
合起来,构成一个完整程序。
完整程序如下:
#include <stdio.h>
main()
{ int i,j,k;
for(i=1;i<=4;i++)
{
for(j=1;j<=4-i;++j) /*输出空格数*/
printf(" ");
for(k=1;k<=2*i-1;++k) /*输出“*”号*/
printf("*");
printf("\n");
}
for(i=1;i<=3;i++)
{
for(j=1;j<=i;++j) /*输出空格数*/
printf(" ");
for(k=1;k<=7-2*i;++k) /*输出“*”号*/
printf("*");
printf("\n");
}
}
习题2-5 统计(stat)
输入一个正整数n,然后读取n个正整数a1,a2,…,an,最后再读一个正整数m。统计a1,a2,…,an中有多少个整数的值小于m。提示:如果重定向和fopen都可以使用,哪个比较方便?
【分析一】
在没有学习过“数组”概念以前,想要做出本题,需要扫描2次输入文件。第一次是为了读到m;第二次是为了读出n个输入。
输入文件:stat.in.txt。
文件格式描述如下:
每行一个正整数,各行分别是:n, a1, a2, ..., an, m;例如:
n
a1
a2
...
an
m
输出文件:stat.out.txt
程序如下:
#include <stdio.h>
void main() {
int m,n,i,
newData, /*新读到的数据*/
count; /*计数器*/
freopen("stat.in.txt","r",stdin);
freopen("stat.out.txt","w",stdout);
scanf("%d",&n);
for (i=1; i<=n; i=i+1) scanf("%d",&newData); /*跳过n个输入数据*/
scanf("%d",&m);
/*秘诀就在这里:再次打开输入文件,再次向程序输入数据*/
freopen("stat.in.txt","r",stdin);
scanf("%d",&n);
count=0;
for (i=1; i<=n; i=i+1) {
scanf("%d",&newData);
if (newData<m) count=count+1;
}
printf("%d",count);
return;
}
【分析二:统计的变种1】
如果没有学习过“数组”的相关知识,那么本题实难解答。因此稍稍修正一下题目。
题目修改如下:
读入一个正整数m,再读入一个正整数n;然后读取n个正整数a1,a2,...,an,统计a1,a2
,...,an,中有多少个整数的值小于m。
这是使用键盘输入的版本。本题主要考察循环语句的使用,以及读入大量数据的操作。
程序如下:
#include <stdio.h>
void main () {
int n,i,m,
newData, /*新输入的数据*/
count; /*计数器*/
scanf("%d",&m);
scanf("%d",&n);
count=0;
for (i=1; i<=n; i=i+1) {
scanf("%d",&newData);
if (newData<m) count=count+1;
}
printf("有 %d 个整数的值小于 %d。",count,m);
return;
}
【分析三:统计的变种2】
如果没有学习过“数组”的相关知识,那么本题实难解答。因此稍稍修正一下题目。
题目修改如下:
读入一个正整数m,再读入一个正整数n;然后读取n个正整数a1,a2,...,an,统计a1,a2
,...,an中有多少个整数的值小于m。
这是使用重定向输入/输出的版本。
输入数据文件:统计的变种.in.txt。
输出数据文件:统计的变种.out.txt。
本题主要考察循环语句的使用,以及读入大量数据的操作。
程序如下:
#include <stdio.h>
void main () {
int n,i,m,
newData, /*新输入的数据*/
count; /*计数器*/
freopen("统计的变种.in.txt","r",stdin);
freopen("统计的变种.out.txt","w",stdout);
scanf("%d",&m);
scanf("%d",&n);
count=0;
for (i=1; i<=n; i=i+1) {
scanf("%d",&newData);
if (newData<m) count=count+1;
}
printf("有 %d 个整数的值小于 %d。",count,m);
return;
}
习题2-6 调和级数(harmony)
输入一个正整数n,输出H(n)=!的值,保留3位小数。例如n=3时答案为1.833。
【分析】
本题主要考察循环语句的使用。秘诀是要从绝对值最小的项开始向绝对值大的项累加。
程序如下:
#include <stdio.h>
void main () {
unsigned int n; /*我们认为int已经足够表达所谓的“正整数”范围*/
double sum=0;
scanf("%u",&n);
for (; n>=1; n=n-1) {
sum=sum+1.0/n;
}
printf("%.3f",sum);
return;
}
习题2-7 近似计算(approximation)
计算,直到最后一项小于10-6。
【分析】
本题主要考察循环语句的使用。秘诀是要从绝对值最小的项开始向绝对值大的项累加。
程序如下:
#include <stdio.h>
void main () {
int n;
double sum=0;
int symbol;
n=1000001; /*显然,1/1000001小于0.000001*/
for (; n>=1;n=n-2){
if ((n+1)/2%2==1) symbol=1;
else symbol=-1;
sum=sum+symbol*(1.0/n);
}
printf("%.20lf",sum);
return;
}
习题2-8 子序列的和(subsequence)
输入两个正整数n<m<106,输出#,保留5位小数。例如,n=2,m=4时答案是0.42361;n=65536,m=655360时答案为0.00001。注意:本题有陷阱。
【分析】
本题主要考察循环语句的使用。秘诀是要从绝对值最小的项开始向绝对值大的项累加。所谓的陷阱,是指m的平方可能会导致整数溢出。所以,另一个秘诀是,把m和n定义为double。
程序如下:
#include <stdio.h>
void main () {
double m,n;
double sum=0;
scanf("%lf%lf",&n,&m); /* 假设用户输入的n总是不大于m */
for (; m>=n; m=m-1) {
sum=sum+1.0/(m*m); /*注意,不能写成 sum=1/m*m */
}
printf("%.5lf",sum);
return;
}
习题2-9 分数化小数(decimal)
输入正整数a,b,c,输出a/b的小数形式,精确到小数点后c位。a,b≤106,c≤100。例如a=1,b=6,c=4时应输出0.1667。
【分析】
注意到a,b,c均为正整数,所以输出结果的小数点后至少会有1位。另外,从本题的示例输出中可以感知到,小数的最后一位,是要考虑到四舍五入进位的。
采取的策略是模拟整数除法的过程,利用整除所得的余数,不断地对目标结果求精。
程序如下:
#include <stdio.h>
void main() {
unsigned int a,b,c, x, y; /* 小数点后的位数 */
scanf("%d%d%d",&a,&b,&c);
x=a/b;
printf("%d.",x);
x=a%b;
y=1;
for (; y<=c; y=y+1) {
x=x*10;
if (y!=c)
printf("%d",x/b);
else {
/* 输出小数的最后一位时,考虑到下一位的数值是否会产生四舍五入的进位 */
if (x%b*10/b>=5)
printf("%d",x/b+1);
else
printf("%d",x/b);
}
x=x%b;
}
return;
}
习题2-10 排列(permutation)
用1,2,3,…,9组成3个三位数abc,def和ghi,每个数字恰好使用一次,要求abc:def:
ghi=1:2:3。输出所有解。提示:不必太动脑筋。
【分析】
采用的策略是穷举法解题。尽管说“不必太动脑筋”,但也不意味着“一点儿都不用动脑筋”。如果说三个三位数是abc, def, ghi,比率是1:2:3,那么显然d>=(2a)并且g>=3a。所以,1<=a<=3,d>=2a,g>=3a;这些论断,都可以减少电脑穷举所需扫描的范围,提高运行速度。
如果还能够提出更加精确的论断,就还能进一步提高程序的运行速度。
程序如下:
#include <stdio.h>
void main () {
int a,b,c,d,e,f,g,h,i;
for (a=1; a<=3; a=a+1)
for (b=1; b<=9; b=b+1) { /*1*/
if (b!=a)
for (c=1; c<=9; c=c+1) { /*2*/
if (c!=a && c!=b)
for (d=a*2; d<=9; d=d+1) { /*3*/
if (d!=a && d!=b && d!=c)
for (e=1; e<=9; e=e+1) { /*4*/
if (e!=a && e!=b && e!=c && e!=d)
for (f=1; f<=9; f=f+1) { /*5*/
if (f!=a && f!=b && f!=c && f!=d && f!=e)
for (g=a*3; g<=9; g=g+1) { /*6*/
if (g!=a && g!=b && g!=c && g!=d && g!=e &&g!=f)
for (h=1; h<=9; h=h+1) { /*7*/
if (h!=a && h!=b && h!=c && h!=d && h!=e && h!=f && h!=g)
for (i=1; i<=9; i=i+1) { /*8*/
if (i!=a && i!=b && i!=c && i!=d&& i!=e && i!=f && i!=g &&i!=h &&(a*100+b*10+c)*2==d*100+e*10+f && (a*100+b*10+c)*3==g*100+h*10+i)
printf("%d%d%d, %d%d%d, %d%d%d\n",a,b,c,d,e,f,g,h,i);
} /*8*/
} /*7*/
} /*6*/
} /*5*/
} /*4*/
} /*3*/
} /*2*/
} /*1*/
return;
}