算法竞赛入门经典授课教案第2章循环结构程序设计精心排版并扩充部分内容.docx
《算法竞赛入门经典授课教案第2章循环结构程序设计精心排版并扩充部分内容.docx》由会员分享,可在线阅读,更多相关《算法竞赛入门经典授课教案第2章循环结构程序设计精心排版并扩充部分内容.docx(46页珍藏版)》请在冰豆网上搜索。
算法竞赛入门经典授课教案第2章循环结构程序设计精心排版并扩充部分内容
第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、dowhile、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.1for循环
请用for循环实现输入正整数n,打印1,2,3,…,10,每个占用一行。
程序如下:
程序2-1输出1,2,3,…,n的值
#include
intmain(){
inti,n;
scanf("%d",&n);
for(i=1;i<=n;i++)
printf("%d\n",i);
return0;
}
提示2-1:
for循环的格式:
for(初始化;条件;调整)循环体;
提示2-2:
尽管for循环反复执行相同的语句,但这些语句每次的执行效果往往不同。
提示2-3:
编写程序时,要特别留意“当前行”的跳转和变量的改变。
有了for循环,可以解决一些简单的问题。
例2-1aabb。
输出所有形如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-27744问题
(1)
#include
#include
intmain(){
inta,b,n;
doublem;
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);
}
}
return0;
}
说明:
函数floor(x)返回x的整数部分,使用floor(m+0.5)==m的原因是浮点数的运算(和函数)有可能存在误差。
提示2-6:
浮点运算可能存在误差。
在进行浮点数比较时,应考虑到浮点误差。
对于四位完全平方数的还有另一个思路就是枚举平方根x,从而避免开平方操作。
补充:
(1)向下取整函数:
doublefloor(doublex);
使用floor函数。
floor(x)返回的是小于或等于x的最大整数。
如:
floor(2.5)==2 floor(-2.5)==-3
floor()是向负无穷大舍入,floor(-2.5)==-3;
(2)向上取整函数:
doubleceil(doublex);
使用ceil函数。
ceil(x)返回的是大于x的最小整数。
如:
ceil(2.5)==3 ceil(-2.5)==-2
ceil()是向正无穷大舍入,ceil(-2.5)==-2。
程序2-37744问题
(2)
#include
intmain(){
intx,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);
}
return0;
}
说明:
本程序中用到break和continue语句。
break是指直接跳出本层循环,continue是指结束本次循环,但不跳出本层循环,进入下一次循环。
2.2循环结构程序设计
例2-23n+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-43n+1问题
(1)
#include
intmain(){
intn,count=0;
scanf("%d",&n);
while(n>1){
if(n%2==1)n=n*3+1;
elsen/=2;
count++;
}
printf("%d\n",count);
return0;
}
提示2-7:
while循环的格式为:
“while(条件)循环体;”。
从程序2-4中可得,while循环与for循环可以相互转化。
在本程序中的count++,相当于一个计算器,这个功能在编程中会经常遇到。
提示2-8:
当需要统计某种事物的个数时,可以用一个变量来充当计算器。
这个程序是正确的吗?
输入:
987654321(根据题目所给范围n≤109,这是合法输入)
输出:
1
提示2-9:
不要忘记测试。
一个看上去正确的程序可能隐含错误。
提示2-10:
在观察无法找出错误时,可以用“输出中间结果”的方法查错。
while(n>1){
if(n%2==1){
n=n*3+1;
printf("%d",n);
}
else{
n/=2;
printf("%d",n);
}
count++;
}
第一次输出:
-133********
原因:
乘法溢出。
程序2-43n+1问题
(2)
#include
intmain()
{
intn,count=0;
scanf("%d",&n);
while(n>1){
if(n%2==1){
n=3*(n-1)/2+2;
count++;
}
else{
n/=2;
}
count++;
}
printf("%d\n",count);
return0;
}
例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
intmain(){
inti,j,n,S=0;
scanf("%d",&n);
for(i=1;i<=n;i++){
intfactorial=1;
for(j=1;j<=i;j++)
factorial*=j;
S+=factorial;
}
printf("%d\n",S%1000000);
return0;
}
注意:
每执行一次循环体,都要重新声明一次累乘器factorial,并初始化为1(因为是乘积,所以初始化为1,要是初始化为0,则循环后的factorial=i!
=0)。
提示2-11:
在循环体开始处定义的变量,每次执行循环体时会重新声明并初始化。
对程序进行一下测试:
输入:
100
输出:
-961703
很显然,乘法溢出了。
提示2-12:
要计算只包含加法、减法和乘法的整数表达式除以正整数n的余数,可以在每步计算之后对n取余,结果不变。
程序2-6阶乘之和
(2)
#include
#include
intmain(){
constintMOD=1000000;
inti,j,n,S=0;
scanf("%d",&n);
for(i=1;i<=n;i++){
intfactorial=1;
for(j=1;j<=i;j++)
factorial=(factorial*j%MOD);
S=(S+factorial)%MOD;
}
printf("%d\n",S);
printf("Timeused=%.2lf\n",(double)clock()/CLOCKS_PER_SEC);
return0;
}
说明:
(1)这个程序真正的特别之处在于计时函数clock()的使用。
该函数返回程序目前为止运行的时间,以毫秒为单位。
这样,在程序结束之前调用它,便可获得整个程序的运行时间。
这个时间除以常数CLOCKS_PER_SEC之后得到的值以“秒”为单位。
(2)在VC++6.0中time.h下宏定义的常量CLOCKS_PER_SEC,其值为1000。
VC++6.0中该符号常量定义如下:
#defineCLOCKS_PER_SEC1000
对于CLOCKS_PER_SEC,它用来表示一秒钟会有多少个时钟计时单元,时钟计时单元的长度为1毫秒,clock()/CLOCKS_PER_SEC就是将毫秒转化为秒。
提示2-13:
可以使用time.h和clock()函数获得程序运行时间。
常数CLOCKS_PER_SEC与操作系统有关,请不要直接使用clock()的返回值,而应总是除以CLOCKS_PER_SEC。
提示2-14:
很多程序的运行时间与规模n存在着近似的简单关系。
可以通过计时函数来发现或验证这一关系。
本节的两个例题展示了计数器、累加器的使用和循环结构程序设计中最常见的两个问题:
算术运算溢出和程序效率低下。
另外,本节中介绍的两个工具——输出中间结果和计时函数,都有是相当实用的。
程序2-6阶乘之和(3)
#include
#include
intmain(){
constintMOD=1000000;
inti,j,n,S=0;
scanf("%d",&n);
clock_tstart,finish;
start=clock();
for(i=1;i<=n;i++){
intfactorial=1;
for(j=1;j<=i;j++)
factorial=(factorial*j%MOD);
S=(S+factorial)%MOD;
}
printf("%d\n",S);
finish=clock();
doublertime=double(finish-start)/CLOCKS_PER_SEC;
printf("Timeused=%.7lf\n",rtime);
return0;
}
实际上,25!
末尾有6个0,所以从第25项开始,后面所有的项都不会影响和的末6位数字,因此,只需在程序最前面加上语句“if(n>25)n=25;”即可。
2.3文件操作
例2-4数据统计。
输入一些整数,求出它们的最小值、最大值和平均值(保留3位小数)。
输入保证这些数都是不超过1000的整数。
样例输入:
28351736
样例输出:
184.375
【分析】
如果是先输入整数n,然后输入n个整数,相信能写出程序。
关键在于:
整数的个数是不确定的。
下面给出程序:
程序2-7数据统计(错误)
#include
intmain(){
intx,n=0,min,max,s=0;
while(scanf("%d",&x)==1){
s+=x;
if(xif(x>max)max=x;
n++;
}
printf("%d%d%.3lf\n",min,max,(double)s/n);
return0;
}
说明:
(1)scanf函数的返回值是成功输入的变量个数。
当输入结束时,scanf无法再次读取x,将返回0。
(2)在测试时,输入28351736,按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数据统计(重定向版)
#defineLOCAL
#include
#defineINF1000000000
intmain(){
#ifdefLOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif
intx,n=0,min=INF,max=-INF,s=0;
while(scanf("%d",&x)==1){
s+=x;
if(xif(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);
return0;
}
上面是一份典型的比赛代码,包含了几个特别的地方:
(1)重定向的部分被写在了#ifdef和#endif中。
它的含义是:
只有定义了符号LOCAL,才编译两条freopen语句。
(2)输出中间结果的printf语句写在注释中——它在最后版本的程序中不应该出现,但是又舍不得删除它(万一发现了新的bug,需要再次用它输出中间信息)。
把它注释化的好处是:
一旦需要的时候,把注释符去掉即可。
上面的代码在程序首部就定义了符号LOCAL,因此在本机测试时使用重定向方式读写文件。
如果比赛要求读写标准输入输出,只需在提交之前删除#defineLOCAL即可。
一个更重好的方法是在编译选项而不是程序里定义这个LOCAL符号,这样在提交之前不需要修改程序。
如果比赛要求用文件输入输出,但禁止用重定向的方式,程序如下:
程序2-9数据统计(fopen版)
#include
#defineINF1000000000
intmain(){
FILE*fin,*fout;
fin=fopen("data.in","rb");
fout=fopen("data.out","wb");
intx,n=0,min=INF,max=-INF,s=0;
while(fscanf(fin,"%d",&x)==1){
s+=x;
if(xif(x>max)max=x;
n++;
}
fprintf(fout,"%d%d%.3lf\n",min,max,(double)s/n);
fclose(fin);
fclose(fout);
return0;
}
说明:
(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,每个一行。
为了方便,现把程序复复制如下:
1#include
2intmain()
3{
4inti,n;
5scanf("%d",&n);
6for(i=1;i<=n;i++)
7printf("%d\n",i);
8return0;
9}
任务1:
修改第7行,不修改第6行。
解答:
修改第7行的如下:
7printf("%d\n",2*i);
任务2:
修改第6行,不修改第7行。
解答:
修改第6行的如下:
6for(i=2;i<=2*n;i+=2)
2.4.2浮点数陷阱
“!
=”运算符表示“不相等”,则下面的程序的运行结果是什么?
#include
intmain()
{
doublei;
for(i=0;i!
=10;i+=0.1)
printf("%.1lf\n",i);
return0;
}
说明:
对于i可以达到10.0,但永远不会与10相等,所以for循环是一个死循环。
对于float和dobule类型的数据不能直接用==和!
=来比较大小,即不要测试精确的浮点型数值,需要用精度比较,来测试一个可接受的数值范围。
如:
for(i=0;fabs(10-i)>1e-5;i+=0.1)
2.4.364位整数
题目:
输入正整数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更大的类型,称为longlong,它的表示范围是-263~263-1,比-1