我们接着第一天开始讲。
for循环
先说for循环的格式:for(初始化;条件;调整) 循环体;
第一次进入循环时,执行初始化的语句;然后判断是否满足条件;然后进行循环体,然后进行调整,然后条件、循环体、调整。。。。直到条件不满足,或者循环体中有break语句,才会跳出循环。
例题:判断aabb样式的数(前两位数字相同,后两位数字也相同)是否为完全平方数。
我们可以通过伪代码来思考和描述算法。
//伪代码
for(int a=1;a<=9;a++)
for(int b=0;b<=9;b++)
if(aabb是完全平方数) printf("%d\n",aabb);
然后我们来用代码来实现if里面的条件和输出里面的aabb两个非法的地方。
// 枚举平方根x
#include<stdio.h>
#include<math.h>
int main()
{
for(int i=1; ;i++)
{
if(i<=1000) continue;
if(i>9999) break;
int a = i / 100;
int b = i % 100;
if(a%11==0 && b%11==0) printf("%d\n", i);
}
return 0;
}
这个问题我们叫它7744问题,因为符合完全平方数的aabb型数的,只有7744一个数
这里还有一个方法,筛选出aabb的四位数,然后判断它是不是完全平方数。可以把他先平方根,再取整,最后平方看看与之前的aabb的数是否相等。
#include <stdio.h>
#include <math.h>
int main()
{
for(int a=1;a<=9;a++)
for(int b=0;b<=9;b++)
{
int n = a*1100 + b*11;
int m = floor(sqrt(n)+0.5);
if(m*m == n) printf("%d\n", n);
}
return 0;
}
这个方法也可以实现,我们来分析一下这个方法。
首先就是floor()函数,他是取整函数,是不超过当前浮点数的最大整数。
其次就是为什么要加0.5,这是因为浮点数的运算存在误差。如果在经过大量运算后,整数1变成了0.99999,那floor()函数就会当成0处理,而不是1,所以我们这里加一个0.5来减小误差的影响。当然,这并不是完全解决了问题,0.5也是浮点数,本身也可能会有误差。
while循环
来个例题,输出一个大于1的自然数n(n<10^9),若n为奇数,则n变成3n+1,否则变为n的一半。经过n次变换,最终会为1。我们要输出需要变换的次数。
#include <stdio.h>
int main()
{
int n, count = 0;
scanf("%d", &n);
while(n>1)
{
if(n%2 == 1) n = 3*n+1;
else n /= 2;
count++;
}
printf("%d\n", count);
return 0;
}
这个程序有个bug,我们输入一个接近109的数值,测试一下。
这个1肯定是错误的答案,我们用printf来看看错误出在哪。我们发现第一次输出的n是-1332004332,在第一天我们也说到了这个问题,是超出int的最大范围了,int只能从-2147483648~2147483647(2-31 ~231-1),这里的错误就是乘法溢出。
那如何解决呢?
#include <stdio.h>
int main()
{
int num, count = 0;
scanf("%d", &num);
long long n = num;
while(n>1)
{
if(n%2 == 1) n = 3*n+1;
else n /= 2;
count++;
}
printf("%d\n", count);
return 0;
}
这里我们用long long这个数据类型(C99中新增的)来解决溢出这个问题(用long也会溢出,可以试试),然后输入改成%lld或者%I64d。这里要注意这也是不保险的,在WinGW的gcc中要把%lld改成%I64d,但是奇怪的是VC2008里面还得用%lld,很奇怪。。= =不过,Linux中都是用%lld。
Windows中为了保险起见可以用常用C++输入输出流或者自定义的输入输出方法,后面我们再讨论。
所以我们上面的代码没有直接的用%lld或%I64d输入,而是用%d输入,然后在定义一个long long类型的变量将int型换成long long类型的数据。当然,我自己也在Dev里试了试%lld和%I64d都可以正常使用,直接输入都没有出现问题。
do-while循环
我们再来看看do-while
再来一道题,pi/4=1-1/3+1/5-1/7+1/9-…,直到最后一项小于10-6。
#include <stdio.h>
int main()
{
double sum=0.0,temp;
int i=0;
do
{
temp = 1.0/(2*i+1);
if(i%2==0) sum += temp;
else sum -= temp;
i++;
} while(temp >= 1e-6);
printf("%.6f\n", sum);
return 0;
}
do-while和while很类似,先执行do中的语句,然后判断while里的条件,满足就再执行do中的语句,不满足就跳出循环。
循环的代价
计算S=1!+2!+3!+…+n!的末6位(不含前导0),n<=10-6,n是阶乘。
这个问题很简单
#include <stdio.h>
int main()
{
int n;
int sum=0;
scanf("%d", &n);
for(int i=1;i<=n;i++)
{
int num = 1;
for(int j=1;j<=i;j++)
{
num *= j;
}
sum += num;
}
printf("%d\n", sum % 1000000);
return 0;
}
这个程序是可以运行的,虽然也会出现溢出这种问题,但是,关键问题在于它运行的很慢,自己的电脑输出的时候都会有1秒多的延时。
接下来我们给出优化后的代码
#include<stdio.h>
#include<time.h>
int main()
{
const int MOD = 1000000; //常量定义,改善了可读性,方便修改
int n, sum=0;
scanf("%d", &n);
for(int i=1;i<=n;i++)
{
int num =1;
for(int j=1;j<=i;j++)
num = (num * j % MOD);
sum = (sum+num) % MOD;
}
printf("%d\n", sum);
printf("Time used = %.2f\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
clock()函数是计时函数,函数返回程序目前为止运行的时间,在程序结束的时候调用此函数,可以获得整个程序的运行时间。time.h和clock()函数可以直接获得程序运行时间,CLOCKS_PER_SEC和操作系统有关,最好不要直接使用clock()的返回值,应该总是除以CLOCKS_PER_SEC。
我们输入20测试一下,输出的时间不是0s而是1s多,这是因为输出的时间把键盘输出也算了进去,我们可以在Windows命令行下输出ehco 20 | abc
,操作系统会自动把20输入,abc是程序名。(这种方法称为管道)
至于命令行,在Windows里面Ctrl+R(华为电脑是Start+R)调出运行窗口,输入cmd回车,然后进入的就是命令行界面。Linux里面的终端就是命令行。这里我们只截了Windows的图。
你的程序名,就是Dev中.cpp源文件的名称,编译运行后会生成一个.exe的文件。在尝试很多的n之后(比如20,40,80。。。),越往后面越发现n成倍增长后,时间×4倍增长。这里我们还需注意一点,就是转换路径。
一开始进入命令行,他会默认在C盘的User\25113(每个人不一样),>是命令提示符输出命令后,Enter回车执行命令。转换路径的指令是cd,转换到根目录直接输入就好了,比如这里我们从C:\User\25113换到D:,输入D:回车,就转换好了,因为我的目录都是中文为了减少麻烦,我将代码的.cpp和.exe文件复制了一份,放在D盘下,结果显示,运行成功。time也终于为0.00了。
现在,我们知道了循环有两个重要的问题,算法运算溢出和程序效率低下,这也是我们后面继续讨论的地方。
算法竞赛中的输入输出框架
输入一些数,求出最小值、最大值和平均值(保留三位小数)。数都是一些不超过1000的整数。
首先是一些数,没有限制是几个数,所以我们用循环来接收输入的数据。
#include <stdio.h>
int main()
{
int n,sum=0,count=0,max=0,min=1000;
while(scanf("%d", &n) == 1)
{
sum += n;
count++;
if(max<n) max=n;
if(min>n) min=n;
}
printf("%d %d %.3f\n", min, max, (double)sum/count);
return 0;
}
运行这段程序的时候,当你输入完成按回车的时候,程序没有输出结果,这是因为我之前提到过的,空格、Tab和回车都不影响输入,那怎么才算是告诉计算机我们输入完成了?先按回车,再Ctrl+Z,再回车,就是输入完成了。(Linux里输入完毕按Ctrl+D就好了)
好,我们来看代码,首先是while的条件输入==1?
scanf()函数是有返回值的,输入成功就返回1,否则返回0,这样有输入的时候scanf()返回1,条件为true,while循环执行循环体;当停止输入时,scanf返回0,条件为false,跳出循环。输出那里的(double)sum/count,double就是强制类型转换,和我之前在Python中提到的强制转换类似。
接下来我们分析这个程序,min和max一定要赋值,不能不赋值,直接放入循环与值比较。在C中变量未赋值前的值是不确定的,不一定等于0。这里max为0,min为1000(因为输入最大不超过1000)。
除了上面我们说过的管道的方法和手动输出的方法,还有一种自动输入的方法,把输入数据保存在文件中,输出数据也保存在文件中。这样只要事先在文件中输入要输入的数据,就不必每次重新输入。在大多的算法竞赛中,输入数据和标准答案都是保存在文件中的。
使用文件最简单的方法就是使用输入输出重定向,freopen()
但是,有点比赛不允许访问文件,或者允许访问文件但不许使用重定向方式读写文件。比赛之前一定要仔细阅读文件读写的相关规定,是标准输入输出(就是标准I/O,即直接读键盘、写屏幕),还是文件输入输出(是否可以使用重定向的方法访问文件?)
在比赛时,应当严格遵守比赛的文件名规定,包括程序文件名、输入输出文件名。不要搞错大小写、拼错、使用绝对路径和相对路径。
下面的代码可以在本机测试时用文件重定向,一旦提交到比赛,就自动“删除”重定向语句。
#define LOCAL
#include <stdio.h>
#define INF 1000
int main()
{
#ifdef LOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif
int n, sum=0, min=INF, max=-INF, count=0;
while(scanf("%d", &n) == 1)
{
sum += n;
count++;
if(max < n) max = n;
if(min > n) min = n;
}
printf("%d %d %.3f\n", min, max, (double)sum/count);
return 0;
}
在代码源文件和可执行文件的文件目录下创建txt文件,写入输入的数据,Ctrl+S保存关闭,重命名为data.in(扩展名.txt也要删掉);然后在创建一个txt文件,重命名为data.out(扩展名也要删)然后运行程序,用记事本打开data.out文件,就可以看见输出的数据了。
在提交的时候,只要删除第一行代码就可以了(或者在编译选项而不是程序里定义这个LOCAL符号,这样就可以不用删除就可以提交了)
当然,写程序肯定不是一下就写好的,肯定有很多bug,然后渐渐修改,直到完成,写个算法花了一个多小时的也很普遍。所以那些测试性的语句可以注释掉,这样程序再次出现问题是可以把注释符号去掉,再次测试。这是一段典型的竞赛代码。
这里也有不使用重定向方法写的代码:
#include <stdio.h>
#define INF 1000
int main()
{
FILE *fin, *fout;
fin = fopen("data.in", "rb");
fout = fopen("data.out", "wb");
int n, sum=0, min=INF, max=-INF, count=0;
while(fscanf(fin, "%d", &n) == 1)
{
sum += n;
count++;
if(max < n) max = n;
if(min > n) min = n;
}
fprintf(fout, "%d %d %.3f\n", min, max, (double)sum/count);
fclose(fin);
fclose(fout);
return 0;
}
我们把data.out中Ctrl+A(全选),然后Delete,Ctrl+S,然后退出,运行程序,再打开data.out文件,我们也会发现输出的数据,正确无误。和Java中的输入输出流类似,在最后都要关闭。
重定向的方法和fopen两种方法各有优缺。重定向的方法简单、自然,但不能同时读写和标准输入输出;fopen的方法有点繁琐,但是灵活性大,可以反复打开并读写文件。
当然,fopen这个版本的代码如果要提交,只需赋值fin = stdin; fout=stdout;
,然后不要调用fopen和fclose方法就可以了。
好的,我们再来一个加强版的数据统计。在上个例子的基础上,我们的输入包含多组数据,每组数据第一行是整数个数n,第二行是n个整数。n=0为输入结束标记,程序应当忽略这组数据。相邻两组数据之间应该输出一个空行。
#include<stdio.h>
#define INF 1000
int main()
{
int x, n=0, min=INF, max=-INF, s=0, kase=0;
while(scanf("%d", &n) == 1 && n)
{
int s=0;
for(int i=0;i<n;i++)
{
scanf("%d" ,&x);
s += x;
if(min > x) min = x;
if(max < x) max = x;
}
if(kase) printf("\n");
printf("Case %d: %d %d %.3f\n", ++kase, min, max, (double)s/n);
}
return 0;
}
kase在这里就是一个计数器,n是输入的第一行数据,x是输入的第二行数据,因为若第一行输入0时结束程序,所以while里面的条件‘与’上了n,n为0时,条件为false。
那为什么不直接写个n就好了,还要判断scanf是否输入成功干嘛呢?
这里是为了增强程序的鲁棒性,程序在设计过程中或多或少都会出现一些错误,如果程序能自动处理好那些有瑕疵的数据
接下来我们就说说程序存在的问题,程序是有bug的。就是min和max的值会保留上一组的值,当然为什么求和可以不受上组数据影响,那是因为s的归零赋值放在了while循环里,每次循环开始就清零,我们也可以把min和max的最大最小赋值也放在这里,从而实现每组数据一来,就重置min,max和平均数。
竞赛题目中像这样“多组数据”的题目很多,多组数据很容易出现的错误就是计算完一组数据后某些变量没有重置,影响到下一组数据的计算。若嵌套的两个代码块中有同名变量时,内层的变量会屏蔽外层变量,有时会引起十分隐蔽的错误。
我们可以在编译的时候加一个-Wall就会看到一条警告:warning: unused variable 's' [-Wunused-variable]
(警告:没有用过的变量’s’)
用编译选项-Wall编译程序时,会给出很多警告信息(但不是全部),以帮助程序员查错。但这并不能解决所有的问题:有些“错误”程序是合法的,只是这些动作不是所期望的。