蓝桥杯备考——算法竞赛入门经典(第2版)学习笔记2

算法竞赛入门经典(第2版)学习笔记2

  • 第二章 循环结构程序设计
    • 2.1 for循环
    • 2.2 while 循环和do-while 循环
    • 2.3 循环的代价
    • 2.4 算法竞赛中的输入输出框架
    • 2.5 注解与习题

第二章 循环结构程序设计

2.1 for循环

例题2-1 aabb
输出所有形如aabb的4位完全平方数(即前两位数字相等,后两位数字也相等)

分析:
第一种思路
(1)首先枚举所有可能的aabb,然后判断他们是否为完全平方数
注:a的范围是1~9,但b可以等于0所以范围为0 ~ 9
可以写出下列伪代码:

for(int a=1;a<=9;a++)
    for(int b=0;b<=9;b++)
        if(aabb是完全平方数)printf("%d\n",aabb);

注1:不拘一格地使用伪代码来思考和描述算法是一种值得推荐的做法
(2)写出伪代码之后,我们需要考虑如何将它变成真正的代码,上面的伪代码有两个“非法”之处,一个是完全平方数的判定,另一个是aabb这个变量,后者用变量n=a1100+b11存储即可
(3)接下来,如何判断n是否完全平方数?可以将其开平方后,看它是否为整数,即用一个int型变量m存储sqrt(n)四舍五入后的整数,然后判断m2是否等于n。函数floor(x)在c语言的库函数中,是返回不超过x的最大整数

程序2-2 7744问题第一种解法

#include
#include
int main(){
 int n;
 for(int a=1;a<=9;a++)
  for(int b=0;b<=9;b++){
   n=a*1100+b*11;
   int m=floor(sqrt(n)+0.5);//注意此处的0.5,为了四舍五入减小误差
   if(m*m==n) printf("%d",n);
  } 
}

为了减少误差的影响,一般改成四舍五入,即floor(x+0.5)
注2:浮点运算可能存在误差,在进行浮点数比较时,应考虑到浮点误差
第二种思路
枚举平方根x,从而可以避免开平方的操作

程序2-3 7744问题第二种解法

#include
int main(){
 for(int i=1; ;i++){
  int m=i*i;
  if(m<1000) continue;
  if(m>9999) break;
  int high=m/100;//high是高两位
  int low=m%100;//low是低两位
  if(high%10 == high/10 && low%10 == low/10)
  printf("%d",m);
 }
}

(1)continue是指跳回for循环的开始,执行调整语句并判断循环条件(即“直接进行下一次循环”),而break是指直接跳出循环
(2)这里continue语句的作用是排除不足四位数的m,直接继续检查后面的数。continue可以帮助我们偷懒,不必求出循环的起始点。有了break,连循环的终点也可以不用指定,当n超过9999时会自动退出循环。
(3)注意这里的for循环语句是残缺的,没有制定循环条件。事实上,3部分都是可以省略的,for(;;)是一个死循环,如果不采取措施(如break),就永远不会结束

2.2 while 循环和do-while 循环

注1:C99并没有规定int类型的确切大小,但在当前流行的竞赛平台中,int都是32位整数,范围是-2147483648~2147483648。

注2:当普通的int类型溢出时,可以使用long long解决问题,范围是-263~263-1,唯一的区别是要把输入时的%d改成%lld(Linux下)。有时Windows平台还要把%lld改成%I(i)64d,很容易搞错,所以如果涉及long long的输入输出,常用C++的输入输出流或自定义的输入输出方法。

2.3 循环的代价

例题2-4 阶乘之和
输入n,计算S=1!+2!+3!+···+n!的末6位(不含前导0)。n<=106
样例输入:
10
样例输出:
37913

#include
int main(){
 int n,sum,s;
 sum=0;
 s=1;
 scanf("%d",&n);
 for(int i=1;i<=n;i++){
  s*=i;
  sum+=s;
 }
 printf("%d",sum%1000000);
}

但如果测试n=100时,则输出-971703,代表乘法溢出了
它的速度太慢!于是把程序改成“每步取模”的形式,然后加一个“计时器”
代码如下:

#include
#include
int main(){
 const int MOD=1000000;
 int n,sum,s;
 sum=0;
 s=1;
 scanf("%d",&n);
 for(int i=1;i<=n;i++){
  s=s*i%MOD;
  sum=(sum+s)%MOD;
 }
 printf("%d",sum%1000000);
 printf("Time used=%.2f\n",(double)clock() / CLOCKS_PER_SEC);
}

输入“20”,系统瞬间输出了答案820313。
注1:可以使用time.h和clock()函数获得程序运行时间。常数CLOCKS_PER_SEC和操作系统相关,请不要直接使用clock()的返回值,而应总是除以CLOCKS_PER_SEC。
注2:要计算只包含加法、减法和乘法的整数表达式除以正整数n的余数,可以在每步计算之后对n取余,结果不变。
注3:很多程序的运行时间与规模n存在着近似的简单关系。可以通过计时函数来发现或验证这一关系
注4:本节展示了循环结构程序设计中最常见的两个问题:算术运算溢出和程序效率低下。另外,本节中介绍的两个工具——输出中间结果和计时函数,都是相当实用的。

2.4 算法竞赛中的输入输出框架

例题2-5 数据统计
输入一些整数,求出他们的最小值、最大值和平均值(保留3位小数)。输入保证这些数都是不超过1000的整数。
样例输入:
2 8 3 5 1 7 3 6
样例输出:
1 8 4.375

分析:此题的关键在于:整数的个数是不确定的,下面首先给出第一个程序:

程序2-9 数据统计(有bug)

#include
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 %.3f\n",min,max,(double)s/n);
}

(1)scanf函数有返回值,返回的是成功输入的变量个数,当输入结束时,scanf函数无法再次读取x,将返回0。
(2)测试程序时,输入“2 8 3 5 1 7 3 6”,按回车键,但并未显示任何结果。这是因为scanf的输入格式,空格、TAB、回车符都是无关紧要的,所以按回车键并不意味着输入的结束。
注1:在windows下,输入完毕后先按回车键,再按Ctrl+Z键,最后再按回车键即可结束输入。在Linux下,输入完毕后按Ctrl+D键即可结束输入
注2:变量在未赋值之前的值是不确定的。特别地,它不一定等于0。所以要在使用之前赋值。
(3)一种方法是定义一个很大的常数,如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这样的重定向方式读写文件。参赛之前请仔细阅读文件读写的相关规定。

注3:请在比赛之前了解文件读写的相关规定:是标准输入输出(即直接读键盘、写屏幕),还是文件输入输出?如果是文件输入输出,是否禁止用重定向方式访问文件?
注4:输入输出文件名和程序名往往都有着严格规定,不要弄错大小写,不要拼错文件名,不要使用绝对路径或相对路径。

利用文件是一种很好的自我测试方法,但如果比赛要求采用标准输入输出,就必须在自我测试完毕之后删除重定向语句。
有一种方法可以在本机测试时用文件重定向,但一旦提交到比赛,就自动“删除”重定向语句。
代码如下:

程序2-10 数据统计(重定向版)

#define LOCAL
#include
#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 %.3f\n",min,max,(double)s/n);
}

重定向的部分被写在了#ifdef和#endif中,其含义是:只有定义了符号LOCAL才编译两条freopen语句。
所以最后只需在提交之前删除#define LOCAL即可。

如果比赛要求用文件输入输出,但禁止用重定向的方式,又当如何,程序如下:

程序2-11 数据统计(fopen版)

#include
#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 %.3f\n",min,max,(double)s/n);
 fclose(fin);
 fclose(fout);
}

把scanf改成fscanf,第一个参数为fin;把printf改成fprintf,第一个参数为fout,最后执行fclose,关闭两个文件
注5:在算法竞赛中,如果不允许使用重定向方式读写数据,应使用fopen和fscanf/fprintf进行输入输出。
如果想把fopen版的程序改成读写标准输入输出,只需赋值“fin=stdin;fout=stdout;"即可,不要调用fopen和fclose。

例题2-6 数据统计Ⅱ
输入一些整数,求出它们的最小值、最大值和平均值(保留3位小数)。输入保证这些数都是不超过1000的整数。
输入包含多组数据,每组数据第一行是整数个数n,第二行是n个整数。n=0为输入结束标记,程序应当忽略这组数据。相邻两组数据之间应输出一个空行。
样例输入:
8
2 8 3 5 1 7 3 6
4
-4 6 10 0
0
样例输出
Case 1:1 8 4.375
(这里是空行)
Case 2:-4 10 3.000

分析:本题和例题2-5本质相同,但是输入输出方式有一定的变化。由于这样的格式在算法竞赛中非常常见。所以在这里展开。

程序2-12 程序统计Ⅱ

#include
#define INF 1000000000
int main(){
 int x,n=0,min=INF,max=-INF,s=0,kase=0;
 while(scanf("%d",&n)==1 && n){
  int s=0,min=INF,max=-INF;
  for(int i=0;i<n;i++){
   scanf("%d",&x);
   s+=x;
   if(x>max) max=x;
   if(x<min) min=x;
  }
  if(kase) printf("\n");
  printf("Case %d: %d %d %.3f\n",++kase,min,max,(double)s/n);
 } 
 return 0;
}
  • 输入循环问题。题目说了n=0位输入标记,为什么还要判断scanf的返回值?答案是:为了鲁棒性(robustness)。有时会出现题目指明以n=0为结束标记而真实数据忘记以n=0结尾的情形。
    注6:在算法竞赛中,偶尔会出现输入输出错误的情况。如果程序鲁棒性强,有时能在数据有瑕疵的情况下仍然给出正确的结果。程序的鲁棒性在工程中也非常重要。
  • kase是当前数据编号的计数器

注7:在多数据的题目中,一个常见的错误是:在计算完一组数据后某些变量没有重置,影响到下组数据的求解。
注8:当嵌套的两个代码块中有同名变量时,内层的变量会屏蔽外层变量,有时会引起十分隐蔽的错误。

2.5 注解与习题

习题2-1 水仙花数
输出100~999中的所有水仙花数。若3位数ABC满足ABC=A3+B3+C3,则称其为水仙花数。例如153=13+53+33,所以153是水仙花数

#include
int main(){
 int a,b,c,x,y,z;
 for(a=1;a<=9;a++)
  for(b=0;b<=9;b++)
   for(c=0;c<=9;c++){
    int m=a*100+b*10+c;
    x=a*a*a;
    y=b*b*b;
    z=c*c*c;
    if(m == (x+y+z))
    printf("%d%d%d\n",a,b,c); 
   }
}

另一种方法可以从分别求出个位、十位、百位进行考虑

习题2-2 韩信点兵
相传韩信才智过人,从不直接清点自己军队的人数,只要让士兵先后以三人一排、五人一排、七人一排地变换队形,而他每次只掠一眼队伍的排尾就知道总人数了。输入包含多组数据,每组数据包含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
int main(){
 int a,b,c,count=0;
 while(scanf("%d %d %d",&a,&b,&c)==3){
 for(int i=10;i<=100;i++)
 {
  if(i%3==a && i%5==b && i%7==c){
  printf("Case %d: %d\n",++count,i);
  break;
 }
  else if(i==100)
  printf("Case %d: No answer\n",++count);
 }
  }
}

习题2-3 倒三角形
输入正整数n<=20,输出一个n层的倒三角形。

#include
int main(){
 int n;
 scanf("%d",&n);
 for(int i=1;i<=n;i++){
  for(int j=1;j<i;j++)
  printf(" ");
  for(int z=1;z<=(n-i)*2+1;z++)
  printf("#");
  printf("\n"); 
 } 
}

思路:注意要找到每行输出’#'的个数和行数的规律,以及每行空格数的规律。

习题2-4 子序列的和
输入两个正整数n6,输出1/n2+1/(n+1)2+···+1/m2,保留5位小数。输入包含多组数据,结束标记为n=m=0。

#include
int main(){
 int n,m,count=0;
 while(1){ //指的是无限循环,直到遇到控制条件(如break) 
  double sum=0;
  scanf("%d%d",&n,&m);
  if(n==m && n==0) break;
  for(int i=n;i<=m;i++){
   //sum+=1.0/(i*i);//这样会溢出
   //sum+=1.0/i/i;//另一种不会溢出的写法
   sum+=(1.0/i)*(1.0/i);
  }
  printf("Case %d:%.5f\n",++count,sum);
 }
}

注:

  • 在输入包含多组数据的题型中,要记得每次循环开始时,为变量重置,否则会导致后面计算出错。
  • 掌握while(1)的用法。
  • 注意乘法溢出问题!

习题2-5 分数化小数
输入正整数a,b,c,输出a/b的小数形式,精确到小数后c位。a,b<=106,c<=100。输入包含多组数据,结束标记为a=b=c=0。
样例输入:
1 6 4
0 0 0
样例输出:
Case 1:0.1667

#include
int main(){
 int a,b,c,count=0;
 while(1){
  scanf("%d%d%d",&a,&b,&c);
  double a1=a;
  double b1=b;
  if(a==b && b==c && c==0)
  break;
  printf("Case %d:%.*lf\n",++count,c,a1/b1);
 }
}

注:

  • count为计数器
  • a和b一开始输入时要求是整数,但后面相除为小数形式,所以后面需要改变成double类型,也可以在别的地方进行改变,比如在最后用1.0*a/b或者在最后(double)a/(double)b
  • %.*lf是特殊的输出符,可以用变量表示小数点后的尾数,对我而言是一个新的知识,要记住!

习题2-6 排列
用1,2,3,···,9组成3个三位数abc,def和ghi,每个数字恰好使用一次,要求abc:def:ghi=1:2:3,按照“abc def ghi”的格式输出所有解,每行一个解。提示:不必太动脑筋。

#include
int main(){
 for(int i=123;i<=329;i++){
  int n=i*2;
  int m=i*3;
  if((i%10+i/10%10+i/100+n%10+n/10%10+n/100+m%10+m/10%10+m/100)==45
  && ((i%10)*(i/10%10)*(i/100)*(n%10)*(n/10%10)*(n/100)*(m%10)*(m/10%10)*(m/100))==362880)
  printf("%d %d %d\n",i,n,m);
 }
}

注:

  • 第一个关键问题在于1:2:3可以说明假设用一个最小的三位数就可以表示出其他的二个数,然后就是最小的三位数是123,它的最大值是987/3得到329,所以1倍的三位数abc的范围就是123~329。

  • 第二个关键问题在于:如何每个数字恰好使用一次,而解决办法是1 ~ 9 加起来只能是45,所以要把三个三位数的个位、十位、百位分离出来,加起来看看是否为45,以及1~9乘起来只能等于362880。两个条件缺一不可,否则答案会出错

思考题:
题目2: 运行结果是 无限循环
目的在于,在定义循环变量时,尽量采用int型及整数的加减,因为循环的本质意义就是 通过各种条件来控制语句重复运行次数,而这个次数本身就是整数,要实现小数的功能尽量通过循环中的语句来实现。

你可能感兴趣的:(蓝桥杯备考——算法竞赛入门经典(第2版)学习笔记2)