算法竞赛入门经典作者刘汝佳.docx
《算法竞赛入门经典作者刘汝佳.docx》由会员分享,可在线阅读,更多相关《算法竞赛入门经典作者刘汝佳.docx(22页珍藏版)》请在冰豆网上搜索。
算法竞赛入门经典作者刘汝佳
算法竞赛-入门经典-作者刘汝佳
算法竞赛-入门经典-作者刘汝佳.doc
第1部分语言篇
第1章程序设计入门
学习目标
☑熟悉C语言程序的编译和运行
☑学会编程计算并输出常见的算术表达式的结果
☑掌握整数和浮点数的含义和输出方法
☑掌握数学函数的使用方法
☑初步了解变量的含义
☑掌握整数和浮点数变量的声明方法
☑掌握整数和浮点数变量的读入方法
☑掌握变量交换的三变量法
☑理解算法竞赛中的程序三步曲:
输入、计算、输出
☑记住算法竞赛的目标及其对程序的要求
计算机速度快,很适合做计算和逻辑判断工作。
本章首先介绍顺序结构程序设计,其基本思路是:
把需要计算机完成的工作分成若干个步骤,然后依次让计算机执行。
注意这里的“依次”二字——步骤之间是有先后顺序的。
这部分的重点在于计算。
接下来介绍分支结构程序设计,用到了逻辑判断,根据不同情况执行不同语句。
本章内容不复杂,但是不容忽视。
实验2:
修改程序1-1,输出5×6的结果
实验3:
修改程序1-1,输出8÷4的结果
实验4:
修改程序1-1,输出8÷5的结果
直接把“1+2”替换成“3+4”即可顺利解决实验1,但读者很快就会发现:
无法在键盘上找到乘号和除号。
解决方法是:
用星号“*”代替乘号,而用正斜线“/”代替除号。
这样,4个实验都顺利完成了。
等一下!
实验4的输出结果居然是1,而不是正确答案1.6。
这是怎么回事?
计算机出问题了吗?
计算机没有出问题,问题出在程序上:
这段程序的实际含义并非和我们所想的一致。
在C语言中,8/5的确切含义是8除以5所得商值的整数部分。
同样地,(-8)/5的值是-1,不信可以自己试试。
那么如果非要得到8÷5=1.6的结果怎么办?
下面是完整的程序。
程序1-2计算并输出8/5的值,保留小数点后1位
#include
intmain()
{
printf("%.1lf\n",8.0/5.0);
return0;
}
注意,百分号后面是个小数点,然后是数字1,再然后是小写字母l,最后是小写字母f,千万不能打错,包括大小写——在C语言中,大写和小写字母代表的含义是不同的。
再来做3个实验:
实验5:
把%.1lf中的数字1改成2,结果如何?
能猜想出“1”的确切意思吗?
如果把小数点和1都删除,%lf的含义是什么?
实验6:
字符串%.1lf不变,把8.0/5.0改成原来的8/5,结果如何?
实验7:
字符串%.1lf改成原来的%d,8.0/5.0不变,结果如何?
实验5并不难解决,但实验6和实验7的答案就很难简单解释了——真正原因涉及整数和浮点数编码,相信多数初学者对此都不感兴趣。
原因并不重要,重要的是规范:
根据规范做事情,则一切尽在掌握中。
提示1-1:
整数值用%d输出,实数用%lf输出。
这里的“整数值”指的是1+2、8/5这样“整数之间的运算”。
只要运算符的两边都是整数,则运算结果也会是整数。
正因为这样,8/5的值才是1,而不是1.6。
8.0和5.0被看作是“实数”,或者说得更专业一点,叫“浮点数”。
浮点数之间的运算结果是浮点数,因此8.0/5.0=1.6也是浮点数。
注意,这里的运算符“/”其实是“多面手”,它既可以拿来做整数除法,又可以拿来做浮点数除法。
提示1-2:
整数/整数=整数,浮点数/浮点数=浮点数。
这条规则同样适用于加法、减法和乘法,不过没有除法这么容易出错——毕竟整数乘以整数的结果本来就是整数。
算术表达式可以和数学表达式一样复杂,例如:
程序1-3复杂的表达式计算
#include
#include
intmain()
{
printf("%.8lf\n",1+2*sqrt(3)/(5-0.1));
return0;
}
相信读者不难把它翻译成数学表达式:
。
尽管如此,读者可能还是有一些疑惑:
5-0.1的值是什么?
“整数-浮点数”是整数还是浮点数?
另外,多出来的#include是做什么用的?
第1个问题相信读者能够“猜到”结果:
整数-浮点数=浮点数。
但其实这个说法并不准确。
确切的说法是:
整数先“变”成浮点数,然后浮点数-浮点数=浮点数。
第2个问题的答案是:
因为程序1-3中用到了数学函数sqrt。
数学函数sqrt(x)的作用是计算x的算术平方根(若不信,可输出sqrt(9.0)的值试试)。
一般来说,只要在程序中用到了数学函数,就需要在程序最开始的地方包含头文件math.h,并在编译时连接数学库。
如果你不知道如何编译并运行这段程序,可阅读附录或向指导教师求助。
1.2变量及其输入
1.1节的程序虽好,但有一个遗憾:
计算的数据是事先确定的。
为了计算1+2和2+3,我们不得不编写两个程序。
可不可以让程序读取键盘输入,并根据输入内容计算结果呢?
答案是肯定的。
程序如下:
程序1-4A+B问题
#include
intmain()
{
inta,b;
scanf("%d%d",&a,&b);
printf("%d\n",a+b);
return0;
}
该程序比1.1节的复杂了许多。
简单地说,第一条语句“inta,b”声明了两个整型(即整数类型)变量a和b,然后读取键盘输入,并放到a和b中。
注意a和b前面的&符号——千万不要漏掉,不信可以试试。
现在,你的程序已经读入了两个整数,可以在表达式中自由使用它们,就好比使用12、597这样的常数。
这样,表达式a+b就不难理解了。
提示1-3:
scanf中的占位符和变量的数据类型应一一对应,且每个变量前需要&符号。
可以暂时把变量理解成“存放值的场所”,或者形象地认为每个变量都是一个盒子、瓶子或箱子。
在C语言中,变量有自己的数据类型,例如int型变量存放整数值,而double型变量存放浮点数值(专业的说法是“双精度”浮点数)。
如果硬要把浮点数值塞给一个int型变量,将会丢失部分信息——我们不推荐这样做。
下面来看一个复杂一点的例子。
例题1-1圆柱体的表面积
输入底面半径r和高h,输出圆柱体的表面积,保留3位小数,格式见样例。
样例输入:
3.59
样例输出:
Area=274.889
【分析】
圆柱体的表面积由3部分组成:
上底面积、下底面积和侧面积。
由于上下底面积相等,完整的公式可以写成:
表面积=底面积×2+侧面积。
根据平面几何知识,底面积=
,侧面积=
。
不难写出完整程序:
程序1-5圆柱体的表面积
#include
#include
intmain()
{
constdoublepi=4.0*atan(1.0);
doubler,h,s1,s2,s;
scanf("%lf%lf",&r,&h);
s1=pi*r*r;
s2=2*pi*r*h;
s=s1*2.0+s2;
printf("Area=%.3lf\n",s)
return0;
}
这是本书中第一个完整的“竞赛题目”,因为和正规比赛一样,题目中包含着输入输出格式规定,还有样例数据。
大多数的算法竞赛包含如下一些相同的“游戏规则”。
首先,选手程序的执行是自动完成的,没有人工干预。
不要在用户输入之前打印提示信息(例如“Pleaseinputn:
”),这不仅不会为程序赢得更高的“界面友好分”,反而会让程序丢掉大量的(甚至所有的)分数——这些提示信息会被当作输出数据的一部分。
例如刚才的程序如果加上了“友好提示”,输出信息将变成:
Pleaseinputn:
Area=274.889
比标准答案多了整整一行!
其次,不要让程序“按任意键退出”(例如调用system(“pause”),或者加一个多余的getchar()),因为不会有人来“按任意键”的。
不少早期的C语言教材会建议在程序的最后加这样一条语句来“观察输出结果”,但注意千万不要在算法竞赛中这样做。
提示1-4:
在算法竞赛中,输入前不要打印提示信息。
输出完毕后应立即终止程序,不要等待用户按键,因为输入输出过程都是自动的,没有人工干预。
在一般情况下,你的程序不能直接读取键盘和控制屏幕:
不要在算法竞赛中使用getch()、getche()、gotoxy()、clrscr()(早期的教材中可能会介绍这些函数)。
提示1-5:
在算法竞赛中不要使用头文件conio.h,包括getch()、clrscr()等函数。
最后,最容易忽略的是输出的格式:
在很多情况下,输出格式是非常严格的——多一个或者少一个字符都是不可以的!
提示1-6:
在算法竞赛中,每行输出均应以回车符结束,包括最后一行。
除非特别说明,每行的行首不应有空格,但行末通常可以有多余空格。
另外,输出的每两个数或者字符串之间应以单个空格隔开。
总结一下,算法竞赛的程序应当只做3件事情:
读入数据、计算结果、打印输出。
不要打印提示信息,不要在打印输出后“暂停程序”,更不要尝试画图、访问网络等与算法无关的任务。
回到刚才的程序,它多了几个新东西。
首先是“constdoublepi=4.0*atan(1.0);”。
这里也声明了一个叫pi的“符号”,但是const关键字表明它的值是不可以改变的——pi是一个真正的数学常数。
提示1-7:
尽量用const关键字声明常数。
接下来是s1=pi*r*r。
这条语句应该如何理解呢?
“s1等于pi*r*r”吗?
并不是这样的。
不信,你把它换成“pi*r*r=s1”试试,编译器会给出错误信息:
invalidlvalueinassignment。
如果这条语句真的是“二者相等”的意思,为何不允许反着写呢?
事实上,这条语句的学术说法是赋值(assignment),它不是一个描述,而是一个动作。
它的确切含义是:
先把“等号”右边的值算出来,然后塞到左边的变量中。
注意,变量是“喜新厌旧”的,即新的值将覆盖原来的值,一旦被赋了新的值,变量中原来的值就丢失了。
提示1-8:
赋值是个动作,先计算右边的值,再赋给左边的变量,覆盖它原来的值。
最后是那个“Area=%.3lf\n”,它的用法很容易被猜到:
只有以%开头的部分才会被后面的值替换掉,其他部分原样输出。
提示1-9:
printf的格式字符串中可以包含其他可打印符号,打印时原样输出。
1.3顺序结构程序设计
例题1-2三位数反转
输入一个三位数,分离出它的百位、十位和个位,反转后输出。
样例输入:
127
样例输出:
721
【分析】
首先将三位数读入变量n,然后进行分离。
百位等于n/100(注意这里取的是商的整数部分),十位等于n/10%10(这里的%是取余数操作),个位等于n%10。
程序如下:
程序1-6三位数反转
(1)
#include
intmain()
{
intn;
scanf("%d",&n);
printf("%d%d%d\n",n%10,n/10%10,n/100);
return0;
}
此题有一个没有说清楚的细节,即:
如果个位是0,反转后应该输出吗?
例如输入是520,输出是025还是25?
如果在算法竞赛中遇到这样的问题,可向监考人员询问。
但是在这里,两种情况的处理方法都应学会。
提示1-10:
算法竞赛的题目应当是严密的,各种情况下的输出均应有严格规定。
如果在比赛中发现题目有漏洞,应向相关人员询问,而尽量不要自己随意假定。
上面的程序输出025,但要改成输出25似乎会比较麻烦——我们必须判断n%10是不是0,但目前还没有学到“根据不同情况执行不同指令”(分支结构程序设计是1.4节的主题)。
一个解决方法是在输出前把结果储存在变量m中。
这样,直接用%d格式输出m,将输出25。
要输出025也很容易,把输出格式变为%03d即可。
程序1-7三位数反转
(2)
#include
intmain()
{
intn,m;
scanf("%d",&n);
m=(n%10)*100+(n/10%10)*10+(n/100);
printf("%03d\n",m);
return0;
}
例题1-3交换变量
输入两个整数a和b,交换二者的值,然后输出。
样例输入:
82416
样例输出:
16824
【分析】
按照题目所说,先把输入存入变量a和b,然后交换。
如何交换两个变量呢?
最经典的方法是三变量法:
程序1-8变量交换
(1)
#include
intmain()
{
inta,b,t;
scanf("%d%d",&a,&b);
t=a;
a=b;
b=t;
printf("%d%d\n",a,b);
return0;
}
可以将这种方法形象地比喻成将一瓶酱油和一瓶醋借助一个空瓶子进行交换:
先把酱油倒入空瓶,然后将醋倒进原来的酱油瓶中,最后把酱油从辅助的瓶子中倒入原来的醋瓶子里。
这样的比喻虽然形象,但是初学者应当注意它和真正的变量交换的区别。
借助一个空瓶子的目的是:
避免把醋直接倒入酱油瓶子——直接倒进去,二者混合以后,将很难分开。
在C语言中,如果直接进行赋值a=b,则原来a的值(酱油)将会被新值(醋)覆盖,而不是混合在一起。
当酱油被倒入空瓶以后,原来的酱油瓶就变空了,这样才能装醋。
但在C语言中,进行赋值t=a后,a的值不变,它只是把值拷贝(即复制)给了变量t而已,自身并不会变化。
尽管a的值马上就会被改写,但是从原理上看,t=a的过程和“倒酱油”的过程有着本质区别。
提示1-11:
赋值a=b之后,变量a原来的值被覆盖,而b的值不变。
另一个方法没有借助任何变量,但是较难理解:
程序1-9变量交换
(2)
#include
intmain()
{
inta,b;
scanf("%d%d",&a,&b);
a=a+b;
b=a-b;
a=a-b;
printf("%d%d\n",a,b);
return0;
}
这次就不太方便用倒酱油做比喻了:
硬着头皮把醋倒在酱油瓶子里,然后分离出酱油倒回醋瓶子?
比较理性的方法是手工模拟这段程序,看看每条语句执行后的情况。
在顺序结构程序中,程序一条一条依次执行。
为了避免值和变量名混淆,假定用户输入的是a0和b0,因此scanf语句执行完后a=a0,b=b0。
执行完a=a+b后:
a=a0+b0,b=b0。
执行完b=a-b后:
a=a0+b0,b=a0。
执行完a=a-b后:
a=b0,b=a0。
这样就不难理解两个变量是如何交换的了。
这个方法看起来很好(少用一个变量),但实际上很少使用,因为它的适用范围很窄:
只有定义了加减法的数据类型才能这么做。
事实上,笔者并不推荐读者采用这样的技巧实现变量交换:
三变量法已经足够好了,这个例子只是帮助读者提高程序阅读能力。
提示1-12:
交换两个变量的三变量法适用范围广,推荐使用。
那么是不是说,三变量法是解决本题的最佳途径了呢?
答案是否定的。
多数算法竞赛采用黑盒测试,即只考查程序解决问题的能力,而不关心它采用的什么方法。
对于本题而言,最合适的程序莫过于:
程序1-10变量交换(3)
#include
intmain()
{
inta,b;
scanf("%d%d",&a,&b);
printf("%d%d\n",b,a);
return0;
}
换句话说,我们的目标是解决问题,而不是为了写程序而写程序,同时应保持简单(KeepItSimpleandStupid,KISS),而不是自己创造条件去展示编程技巧。
提示1-13:
算法竞赛是在比谁能更好地解决问题,而不是在比谁写的程序看上去更高级。
1.4分支结构程序设计
例题1-4鸡兔同笼
已知鸡和兔的总数量为n,总腿数为m。
输入n和m,依次输出鸡的数目和兔的数目。
如果无解,则输出“Noanswer”(不要引号)。
样例输入:
1432
样例输出:
122
样例输入:
1016
样例输出:
Noanswer
【分析】
设鸡有a只,兔有b只,则a+b=n,2a+4b=m,联立解得a=(4n-m)/2,b=n-a。
在什么情况下此解“不算数”呢?
首先,a和b都是整数;其次,a和b必须是非负的。
可以通过下面的程序判断:
程序1-11鸡兔同笼
#include
intmain()
{
inta,b,n,m;
scanf("%d%d",&n,&m);
a=(4*n-m)/2;
b=n-a;
if(m%2==1||a<0||b<0)
printf("Noanswer\n");
else
printf("%d%d\n",a,b);
return0;
}
上面的程序用到了if语句,它的一般格式是:
if(条件)
语句1;
else
语句2;
注意语句1和语句2后面的分号,以及if后面的括号。
“条件”是一个表达式,当该表达式的值为“真”时执行语句1,否则执行语句2。
另外,“else语句2”这个部分是可以省略的。
语句1和语句2前面的空行是为了让程序更加美观,并不是必需的,但强烈推荐读者使用。
提示1-14:
if语句的基本格式为:
if(条件)语句1;else语句2。
换句话说,m%2==1||a<0||b<0是一个表达式,它的字面意思是“m是奇数,或者a小于0,或者b小于0”。
这句话可能正确,也可能错误。
因此这个表达式的值可能为真,也可能为假,取决于m、a和b的具体数值。
这样的表达式称为逻辑表达式。
和算术表达式类似,逻辑表达式也由运算符和值构成,例如“||”运算符称为“逻辑或”,a||b表示a为真,或者b为真。
换句话说,a和b只要有一个为真,a||b就为真;如果a和b都为真,则a||b也为真。
提示1-15:
if语句的条件是一个逻辑表达式,它的值可能为真,也可能为假。
细心的读者也许发现了,如果a为真,则无论b的值如何,a||b均为真。
换句话说,一旦发现a为真,就不必计算b的值。
C语言正是采取了这样的策略,称为短路(short-circuit)。
也许你会觉得,用短路的方法计算逻辑表达式的唯一优点是速度更快,但其实并不是这样,稍后我们会通过几个例子予以证实。
提示1-16:
C语言中的逻辑运算符都是短路运算符。
一旦能够确定整个表达式的值,就不再继续计算。
例题1-5三整数排序
输入3个整数,从小到大排序后输出。
样例输入:
20733
样例输出:
72033
【分析】
a、b、c 3个数一共只有6种可能的顺序:
abc、acb、bac、bca、cab、cba,所以最简单的思路是使用6条if语句。
程序1-12三整数排序
(1)(错误)
#include
intmain()
{
inta,b,c;
scanf("%d%d%d",&a,&b,&c);
if(a
if(aif(bif(bif(cif(c
return0;
}
上述程序看上去没有错误,而且能通过题目中给出的样例,但可惜有缺陷:
输入111将得不到任何输出!
这个例子告诉我们:
即使通过了题目中给出的样例,程序仍然可能存在问题。
提示1-17:
算法竞赛的目标是编程对任意输入均得到正确的结果,而不仅是样例数据。
稍微修改一下:
把所有的小于符号“<”改成小于等于符号“<=”(在一个小于号后添加一个等号)。
这下总可以了吧?
很遗憾,还是不行。
对于“111”,6种情况全部符合,程序一共输出了6次111。
一种解决方案是人为地让6种情况没有交叉:
把所有的if改成elseif。
程序1-13三整数排序
(2)
#include
intmain()
{
inta,b,c;
scanf("%d%d%d",&a,&b,&c);
if(a<=b&&b<=c)printf("%d%d%d\n",a,b,c);
elseif(a<=c&&c<=b)printf("%d%d%d\n",a,c,b);
elseif(b<=a&&a<=c)printf("%d%d%d\n",b,a,c);
elseif(b<=c&&c<=a)printf("%d%d%d\n",b,c,a);
elseif(c<=a&&a<=b)printf("%d%d%d\n",c,a,b);
elseif(c<=b&&b<=a)printf("%d%d%d\n",c,b,a);
return0;
}
最后一条语句还可以简化成单独的“else”(想一想,为什么),不过,幸好程序正确了。
提示1-18:
如果有多个并列、情况不交叉的条件需要一一处理,可以用elseif语句。
另一种思路是把a、b、c这3个变量本身改成a≤b≤c的形式。
首先检查a和b的值,如果a>b,则交换a和b(利用前面讲过的三变量交换法);接下来检查a和c,最后检查b和c,程序如下:
程序1-14三整数排序(3)
#include
intmain()
{
inta,b,c,t;
scanf("%d%d%d",&a,&b,&c);
if(a>b){t=a;a=b;b=t;}
if(a>c){t=a;a=c;c=t;}
if(b>c){t=b;b=c;c=t;}
printf("%d%d%d\n",a,b,c);
return0;
}
为什么这样做是对的呢?
因为经过第一次检查以后,必然有a≤b,而第二次检查以后a≤c。
由于第二次检查以后a的值不会变大,所以a≤b依然成立。
换句话说,a已经是3个数中的最小值。
接下来只需检查b和c的顺序即可。
一个很自然的问题产生了:
其他检查顺序是否也可以呢?
例如先(a,b),然后(b,c),最后(a,c)?
这个问题留给读者思考。
提示:
上机实验。
注意上面的程序中唯一的新东西:
花括号。
前面讲过,if语句中有一个“语句1”和可选的“语句2”,且都要以分号结尾。
有一种特殊的“语句”是由花括号括起来的多条语句。
这多条语句可以作为一个整体,充当if语句中的“语句1”或“语句2”,且