算法竞赛入门经典授课教案第5章基础题目选解精心排版并扩充部分内容资料.docx
《算法竞赛入门经典授课教案第5章基础题目选解精心排版并扩充部分内容资料.docx》由会员分享,可在线阅读,更多相关《算法竞赛入门经典授课教案第5章基础题目选解精心排版并扩充部分内容资料.docx(32页珍藏版)》请在冰豆网上搜索。
算法竞赛入门经典授课教案第5章基础题目选解精心排版并扩充部分内容资料
第5章基础题目选解
【教学内容相关章节】
5.1字符串5.2高精度运算5.3排序与检索
5.4数学基础5.5训练参考
【教学目标】
(1)学会用常量表简化代码;
(2)学会用状态变量辅助字符串输入;
(3)学会用结构体定义高精度整数,并设计构造函数、复制构造函数和输入输出方法;
(4)学会为结构体定义“小于”运算符,并用它定义其他比较运算符;
(5)熟练掌握冒泡排序和顺序搜索;
(6)熟练掌握用qsort库函数给整数和字符串排序的方法;
(7)熟练掌握小规模素数表的构造方法;
(8)熟练掌握素因子分解的方法;
(9)熟练掌握三角形有向面积的意义和计算方法;
(10)完成一定数量的编程练习。
【教学要求】
掌握字符串的操作,用状态变量辅助字符串输入;掌握高精度整数的计算;掌握有关排序与检索的算法;掌握素数的计算和分解素因子方法;掌握有向三角形的面积的计算方法。
【教学内容提要】
在算法竞赛中,编程能力是非常重要的。
算法设计得再好,如果程序写不出来就是零分;即使程序写出来了,也可能会因为细小的错误导致丢失大量的得分。
本章通过一定数量和类型的例题和习题熟悉常见的编程技巧,为接下来的算法学习打下坚实的基础。
在本章中,程序的正确性是第一位的。
【教学重点、难点】
教学重点:
(1)掌握字符串的操作,用状态变量辅助字符串输入;
(2)掌握高精度整数的计算;
(3)掌握有关排序与检索的算法;
(4)掌握素数的计算和分解素因子方法;
(5)掌握有向三角形的面积的计算方法。
教学难点:
(1)掌握高精度整数的计算;
(2)掌握有关排序与检索的算法;
(3)掌握素数的计算和分解素因子方法。
【课时安排(共5学时)】
5.1字符串5.2高精度运算5.3排序与检索
5.4数学基础5.5训练参考
5.1字符串
5.1.1WERTYU
把手放在键盘上时,稍不注意就会往右错一位。
这样的话,Q会变成W,J会变成K等。
输入一个错位敲出的字符串,输出打字员本来想打出的句子。
样例输入:
OS,GOMRYPFSU/
样例输出:
IAMFINETODAY.
【分析】
每输入一个字符,都可以直接输出一个字符。
但是对输入的字符转换成输出的字符的一种较好的方法是使用常量数组。
完整的程序如下:
#include
char*s="`1234567890-=QWERTYUIOP[]\\ASDFGHJKL;'ZXCVBNM,./";
intmain(){
inti,c;
while((c=getchar())!
=EOF){
for(i=1;s[i]&&s[i]!
=c;i++);/*空语句*/
if(s[i])putchar(s[i-1]);
elseputchar(c);
}
return0;
}
说明:
现将getchar函数与EOF总结如下:
(1)对于getchar的两点总结
①getchar是以行为单位进行存取的。
当用getchar进行输入时,如果输入的第一个字符为有效字符(即输入是文件结束符EOF,Windows下为组合键Ctrl+Z,Unix/Linux下为组合键Ctrl+D),那么只有当最后一个输入字符为换行符'\n'(也可以是文件结束符EOF)时,getchar才会停止执行,整个程序将会往下执行。
譬如下面程序段:
while((c=getchar())!
=EOF){
putchar(c);
}
执行程序,输入:
abc,然后回车。
则程序就会去执行puchar(c),然后输出abc,这个地方不要忘了,系统输出的还有一个回车。
然后可以继续输入,再次遇到换行符的时候,程序又会把那一行的输入的字符输出在终端上。
对于getchar必须读到一个换行符或者文件结束符EOF才进行一次输出。
对这个问题的一个解释是,在大师编写C的时候,当时并没有所谓终端输入的概念,所有的输入实际上都是按照文件进行读取的,文件中一般都是以行为单位的。
因此,只有遇到换行符,那么程序会认为输入结束,然后采取执行程序的其他部分。
同时,输入是按照文件的方式存取的,那么要结束一个文件的输入就需用到EOF(EnfOfFile),这也就是为什么getchar结束输入退出时要用EOF的原因。
②getchar()的返回值一般情况下是字符,但也可能是负值,即返回EOF。
getchar函数的返回值类型不是char类型,而是int类型。
其原型如下:
intgetchar(void);
需要强调的是getchar函数通常返回终端所输入的字符,这些字符系统中对应的ASCII值都是非负的。
因此,很多时候,我们会写这样的两行代码:
charc;
c=getchar();
这样就很有可能出现问题。
因为getchar函数除了返回终端输入的字符外,在遇到Ctrl+D(Linux下)即文件结束符EOF时,getchar()的返回EOF,这个EOF在函数库里一般定义为-1。
因此,在这种情况下,getchar函数返回一个负值,把一个负值赋给一个char型的变量是不正确的。
导致这种错误的责任并不是用户,是函数getchar函数误导了使用者。
为了能够让所定义的变量能够包含getchar函数返回的所有可能的值,正确的定义方法为:
intc;
c=getchar();
(2)EOF的两点总结(主要指普通终端中的EOF)
①EOF作为文件结束符时的情况
EOF虽然是文件结束符,但并不是在任何情况下输入Ctrl+D(Windows下Ctrl+Z)都能够实现文件结束的功能,只有在下列的条件下,才作为文件结束符。
(a)遇到getcahr函数执行时,要输入第一个字符时就直接输入Ctrl+D,就可以跳出getchar(),去执行程序的其他部分;
(b)在前面输入的字符为换行符时,接着输入Ctrl+D;
(c)在前面有字符输入且不为换行符时,要连着输入两次Ctrl+D,这时第二次输入的Ctrl+D起到文件结束符的功能,至于第一次的Ctrl+D的作用将在下面介绍。
其实,这三种情况都可以总结为只有在getchar()提示新的一次输入时,直接输入Ctrl+D才相当于文件结束符。
②EOF作为行结束符时的情况,这时候输入Ctrl+D并不能结束getchar(),而只能引发getchar()提示下一轮的输入。
这种情况主要是在进行getchar()新的一行输入时,当输入了若干字符(不能包含换行符)之后,直接输入Ctrl+D,此时的Ctrl+D并不是文件结束符,而只是相当于换行符的功能,即结束当前的输入。
以上面的代码段为例,如果执行时输入abc,然后Ctrl+D,程序输出结果为:
abcabc
注意:
第一组abc为从终端输入的,然后输入Ctrl+D,就输出第二组abc,同时光标停在第二组字符的c后面,然后可以进行新一次的输入。
这时如果再次输入Ctrl+D,则起到了文件结束符的作用,结束getchar()。
如果输入abc之后,然后回车,输入换行符的话,则终端显示为:
abc //第一行,带回车
abc //第二行
//第三行
其中第一行为终端输入,第二行为终端输出,光标停在了第三行处,等待新一次的终端输入。
从这里也可以看出Ctrl+D和换行符分别作为行结束符时,输出的不同结果。
EOF的作用也可以总结为:
当终端有字符输入时,Ctrl+D产生的EOF相当于结束本行的输入,将引起getchar()新一轮的输入;当终端没有字符输入或者可以说当getchar()读取新的一次输入时,输入Ctrl+D,此时产生的EOF相当于文件结束符,程序将结束getchar()的执行。
5.1.2TeX括号
在TeX中,左双引号``,右双引号"。
输入一篇篇包含双引号的文章,你的任务是把它转换成TeX的格式。
样例输入:
"Tobeornottobe,"quoththeBard,"thatisthequestion".
样例输出:
``Tobeornottobe,"quoththeBard,``thatisthequestion''.
【分析】
本题的关键是,如何判断一个双引号是“左”双引号还是“右”双引号。
方法很简单,使用一个标志变量即可。
完整的程序如下:
#include
intmain(){
intc,q=1;
while((c=getchar())!
=EOF){
if(c=='"'){printf("%s",q?
"``":
"''");q=!
q;}
elseprintf("%c",c);
}
return0;
}
5.1.3周期串
如果一个字符串可以由某个长度为k的字符串重复多次得到,我们说该串以为周期。
例如,abcabcabcabc以3为周期(注意,它也以6和12为周期)。
输入一个长度不超过80的串,输出它的最小周期。
样例输入:
HoHoHo
样例输出:
2
【分析】
字符串可能会有多个周期。
但因为只需求出最小的一个,可以从小到在枚举各个周期,一旦符合条件就立即输出。
下面的程序用到了一个新的语法:
临时定义变量,例如,变量i和j只定义在循环体内,因此在循环体后无法访问到它们。
完整的程序如下:
#include
#include
intmain(){
charword[100];
scanf("%s",word);
intlen=strlen(word);
for(inti=1;i<=len;i++){
if(len%i==0){//确定word的长度len为i的整数倍
intok=1;
for(intj=i;jif(word[j]!
=word[j%i]){//判断是否与word[0]~word[i-1]相等
ok=0;break;
}
if(ok){
printf("%d\n",i);break;
}
}
}
return0;
}
5.2高精度运算
在介绍C语言时,已经看到了很多整数溢出的情形。
如果运算结果真的很大,需要用所谓的高精度算法,用数组来储存整数,模拟手算的方法进行四则运算。
5.2.1小学生算术
在学习加法是时,发现“进位”特别容易出错。
你的任务是计算两个整数在相加时需要多少次进位。
你编制的程序应当可以连续处理多组数据,直到读到两个0(这是输入结束标记)。
假设输入的整数都不超过9个数字。
样例输入:
123456
555555
123594
00
样例输出:
0
3
1
【分析】
注意int的上限约是2000000000,可以保存所有9位整数,因此可以用整数来保存输入每次把a和b分别模10就能获取它们的个位数。
完整的程序如下:
#include
intmain(){
inta,b;
while(scanf("%d%d",&a,&b)==2){
if(!
a&&!
b)return;/*输入0和0结束*/
intc=0,ans=0;/*c储存进位的标志,ans储存进位的次数*/
for(inti=9;i>=0;i--){
c=(a%10+b%10+c)>9?
1:
0;
ans+=c;
a/=10;b/=10;
}
printf(“%d\n”,ans);
}
return0;
}
5.2.2阶乘的精确值
输入不超过1000的正整数n,输出n!
=1×2×3×…×n的精确结果。
样例输入:
30
样例输出:
265252859812191058636308480000000
【分析】
为了保存结果,先分析1000!
大约等于4×102567,因此可以用一个3000个元素的数组f保存。
让f[0]保存结果的个位,f[1]是十位,f[2]是百位,…,则每次只需要模似手算即可完成n!
。
在输出时需要忽略前导0。
注意,如果结果本身就是0,那么忽略所有前导0后将什么都不输出。
所幸n!
肯定不等于0,因本题可以忽略这个细节。
0
1
2
3
4
5
6
7
i=1
1
0
0
0
0
0
0
0
i=2
2
0
0
0
0
0
0
0
i=3
6
0
0
0
0
0
0
0
i=4
4
2
0
0
0
0
0
0
i=5
0
2
1
0
0
0
0
0
i=6
0
2
7
0
0
0
0
0
i=7
0
4
0
5
0
0
0
0
完整的程序如下:
#include
#include
constintmaxn=3000;
intf[maxn];
intmain(){
inti,j,n;
scanf("%d",&n);
memset(f,0,sizeof(f));
f[0]=1;
for(i=2;i<=n;i++){/*乘以i*/
intc=0;
for(j=0;jints=f[j]*i+c;//c表示进位
f[j]=s%10;
c=s/10;
}
}
for(j=maxn-1;j>=0;j--)if(f[j])break;/*忽略前导0*/
for(i=j;i>=0;i--)printf("%d",f[i]);
printf("\n");
return0;
}
5.2.3高精度运算类bign
虽然前面的代码能实现高精度运算,但是代码不能重用。
如果写一个“高精度函数库”,实现“代码模板”,这样现成、好用的代码在测试中更加方便。
所以,设计一个结构体bign来储存高精度非负整数:
constintmax=1000;
structbign
{
intlen,s[maxn];
bign(){memset(s,0,sizeof(s));len=1;}
};
其中,len表示位数,而s数组就是具体的各个数字。
上面的结构体中有一个函数,称为构造函数(Constructor)。
构造函数是C++中特有的,作用是进行初始化。
说明:
(1)C++语言对C语言的struct进行了改造,使其也可以像class那样支持成员函数的定义,从而使struct变成真正的抽象数据类型(ADT,AbstractDataType)。
(2)在C++语言中,如果不特别指明,struct的成员的默认访问说明符为public,而class的成员的默认访问说明符为private。
实际上就C++语言来讲,struct和class除了“默认的成员访问说明符”这一点外,没有任何区别。
(3)C++的struct和class差别很小,其实class就是从struct发展出来的。
struct定义的结构体在C++中也是一个类,结构体可以有class的任何东西。
现在来重新定义赋值运算(注意,下面的函数要写在bign结构体定义的内部,千万不要写在外面):
bignoperator=(constchar*num){
len=strlen(num);
for(inti=0;ireturn*this;
}
说明:
每个类实例化一个对象后,就会有一个this指针,指向当前实例本身。
this是由编译器自动产生的,在类的成员函数中有效。
this是一个常量,不允许对其赋值。
可以用x="1234567898765432123456789"给x赋值,它会也这个字符串转化为“逆序数组+长度”的内部表示法。
为了支持x=1234的赋值方式,再定义另外一种赋值运算(定义在结构体内部):
bignoperator=(intnum){
chars[maxn];
sprintf(s,"%d",num);
*this=s;
return*this;
}
可以用“bignx;x=100;”来声明一个x并给它赋值,却不能写成“bignx=100;”。
原因在于,bignx=100是初始化,而非普通的赋值操作。
为了让代码支持“初始化”操作,需要增加两个函数(定义在结构体内部):
bign(intnum){*this=num;}
bign(constchar*num){*this=num;}
下面需要提供一个函数把它转化为字符串:
stringstr()const{
stringres="";
for(inti=0;iif(res=="")res="0";
returnres;
}
说明:
任何不会修改数据成员(即函数中的变量)的函数都应该声明为const类型。
如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。
接下来,重新定义<<和>>运算符,让输入输入出流直接支持bign结构体(这两个孙函数要定义在结构体bign的外边,不在写在里面):
istream&operator>>(istream&in,bign&x){
strings;
in>>s;
x=s.c_str();
returnin;
}
ostream&operator<<(ostream&out,constbign&x){
out<returnout;
}
5.2.4重载bign的常用运算符
加法写成函数(定义在结构体内部):
bignoperator+(constbign&b)const{
bignc;
c.len=0;
for(inti=0,g=0;g||iintx=g;
if(iif(ic.s[c.len++]=x%10;
g=x/10;
}
returnc;
}
两个数中只要任何一个数还有数字,加法就要继续,即使两个数都加完了,也不要忘记处理进位。
注意,上面的算法并没有假设s数组中“当前没有用到的数字都是0”。
如果事先已经清零,就可以把循环体的前3行简化为intx=s[i]+b.s[i]=g。
甚至可以直接把循环次数设置max(len,b.len)+1,然后检查最终结果是否有前导零。
如果有,则把len减1。
为了让使用简单,可以重新定义+=运算符(定义在结构体内部):
bignoperator+=(constbign&b){
*this=*this+b;
return*this;
}
接下来,实现“比较”操作(定义在结构体内部):
booloperator<(constbign&b)const{
if(len!
=b.len)returnlenfor(inti=len-1;i>=0;i--)
if(s[i]!
=b.s[i])returns[i]returnfalse;
}
一开始就比较两个bign的位数,如果不相等则直接返回,否则比较两个数组的逆序的字典序。
注意,这样做的前提是两个数都没有前导零,如果不注意的话,很可能出现“运算结果都没有问题,但一比较就错”的情况。
用“小于(<)”符号,就可用它来定义其他所有比较运算符:
booloperator>(constbign&b)const{
returnb<*this;
}
booloperator<=(constbign&b){
return!
(b>*this);
}
booloperator>=(constbign&b)const{
return!
(*this
}
booloperator!
=(constbign&b){
returnb<*this)||*this
}
booloperator==(constbign&b){
return!
(b<*this)&&!
(*this
}
后面如果要用到高精度运算的题目中,将直接使用bign类中的所有运算。
5.3排序与检索
数据处理是计算机的强项,包括排序、检索和统计等。
下面举一些例子,展示排序和检索引的技巧。
5.3.16174问题
假设你有一个各位数字互不相同的四位数,把所有数字从到小排序后得到a,从小到大排序后得到b,然后a-b替换原来的数,并且继续操作。
例如,从1234出发,依次可以得到4321-1234=3087、8730-378=8352、8532-2358=6174。
有趣的是,7641-1467=6174,回到了它自己。
输入一个n位数,输出操作序列,直到出现循环(即新得到的数曾经得到过)。
输入保证在循环之前最多只会产生1000个整数。
样例输入:
1234
样例输出:
1234->3087->8352->6174->6174
【分析】
要解决本问题需要解决下面两个问题:
(1)需要把各个数字排序,因此首先需要把各个数字提取出来。
下面的函数使用“冒泡排序”的方法,可以方便地把一个数组按照从大到小的或者从小到大的顺序排序:
intget_next(intx){
inta,b,n;
chars[10];
//转换成字符串
sprintf(s,"%d",x);
n=strlen(s);
//冒泡排序
for(inti=0;ifor(intj=i+1;jif(s[i]>s[j]){
chart=s[i];s[i]=s[j];s[j]=t;
}
sscanf(s,"%d",&b);//将s以%d形式存入b中
//字符串反转
for(inti=0;ichart=s[i];s[i]=s[n-1-i];s[n-1-i]=t;
}
ss