《算法竞赛入门经典》习题源码 Github开源:https://github.com/RyanHe123/Classic-Introduction-to-Algorithmic-Competition
本文中的习题题解为本人完成,如果存在错误或可以改进的地方,欢迎在评论区提出,谢谢!
-
疑问:P19
#include<stdio.h> int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++) printf("%d\n",i); return 0; }
上面的代码里有一个重要的细节,变量 i 定义在循环语句中,因此 i 在循环体中不可见,例如,在第8行,之前再插入一条
printf("%d\n",i);
会报错。笔者认为应该是i在循环体外不可见。
-
建议尽量缩短变量的定义范围,例如在for循环的初始化部分定义循环变量
-
三种取整方法:
向上取整:ceil();
向下取整:floor();
四舍五入:round(); 或floor(n+0.5);
-
浮点运算可能存在误差。在进行浮点数比较时,应考虑到浮点误差。
判定平方数时:
最好不要用
if(sqrt(n)==floor(sqrt(n)))
,因为浮点数的运算有可能存在误差,例如假设在经过大量计算后,由于误差的影响,整数1变成了0.9999999999,floor的结果是0而不是1。所以为了减小误差的影响,一般改为四舍五入,但是小数部分为0.5的数也会受到浮点误差的影响,因此任何一道严密的算法竞赛题目中都需要想办法解决这个问题。 -
while和do-while的区别:
- while是先判断再循环 do-while是先循环再判断
- while可能未循环直接跳出,而do-while至少循环1次
-
计时函数clock():
可以使用time.h和clock()函数获得程序运行时间
函数原型:
clock_t clock(void) ;
typedef long clock_t;
也就是说clock_t是一个长整形,得到的时间除以常数CLOCKS_PER_SEC,得到的数以秒为单位
该函数返回程序目前为止运行的时间
为了避免输入数据的时间影响测试结果,使用一种称为“管道”的小技巧,在Windows命令行进入该项目的.exe的目录下,执行echo <输入数据>|<程序名称>。
-
循环结构程序设计中最常见的两个问题:算术运算溢出和程序效率低下
-
在“多数据输入”的题目中,常犯的错误是一次计算后,未将数据重新初始化,所以要尽量缩短变量定义的范围。
习题
习题 2-1水仙花数(daffodil)
输入100~999中的所有水仙花数。若3位数ABC满足ABC=A3+B3+C3,则称其为水仙花数。例如153=13+53+33,所以153是水仙花数。
#include<stdio.h>
int main()
{
int n=100;
int a,b,c;
for(;n<1000;n++)
{
a=n/100;
b=n%100/10;
c=n%10;
if(n==a*a*a+b*b*b+c*c*c)
{
printf("%d\n",n);
}
}
}
习题2-2 韩信点兵(hanxin)
相传韩信才智过人,从不直接清点自己军队的人数,只要让士兵先后以三人一排、五人一排、七人一排地变化队形,而他每次只掠一眼队伍的排尾就知道总人数了。输入3个非负整数a,b,c,表示每种队形排位的人数(a<3,b<5,c<7),输出总人数的最小值(或报告无解)。已知总人数不小于10,不超过100。输入到文件结束为止。
样例输入:
2 1 6
2 1 3
样例输出:
Case 1: 41
Case 2: No answer
#include<stdio.h>
int main()
{
int kase=1;
int a,b,c;
while(scanf("%d %d %d",&a,&b,&c)==3)
{
int state=0;
int i;
for(i=10;i<101;i++)
{
if(i%3==a&&i%5==b&&i%7==c)
{
state=1;
printf("Case %d: %d\n",kase++,i);
break;
}
}
if(state==0)
{
printf("Case %d: No answer\n",kase++);
}
}
return 0;
}
习题2-3 倒三角形(triangle)
输入正整数n<=20,输出一个n层的倒三角形。例如n=5时输出如下:
#########
#######
#####
###
#
#include<stdio.h>
int main()
{
int n;
scanf("%d",&n);
int i=n;
for(;i>0;i--)
{
int j;
for(j=0;j<n-i;j++)
{
printf(" ");
}
for(j=0;j<2*i-1;j++)
{
printf("#");
}
printf("\n");
}
return 0;
}
习题2-4 子序列的和(subsequence)
输入两个正整数n<m<106,输出 1 n 2 + 1 ( n + 1 ) 2 + . . . + 1 m 2 \frac{1}{n^2}+\frac{1}{(n+1)^2}+...+\frac{1}{m^2} n21+(n+1)21+...+m21,保留5位小数。输入包含多组数据,结束标记为n=m=0。注意:本题有陷阱。
样例输入:
2 4
65536 655360
0 0
样例输出:
Case 1:0.42361
Case 2:0.00001
//Version 1
#include<stdio.h>
int main()
{
int n,m;
int kase=0;
while(scanf("%d %d",&n,&m)==2&&n&&m)
{
int i;
double sum=0.0;
for(i=n;i<m+1;i++)
{
sum+=(double)1/i/i;
//此处如果使用(i*i),数据会溢出
}
printf("Case %d: %.5lf\n",++kase,sum);
}
}
笔者后来在print()函数详解中发现了这样的用法:
利用*来占位,然后在后面的参数列表中给出最小宽度,所以本程序可以进行简化:
//Version 2
#include<stdio.h>
int main()
{
int a,b,c;
int kase=0;
while(scanf("%d %d %d",&a,&b,&c)==3&&a&&b&&c)
{
double res;
res=(double)a/b;
printf("Case %d:%.*f\n",++kase,c,res);
}
return 0;
}
习题2-5 分数化小数(decimal)
输入正整数a,b,c,输出a/b的小数形式,精确到小数点后c位。a,b$\leqKaTeX parse error: Double superscript at position 5: 10^6^̲,c\leq$100。输入包含多组数据,结束标记为a=b=c=0。
样例输入:
1 6 4
0 0 0
样例输出:
Case 1: 0.1667
#include<stdio.h>
int main()
{
int a,b,c;
int kase=0;
while(scanf("%d %d %d",&a,&b,&c)==3&&a&&b&&c)
{
double res;
res=(double)a/b;
char s[15]="Case %d:%.cf\n";
s[10]='0'+c;
printf(s,++kase,res);
}
return 0;
}
习题2-6 排列(permutation)
用1,2,3,…,9组成3个三位数abc,def和ghi,每个数字恰好使用一次,要求abc:def:ghi=1:2:3。按照“abc def ghi”的格式输出所有解。提示:不必太动脑筋。
#include<stdio.h>
int main()
{
int n1,n2,n3;
int a,b,c,d,e,f,g,h,i;
int count[10];
n1=123;
for(;n1<330;n1++)
{
n2=n1*2;
n3=n1*3;
int i;
for(i=0;i<10;i++)
count[i]=0;
count[n1/100]++;
count[n1%100/10]++;
count[n1%10]++;
count[n2/100]++;
count[n2%100/10]++;
count[n2%10]++;
count[n3/100]++;
count[n3%100/10]++;
count[n3%10]++;
int state=1;
for(i=1;i<10;i++)
{
if(count[i]!=1)
{
state=0;
break;
}
}
if(state==1)
{
printf("%d %d %d\n",n1,n2,n3);
}
}
}
思考题:
题目1:
假设需要输出 2 , 4 , 6 , 8 , . . . , 2 n 2,4,6,8,...,2n 2,4,6,8,...,2n,每个一行,能不能通过改动下文的程序实现?
#include<stdio.h>
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
printf("%d\n",i);
return 0;
}
任务1: 修改第7行,不修改第6行。
printf("%d\n",2*i);
任务2: 修改第6行,不修改第7行。
for(int i=2;i<=2*n;i+=2)
题目2:
下面程序的运行结果是什么?
#include<stdio.h>
int main()
{
double i;
for(i=0;i!=10;i+=0.1)
printf("%.1f\n",i);
return 0;
}
运行后发现循环并不能停止,笔者搜索后发现是浮点数陷阱。
参考链接:http://blog.sina.com.cn/s/blog_6da76f9b0100yr8d.html
浮点数陷阱:
之所以无限循环,那么就可以推断是for循环的条件始终成立,即 i 始终不等于10. 但是 i 是从0开始的,每次都自加0.1.那么应该是100次后就停止.为什么i始终不等于10呢? 是因为浮点数的原因. 我们把10改成10.0,结果仍然是无限循环. 我们把 i += 0.1 改成 i++,发现执行10次后正常停止.我们可以初步推断,是浮点数的加法运算引起的.
接下来调用gdb输出中间结果来观察,发现 i 自加0.1后,并不是我们预想的等于0.1而是等于 0.10000000000000001.往下执行几次,i 的值分别是0.20000000000000001 0.30000000000000004 0.40000000000000002.
这样,我们就理解了为什么i 始终不等于10. 因为浮点数在进行小数运算的时候由于精度问题,会有很小的误差,然而用 = 或者 != 这样的运算符来比较,是会检测出这种误差的.所以导致结果的不正确.
我们还可以多测试一下,将循环条件改为 i != 0.1 或者 i != 0.2时,程序能够正常运行,得到正常结果.但是当i != 0.3时,就是无限循环.显然,在我们的程序中,这种不确定的错误是不应该存在的.
因此,在定义循环变量时,尽量采用int型及整数的加减.因为循环的本质意义就是通过各种条件来控制语句重复运行次数.而这个次数本身就是整数.要实现小数的功能尽量通过循环中的语句来实现。