NOIP学习小站
西安交通大学附属中学航天学校

选择结构例题

tangyj626阅读(735)

一、除法结果

【问题描述】

输入两个整数\(a,b\),按如下三种情况计算 \(a \div b\) 的结果:

  1. 如果\(b =0\),输出Error
  2. 如果\(a\)能被\(b\)整除,输出除法的商;
  3. 如果\(a\)不能被\(b\)整除,输出除法的结果,保留2位小数。

其实题目描述已经很清晰了,分三种情况处理,可以很容易写出if...else if...else的多重分支结构:

#include<iostream>
#include<iomanip>
using namespace std;
int main()
{
	int a,b;
	cin>>a>>b;
	if(b==0) cout<<"Error"<<endl;
	else if(a%b==0) cout<<a/b<<endl;
	else cout<<fixed<<setprecision(2)<<1.0*a/b<<endl;
    return 0;
}

此外,这里介绍一种“特判”的编程思路,特判就是特殊判断处理,也就是把某个或者一些特殊的情况(使用if语句)优先处理。例如本题中的除数为0的情况就可以用特判优先处理:

#include<iostream>
#include<iomanip>
using namespace std;
int main()
{
	int a,b;
	cin>>a>>b;
	if(b==0) {		//特判 
		cout<<"Error"<<endl;
		return 0;	 //注意:这里的return 0;会强制结束程序 
	}
	//有了前面的特判,接下来就不用再考虑b==0的情况了
	if(a%b==0) cout<<a/b<<endl;
	else cout<<fixed<<setprecision(2)<<1.0*a/b<<endl;
    return 0;
}

注意体会这里介绍的“特判”的解题思路。上面的例子还不能很好地体现出“特判”解题的优势。当某些特殊情况很容易得出结果,其它情况需要较复杂处理的时候,使用特判将特殊情况优先处理,然后就可以集中精力处理其它情况。

二、[洛谷]P1425 小鱼的游泳时间

顺序结构例题中已经做过讲解,再来回顾一下顺序结构处理方法:

#include<iostream>
using namespace std;
int main()
{
    int a,b,c,d,t;
    cin>>a>>b>>c>>d;
    t = (c*60+d)-(a*60+b);
    cout<<t/60<<" "<<t%60;
    return 0;
}

这里不能直接输出\(c-a\)作为小时数,\(d-b\)作为分钟数,因为有可能\(d<b\)。其实可以将这样的情况用if语句特殊处理:

#include<iostream>
using namespace std;
int main()
{
    int a,b,c,d,h,m;
    cin>>a>>b>>c>>d;
    h = c-a;
    m = d-b;
    if(m<0){		//分钟数为负数
		h--;		//小时数-1 
    	m += 60;    //借1当60,分钟数+60 
	}
	cout<<h<<" "<<m;
    return 0;
}

三、小规模排序

1.两个整数升序排序

问题描述:输入两个整数\(a,b\),按照从小到大的顺序输出这两个数。

思路一:如果\(a<b\),那么依次输出\(a,b\),否则依次输出\(b,a\)

#include<iostream>
using namespace std;
int main()
{
    int a,b;
    cin>>a>>b;
    if(a<b) cout<<a<<" "<<b<<endl;
    else cout<<b<<" "<<a<<endl;
    return 0;
}

思路二:程序最后依次输出\(a,b\)。如果\(a \le b\),此时依次输出\(a,b\)正好是升序排序结果。如果\(a>b\),在依次输出\(a,b\)前可以交换\(a,b\)的值(题目要求是升序,\(a>b\)此时\(a,b\)与要求顺序不符合,那么交换两者的值)。

#include<iostream>
using namespace std;
int main()
{
    int a,b,t;
    cin>>a>>b;
    if(a>b){
    	t = a;
    	a = b;
    	b = t;
	}
	cout<<a<<" "<<b<<endl;
    return 0;
}

2.三个整数升序排序

问题描述:输入三个整数\(a,b,c\),按照从小到大的顺序输出这三个数。同上一个问题比较,问题规模稍微加大。

思路一:分六种情况讨论,使用if...else if...实现:

  1. 如果a<=b && b<=c,那么输出a,b,c;
  2. 否则如果a<=c && c<=b,那么输出a,c,b;
  3. 否则如果b<=a && a<=c,那么输出b,a,c;
  4. 否则如果b<=c && c<=a,那么输出b,c,a;
  5. 否则如果c<=a && a<=b,那么输出c,a,b;
  6. 否则(也就是c<=b && b<=a),输出c,b,a;
#include<iostream>
using namespace std;
int main()
{
    int a,b,c;
    cin>>a>>b>>c;
    if(a<=b && b<=c) cout<<a<<" "<<b<<" "<<c<<endl;
    else if(a<=c && c<=b) cout<<a<<" "<<c<<" "<<b<<endl;
    else if(b<=a && a<=c) cout<<b<<" "<<a<<" "<<c<<endl;
    else if(b<=c && c<=a) cout<<b<<" "<<c<<" "<<a<<endl;
    else if(c<=a && a<=b) cout<<c<<" "<<a<<" "<<b<<endl;
    else cout<<c<<" "<<b<<" "<<a<<endl;
    return 0;
} 

思路二:仍然使用交换变量的方式来处理。按照下面的步骤依次处理:

  1. 如果\(a>b\),那么交换\(a,b\)的值(\(a,b\)顺序不对则交换)
  2. 如果\(a>c\),那么交换\(a,c\)的值(\(a,c\)顺序不对则交换)

经过第1、2步处理后,最小值肯定在变量 \(a\) 中,还需要考虑 \(b,c\) 的顺序是否正确

  1. 如果\(b>c\),那么交换\(b,c\)的值(\(b,c\)顺序不对则交换)
  2. 依次输出\(a,b,c\)的值
#include<iostream>
using namespace std;
int main()
{
    int a,b,c,t;
    cin>>a>>b>>c;
    //if语句中如果有多条语句需要使用{},也可以用,将多条语句分隔开,最后用;
    //这个时候是一个逗号表达式语句,是一个完整的语句,可以省略{}
    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;
    cout<<a<<" "<<b<<" "<<c;
    return 0;
} 

注意:上面的程序注释中介绍了逗号表达式,请仔细阅读!

其实也可以按照下面的步骤来依次处理:

  1. 如果\(a>b\),那么交换\(a,b\)的值(\(a,b\)顺序不对则交换)
  2. 如果\(b>c\),那么交换\(b,c\)的值(\(b,c\)顺序不对则交换)

经过第1、2步处理后,最大值肯定在变量 \(c\) 中,还需要考虑 \(a,b\) 的顺序是否正确

  1. 如果\(a>b\),那么交换\(a,b\)的值(\(a,b\)顺序不对则交换)
  2. 依次输出\(a,b,c\)的值

四、判断三角形形状

输入三个浮点数\(a,b,c\),判断\(a,b,c\)作为边长能否组成三角形,如果能够组成三角形,判断三角形的形状(等边 or 等腰 or 直角 or 普通)

思路

1.三条\(a,b,c\)长度的线段能组成三角形的条件:任意两边之和大于第三边,任意两边之差小于第三边。其实只需要判断“任意两边之和大于第三边”即可。注意这里任意的含义,表示随意选出来的两条边的和都要大于第三边,那么条件可以描述为: a+b>c && b+c>a && c+a>b

2.判断是否为等边三角形、等腰三角形:等边三角形的条件很容易想到 a==b && b==c。等腰三角形的腰可以是\(a,b\),也可以是\(b,c\),也可以是\(c,a\),这三种情况都有可能,那么条件应该描述为:a==b || b==c || c==a。再来考虑判断顺序,对于等边三角形和等腰三角形,应该先判断等边再判断等腰(思考原因)。

3.判断直角三角形:和判断等腰三角形相似,\(a,b,c\)都有可能是斜边,所以条件可以描述为:a*a+b*b==c*c || b*b+c*c==a*a || c*c+a*a==b*b

#include<iostream>
using namespace std;
int main()
{
	double a,b,c;
	cin>>a>>b>>c;
	if(a>0 && b>0 && c>0 && a+b>c && b+c>a && c+a>b){
		//判断顺序:
		//等边->直角(等腰直角 or 普通直角)->等腰->普通
		//思考:这个判断顺序可以随意改变吗? 
		if(a==b && b==c){
			cout<<"等边三角形";
		}else if(a*a+b*b==c*c || b*b+c*c==a*a || c*c+a*a==b*b){
			if(a==b || b==c || c==a){  //几乎不可能找到测试样例满足等腰直角三角形
				cout<<"等腰直角三角形";
			}else{
				cout<<"普通直角三角形";
			}
		}else if(a==b || b==c || c==a){
			cout<<"等腰三角形";
		}else{
			cout<<"普通三角形";
		} 
	}else{
		cout<<"不能组成三角形";
	}
	return 0;
}

五、小组成员成绩分析

小组有4位成员,参加了测试,测试总分为100分,60分及以上为及格,85分及以上为优秀。现在提供4位小组成员的成绩\(a,b,c,d\),判断下面的情况是否成立:

  1. 恰好只有1位成员不及格
  2. 不及格人数不超过3人
  3. 及格人数不少于3人,优秀人数不少于2人
  4. 不及格人数和优秀人数的和不超过2人

思路一:借助复合逻辑运算列清所有条件

对于情况1,“恰好只有1位成员不及格”,判断的条件可以描述为:(a<60 && b>=60 && c>=60 && d>=60) || (a>=60 && b<60 && c>=60 && d>=60) || (a>=60 && b>=60 && c<60 && d>=60) || (a>=60 && b>=60 && c>=60 && d<60)

对于情况2,“不及格人数不超过3人”,可以细化为“不及格人数为0”或者“不及格人数为1”或者“不及格人数为2”或者“不及格人数为3”:

  1. “不及格人数为0”和“全部都及格”是同一个意思,可以描述为:a>=60 && b>=60 && c>=60 && d>=60
  2. “不及格人数为1”,可以描述为:(a<60 && b>=60 && c>=60 && d>=60) || (a>=60 && b<60 && c>=60 && d>=60) || (a>=60 && b>=60 && c<60 && d>=60) || (a>=60 && b>=60 && c>=60 && d<60)
  3. “不及格人数为2”,可以描述为:(a<60 && b<60 && c>=60 && d>=60) || (a<60 && b>=60 && c<60 && d>=60) || (a<60 && b>=60 && c>=60 && d<60) || (a>=60 && b<60 && c<60 && d>=60) || (a>=60 && b<60 && c>=60 && d<60) || (a>=60 && b>=60 && c<60 && d<60)
  4. “不及格人数为3”和“恰好只有1个人及格”是同一个意思,可以描述为:(a>=60 && b<60 && c<60 && d<60) || (a<60 && b>=60 && c<60 && d<60) || (a<60 && b<60 && c>=60 && d<60) || (a<60 && b<60 && c<60 && d>=60)

经过上面分析,“不及格人数不超过3人”的完整条件可以描述为:(a>=60 && b>=60 && c>=60 && d>=60) || ((a<60 && b>=60 && c>=60 && d>=60) || (a>=60 && b<60 && c>=60 && d>=60) || (a>=60 && b>=60 && c<60 && d>=60) || (a>=60 && b>=60 && c>=60 && d<60)) || ((a<60 && b<60 && c>=60 && d>=60) || (a<60 && b>=60 && c<60 && d>=60) || (a<60 && b>=60 && c>=60 && d<60) || (a>=60 && b<60 && c<60 && d>=60) || (a>=60 && b<60 && c>=60 && d<60) || (a>=60 && b>=60 && c<60 && d<60)) || ((a>=60 && b<60 && c<60 && d<60) || (a<60 && b>=60 && c<60 && d<60) || (a<60 && b<60 && c>=60 && d<60) || (a<60 && b<60 && c<60 && d>=60))。要完全理清、读懂,表示压力很大!为了增强可读性,可以这样来编写程序:

bool b1 = a>=60 && b>=60 && c>=60 && d>=60;

bool b2 = (a<60 && b>=60 && c>=60 && d>=60) || (a>=60 && b<60 && c>=60 && d>=60) || (a>=60 && b>=60 && c<60 && d>=60) || (a>=60 && b>=60 && c>=60 && d<60);

bool b3 = (a<60 && b<60 && c>=60 && d>=60) || (a<60 && b>=60 && c<60 && d>=60) || (a<60 && b>=60 && c>=60 && d<60) || (a>=60 && b<60 && c<60 && d>=60) || (a>=60 && b<60 && c>=60 && d<60) || (a>=60 && b>=60 && c<60 && d<60);

bool b4 = (a>=60 && b<60 && c<60 && d<60) || (a<60 && b>=60 && c<60 && d<60) || (a<60 && b<60 && c>=60 && d<60) || (a<60 && b<60 && c<60 && d>=60);

if(b1 || b2 || b3 || b4){

}

对于情况3,使用“复合逻辑运算列清所有条件”这样的思路,表示压力很大!

对于情况4,感觉无从下手,其实“不及格人数和优秀人数的和不超过2人”和“成绩不低于60分并且小于85分的人数不低于2人”是同一个意思,不过使用“复合逻辑运算列清所有条件”处理起来仍然很繁琐!

思路二:其实现实生活中我们的处理上面问题的方法是非常朴素的——数数。编程时可以直接模拟现实生活中的数数,使用“计数器”来统计及格(或者不及格)的人数即可。算法有一个重要的来源就是模拟现实生活中处理问题的方法。

对于情况1,参考代码如下:

#include<iostream>
using namespace std;
int main()
{
    int a,b,c,d,tot=0;
    cin>>a>>b>>c>>d;
    if(a<60) tot++;
    if(b<60) tot++;
    if(c<60) tot++;
    if(d<60) tot++;
    if(tot==1){
    	cout<<"恰好只有一人不及格";
	}
    return 0;
} 

对于情况2,参考代码如下:

#include<iostream>
using namespace std;
int main()
{
    int a,b,c,d,tot=0;
    cin>>a>>b>>c>>d;
    if(a<60) tot++;
    if(b<60) tot++;
    if(c<60) tot++;
    if(d<60) tot++;
    if(tot<=3){
    	cout<<"不及格人数不超过3人";
	}
    return 0;
} 

对于情况3,参考代码如下:

#include<iostream>
using namespace std;
int main()
{
    int a,b,c,d,tot1=0,tot2=0;  //tot1不及格人数,tot2优秀人数
    
    cin>>a>>b>>c>>d;
    
    if(a<60) tot1++;
    else if(a>=85) tot2++;
    
    if(b<60) tot1++;
    else if(b>=85) tot2++;
    
    if(c<60) tot1++;
    else if(c>=85) tot2++;
    
    if(d<60) tot1++;
    else if(d>=85) tot2++;
    
    if(tot1>=3 && tot2>=2){
    	cout<<"及格人数不少于3人,优秀人数不少于2人";
	}
    return 0;
} 

对于情况4,我们可以自行在情况3的基础上修改代码解决问题。

程序的调试

tangyj626阅读(269)

当运行程序发现没有编译错误但是结果与预期不符的时候,往往是因为程序中出现了逻辑错误,有时候排查逻辑错误是一件痛苦的事情,有可能是不经意错误的写法(例如将判断相等写成了赋值语句),也有可能是对语言基础知识掌握不够好写出有漏洞的语句,也有可能是考虑不周全导致设计的算法有缺陷(例如遗漏了某些重要的操作),这里介绍一些常用的程序调试方法。

发现程序存在逻辑问题,首先应该分析输出结果,考虑程序可能在哪里出现问题。

例如预期的计算结果是小数,程序多次运行输出却一直都是整数,那么应该联想到是不是用了整数除以整数导致结果是整数。又例如前面的例题“P1425 小鱼的游泳时间”,如果直接小时数相减、分钟数相减,会发现结果中的分钟数可能会出现负数,很自然联想到分钟数直接相减,可能不够减导致出现负数,要解决这个逻辑错误,要么重新设计算法(顺序结构例题里有介绍),要么分钟数出现负数时用if语句来特别处理。

如果程序较复杂、代码较多,可能不容易马上想到哪里有问题,这个时候应该仔细阅读程序代码,尝试找出一些容易发现的问题,特别是初学者容易犯的错误。例如判断相等写成赋值(例如if(n==0)不小心写成if(n=0))、表示条件的关系比较错误使用了连续比较(例如本来应该用a<b && b<c,不小心写成了a<b<c)、未考虑运算优先级导致的错误(例如!(n==0)写成了!n==0)等等。

如果经过上面的步骤仍然没有排查出逻辑错误,那么还有一个简单的方法,就是追踪查看关键语句处关键变量的值。查看分析关键语句处关键变量的值,一般很容易找出逻辑出错的位置,这样就能进一步修改代码避免逻辑错误了。下面介绍两种追踪查看关键语句处关键变量的值的方法。

一、借助输出语句输出变量的值来调试程序

问题描述:输入简单的两个整数的五则运算表达式,例如1 + 2、345 * 498,输出计算结果

输入格式:输入内容包括两个整数和整数间的五则运算符号(只可能是+、-、*、/、%中的一个),整数和运算符号间用一个空格隔开。测试时保证输入的运算合法(例如不会出现除数为0的情况)。

输出格式:一个整数,就是输入的运算表达式的计算结果。

分析:可以使用switch多分支结构语句处理,根据输入的代表不同运算的字符对两个整数进行相应的运算得出结果。

#include<cstdio>
int main()
{
	int a,b,c;
	char op;
	scanf("%d%c%d",&a,&op,&b);
	switch(op){
		case '+':c=a+b;break;
		case '-':c=a-b;break;
		case '*':c=a*b;break;
		case '/':c=a/b;break;
		case '%':c=a%b;break;
	}
	printf("%d",c);
    return 0;
}

左边的程序使用了%d%c%d作为输入格式符,运行程序,按题目输入格式要求输入1 + 2时,运行结果如下:

4233934

很显然,运行的结果与预期不符,程序存在逻辑问题。初步排查程序,switch语句部分应该没有问题,最后的输出结果也没问题。是不是scanf语句输入到变量的值有问题?

在scanf输入语句后面添加printf输出变量a、op、b的值,运行查看结果以验证是否能正确输入。

#include<cstdio>
int main()
{
	int a,b,c;
	char op;
	scanf("%d%c%d",&a,&op,&b);
	printf("%d %c %d",a,op,b);
	return 0;//额外添加上这一句 
	switch(op){
		case '+':c=a+b;break;
		case '-':c=a-b;break;
		case '*':c=a*b;break;
		case '/':c=a/b;break;
		case '%':c=a%b;break;
	}
	printf("%d",c);
    return 0;
}

在scanf语句后添加了printf("%d %c %d",a,op,b);来输出变量a、op、b的值,甚至还添加了一句return 0;程序运行时执行printf语句后碰到return 0;就自动结束了,这样我们能够专注于查看printf输出的结果。

仍然输入1 + 2,此时运行结果如下:

1   6815400

变量a的值正常接收到了,变量op和b的值没有正常接收到。应该是输入格式的问题,修改输入语句为:scanf("%d %c %d",&a,&op,&b);再次运行,仍然输入1 + 2,此时运行结果如下(问题解决):

1 + 2

上面介绍的就是一种初学者常用的调试程序方法,通过添加额外的输出语句来查看变量的值,进而排查程序的逻辑问题。需要特别注意的是,额外添加的用于查看变量的值以调试程序的输出语句在问题解决后一定要删除掉哦,因为这些语句输出的内容也是程序的输出内容,在提交测评的时候会导致无法通过测评。

二、借助Dev C++的调试功能调试程序

Dev C++有调试程序代码的功能,通过调试可以在调试窗口查看变量的值,这样就不用像上面那样添加额外的输出语句来调试程序了。下面介绍Dev C++中调试程序的步骤:

1.在调试前编译程序(F9快捷按键)。

2.设置断点。断点的作用是在调试的时候能够自动暂停在断点处以方便查看变量的值。要在某行设置断点,只需要点击该行的行标即可:

如上图所示,我们想查看scanf执行后变量的值,所以在紧跟scanf的switch语句(第7行)的行标处点击一下,会发现行标处出现了一个红色小圆点,整行也被红色背景醒目标识,表示已经在第7行设置了断点。如果要取消断点,再次点击行标处即可。调试时可以设置多个断点。

3.添加要查看的变量。首先点击底部面板区域的“调试”选项卡,然后点击调试面板中的“添加查看”按钮,如下图所示:

在弹出来的“新变量”对话框中,输入调试要查看的变量的变量名,这里貌似只能一次输入添加一个变量名,要查看多个变量需要多次“添加查看”操作:

添加后,在左侧面板的“调试”选项卡下可以看到所有添加查看的变量:

4.进入调试。点击底部面板“调试”选项卡中的“调试”按钮,或者使用快捷键F5,进入调试状态:

调试也是运行,不过可以借助调试在设置的断点处自动暂停并查看变量的值,所以进入调试后会自动打开运行程序的命令行窗体,如果程序需要输入数据,那么就像运行程序一样,也要在命令行窗体中输入数据。

本例中,输入数据后会执行后面的switch语句,但是此处设置了断点,调试会在断点处自动暂停,如上图所示,此时可以在左侧面板的“调试”选项卡下查看变量的值以判断断点附近是否有逻辑问题(本例在此时可以看到变量op和b没有正确接收到输入的数据)。

调试会在设置的断点处自动暂停,如果要继续执行断点后面的语句,可以点击下部面板“调试”选项卡中的“下一步”按钮。如果要停止调试,可以点击“停止执行”按钮。

循环结构与while循环

tangyj626阅读(877)

现实生活中,我们要处理的问题往往有很多规律性的重复操作,例如要找出上百万整数的最值,或者计算某次考试全班每位同学的平均分。这个时候仅仅使用前面的顺序结构和选择结构,对于重复性的每一步操作都写出对应的语句是不现实的,可以使用循环语句让计算机反复执行完全相同或者相似的任务。学习了循环结构,我们更能体会到计算机程序的威力!

一、循环结构

我们先来看一个简单的问题,输出5行Hello World,这里当然可以使用顺序结构实现:

#include<iostream>
using namespace std;
int main()
{
	cout<<"Hello World"<<endl;
	cout<<"Hello World"<<endl;
	cout<<"Hello World"<<endl;
	cout<<"Hello World"<<endl;
	cout<<"Hello World"<<endl;
    return 0;
} 

扩大问题规模,输出100行(或者成千上万行)Hello World,很显然,这个时候书写100行(甚至更多)cout<<"Hello World"<<endl;是不可取的。我们编写程序是要让使用程序的人偷懒,当然不能让编写程序的人累死!

再改变一下需求,首先输入一个正整数n,然后输出n行Hello World。这个时候即使想用上面的笨办法也无法解决了。

像上面的要重复执行完全相同的任务,使用顺序结构解决问题是不可取的,甚至也是无法完成的。这个时候需要使用程序三大结构的循环结构来解决,而循环结构不仅仅可以重复执行完全相同的任务,也能重复执行相似的任务。

我们先来看使用循环结构处理上述问题的流程图:

可以看出,判断条件成立的话,会执行输出Hello World和i++(这样被重复执行的部分称为循环体),然后又回去判断条件是否成立...直到某次回去判断发现条件不成立就退出循环不再执行循环体了。简单地说,就是当条件成立的时候,一直执行循环体

循环结构的流程图和选择结构相似,都出现了条件的判断,不同的是,循环结构有一个返回到条件判断前的流程箭头。正是这个流程实现了循环结构当条件成立的时候,一直执行循环体的特殊功能。

从流程图中可以看出,变量i对循环结构很重要,不仅仅是循环的条件是用i来描述的,并且在进入循环判断前i有初值,在循环体中还有一条语句在改变i的值(这条语句也很重要,正是因为控制变量i的值在变化所以才能在某次执行完循环体后判断条件成立了退出循环)。这里的变量i称为循环的控制变量,正是它决定了循环的执行流程。我们可以通过追踪i值的变化情况来分析循环的执行流程:

判断前
i的值
i<=5
是否成立
执行循环体执行循环体后
i的值
1Y输出Hello World;
i++;
2
2Y输出Hello World;
i++;
3
3Y输出Hello World;
i++;
4
4Y输出Hello World;
i++;
5
5Y输出Hello World;
i++;
6
6N退出循环

上面介绍的通过追踪循环控制变量值变化情况来分析循环结构执行流程的方法对于初学者来说比较重要,有了这个强有力的工具,我们能够全面把握循环结构执行流程的细节。我们后面阅读循环结构代码感觉困难的时候,可以用这样的方法去追踪控制变量分析循环执行流程,往往追踪几次重复操作就能理解循环的意图。

二、while循环

C++中常用的循环语句有for循环、while循环和do...while循环。这里先介绍while循环,原因是while循环和循环结构的流程图一致,从流程图可以毫无压力地写出while循环:

#include<iostream>
using namespace std;
int main()
{
	int i;
	i = 1;
	while(i<=5){
		cout<<"Hello World"<<endl;
		i++;
	}
    return 0;
} 
#include<iostream>
using namespace std;
int main()
{
	int i,n;
	cin>>n;
	i = 1;
	while(i<=n){
		cout<<"Hello World"<<endl;
		i++;
	}
    return 0;
} 

从上面的例子可以总结出while循环语句的用法如下:

while(条件){
	循环体; 
}

条件的描述和循环的控制变量有关,那么在while循环前得有循环控制变量的初始化语句;在循环体中,还得有语句改变控制变量的值,这样某次循环体执行后再判断循环条件时由于控制变量值改变导致条件不成立而退出循环。

对应左侧流程图,更详细while循环的一般使用方法如下:

控制变量初始化语句;
while(用控制变量描述的循环条件){
	要重复执行的语句; 
	改变控制变量的值的语句;
}

将关键字while读作“当”,那么while循环和自然语言很接近,就是当条件成立时,一直执行循环体的内容。

对于可以用循环结构解决的问题,有时候循环的次数很明显,就像上面的两个例子,有时候循环的次数不明显而循环的条件很明显,我们来看下面的例子:

分析:\(a\)是木棍的长度,循环的条件可以描述为\(a>1\),循环体中使用a / = 2;来记录木棍每次取半后的长度,使用计数器来记录取半的次数,其实就是循环执行的次数。本例循环条件很明显,循环次数不明确,其实求解的正是循环执行次数。

#include<iostream>
using namespace std;
int main()
{
	int a,days = 1;
	cin>>a;
	while(a>1){
		days ++;
		a /= 2;
	}
	cout<<days;
    return 0;
} 

三、可控的循环与无限循环

竞赛时,我们是要通过有限的步骤在有限的时间(一般是1s内)得出问题的答案,所以用到循环结构来实现重复相似计算时,肯定最后要退出循环(甚至为了提高效率需要想尽办法减少循环次数),这样的循环就是可控的循环。

但是初学者可能不小心就写出不可控的无限循环,也称为“死循环”,这样的程序提交后肯定会超时(TLE)。例如不小心将判断相等写成了赋值:

int n;
cin>>n;
while(n=1){      //n==1不小心写成n=1,造成“死循环”

}

“死循环”在竞赛时不会用到,但是在一些特殊的场合会用到,例如程序控制的自动饮水机,一旦发现需要烧水的时候就开始加热烧水,烧开后停止加热,这里编程时就需要使用死循环,这样只要接通了电源,饮水机就会永不停止地执行任务。

for循环与do...while循环

tangyj626阅读(942)

使用循环结构编程解决问题时,while循环和算法的流程图是一一对应的,最容易理解。除了while循环,C++还有高度结构化的for循环和与while相似的do...while循环。

一、for循环

首先来看两段功能相同的while循环和for循环程序:

#include<iostream>
using namespace std;
int main()
{
    int i;
    i = 1;
    while(i<=5){
        cout<<"Hello"<<endl;
        i++;
    }
    return 0;
} 
#include<iostream>
using namespace std;
int main()
{
    int i;
    //for比while更精简,高度结构化
    for(i=1;i<=5;i++){
        cout<<"Hello"<<endl;
    }
    return 0;
} 

for循环的使用格式与while循环的对应关系如下(注意for循环()中是两个分号隔开了三条语句):

for(①;②;③){
    ④;
}
①;
while(②){
    ④;
    ③;
}

可见for循环和while循环是相通的,不过for循环整合得更精简,其执行流程如右图所示,对①②③④语句具体功能描述后,for循环的使用格式如下:

for(控制变量初始化;循环条件;控制变量改变语句){
    循环体;
}

可见for循环是一种“高度结构化”的循环语句,就像一个功能模块一样,for循环不同位置的语句在其自动执行的流程图中发挥不同的作用。而且for循环中涉及到循环控制变量的语句集中在for后面的()中,所以读了for循环的头部就能够明确循环执行的具体流程,而while往往要读到循环体中修改控制变量值的语句才行,这也是for循环比较while循环的优势。

for循环和while循环是相通的,可以将for循环改写成while循环,也可以将while循环改写成for循环。一般地,要解决的问题循环次数明确的时候,习惯使用for循环;循环条件明确的时候,习惯使用while循环。

来看一个例子,计算\(1+2+3+...+100\)(也就是\(\sum\limits_{i=1}^{100}i\))的结果。如果不使用高中数学的“等差数列求和公式”,大家应该能联想到前面介绍的累加器:

#include<iostream>
using namespace std;
int main()
{
    int s = 0;
    s += 1;
    s += 2;
    s += 3;
    //...
	s += 100;
	cout<<s; 
    return 0;
}

这里的100条累加语句很显然应该借助循环结构实现,累加语句的形式都是 s += ?;,那么这样的语句应该作为重复执行100次的for循环的循环体(可以很容易总结出重复执行n次的for循环头部可以写出for(i=1;i<=n;i++)):

int i,s = 0;
for(i=1;i<=100;i++){
	s += ?;
}
cout<<s;

那么循环体中的s += ?;中?处应该填写什么内容呢?再来分析100次循环执行语句:第1次循环是 s += 1; 第2次循环是 s += 2;第3次循环是 s += 3;……第100次循环是 s += 100;,那么第i次循环应该是 s += i;。大家要注意这一个找规律并抽象归纳的过程,以后我们再探讨循环执行流程就应该说第i次循环执行什么操作,而不是停留在分析具体的第1次、第2次……循环执行什么操作。这样完整的程序代码如下:

#include<iostream>
using namespace std;
int main()
{
	int i,s = 0;
	for(i=1;i<=100;i++){
		s += i;     //第i次循环:s累加上i
	}
	cout<<s; 
    return 0;
} 

分析变量i的作用,可知其首先作为循环的控制变量,控制循环的执行流程;此外在循环体中变量i还参加了计算。

再来看一个例子:指定正整数\(n\),计算\(n\)的阶乘: \(n! = 1 \times 2 \times 3 \times ... \times n\)(也就是 \(n! = \prod\limits_{i=1}^{n}i\))

分析:这里应该使用累乘器来计算\(n!\)。首先输入正整数\(n\),累乘器变量\(t\)初值赋值为1,使用for实现n次循环,循环体中实现累乘:

#include<iostream>
using namespace std;
int main()
{
    int n;
    //计算n!,n值即使不大,但n!可能很大,最好使用long long 
    long long t = 1; 
    cin>>n;
    for(int i=1;i<=n;i++){
    	t *= i;     //第i次循环:t累乘上i
	}
	cout<<t;
    return 0;
} 

参照上面的程序,尝试编程完成下面的计算:

  1. 计算\(1^2\)+\(2^2\)+\(3^2\)+...+\(100^2\)(也就是\(\sum\limits_{i=1}^{100}i^2\))的结果
  2. 计算\(1\)+\(\frac{1}{2}\)+\(\frac{1}{3}\)+...+\(\frac{1}{100}\)(也就是\(\sum\limits_{i=1}^{100}\frac{1}{i}\))的结果(注意累加器变量的数据类型,注意整数/整数结果是整数!)

二、do...while循环

首先来对比while循环和do...while循环的流程图:

while循环执行流程图
do...while循环执行流程图

从流程图可以看出来,while循环是先判断条件是否成立,条件成立才执行循环体;do...while循环是先执行循环体,然后判断条件是否成立,如果成立继续执行循环体然后再判断。

#include<iostream>
using namespace std;
int main()
{
    int i;
    i = 1;
    while(i<=5){
        cout<<"Hello"<<endl;
        i++;
    }
    return 0;
} 
#include<iostream>
using namespace std;
int main()
{
    int i;
    i = 1;
    do{
        cout<<"Hello"<<endl;
        i++;
    }while(i<=5);   //;不能少
    return 0;
} 

while循环是当条件成立时一直执行循环体,do...while循环是执行循环体后判断是否再次重复执行循环体。一般情况下,当循环控制变量初始值相同、循环条件相同、循环体相同时,while循环和do...while循环的效果几乎是相同的。while循环有可能不会执行循环体(循环的条件上来就不成立),而do...while循环会至少执行一次循环体。

三、goto语句实现循环

首先说明,这里只是兴趣扩展,简单介绍一下goto语句以及使用goto语句实现循环。在实际编程中,不要随意使用goto语句,并且应该避免使用goto语句。具体原因可以点击 这里 阅读一篇博文。

#include<iostream>
using namespace std;
int main()
{
    int i = 1;
	label:             //在这一行作了一个标记,标记名称为label
	cout<<"Hello World"<<endl;
	i++;
	if(i<=5){
		goto label;    //goto意思是前往,语句的作用是跳转到label标记处
	} 
    return 0;
} 

四、循环控制变量的命名

仔细分析前面例题参考代码,循环控制变量习惯使用 \(i,j,k\) 这些变量。这只是惯例,就像符号常量一般全大写一样,遵循这样的惯例可以一定程度增强程序的可读性。

循环结构例题(一)

tangyj626阅读(1601)

一、累加求和

1.计算\(1+2+3+...+100\)(也就是\(\sum\limits_{i=1}^{100}i\))

前面章节已经具体分析过并给出了参考代码:

#include<iostream>
using namespace std;
int main()
{
	int i,s = 0;
	for(i=1;i<=100;i++){
		s += i;    //第i次循环:s累加上i
	}
	cout<<s; 
    return 0;
} 

注意上面程序循环体中的注释,我们以后再描述循环体中语句作用的时候,就会像 第i次循环:s累加上i 这样抽象地概括语句的作用,而不再是从细节上去说:第1次循环,s累加上1;第2次循环,s累加上2;...这样可以帮助我们从整体结构去构思程序的组成部分,然后再进一步细化程序各部分。

2.计算\(1\)-\(\frac{1}{2}\)+\(\frac{1}{3}\)-\(\frac{1}{4}\)+...+\(\frac{1}{99}\)-\(\frac{1}{100}\)

思路一: \( 原式 = (1+ \frac{1}{3} + \frac{1}{5} +...+ \frac{1}{99} )-( \frac{1}{2}+ \frac{1}{4}+...+ \frac{1}{100} ) \) ,可以使用累加器来计算两个累加和,然后做减法即可。

#include<iostream>
using namespace std;
int main()
{
	double s1 = 0,s2 = 0;
	int i;
	for(i=1;i<=99;i+=2){
		s1 += 1.0/i;
	}
	for(i=2;i<=100;i+=2){
		s2 += 1.0/i;
	}
	cout<<s1-s2<<endl;    
	return 0;
}
#include<iostream>
using namespace std;
int main()
{
	double s1 = 0,s2 = 0;
	int i;
	for(i=1;i<=99;i+=2){
		s1 += 1.0/i;
		s2 += 1.0/(i+1); 
	}
	cout<<s1-s2<<endl;    
	return 0;
}

思路二:仍然使用累加器实现,第i次累加使用的是1.0/i,不过奇数项是累加1.0/i,偶数项是累加-1.0/i

#include<iostream>
using namespace std;
int main()
{
	int i;
	double s = 0;
	for(i=1;i<=100;i++){
		if(i%2==0)
			s += -1.0/i;
		else
			s += 1.0/i;
	}
	cout<<s; 
    return 0;
} 
#include<iostream>
using namespace std;
int main()
{
	double s = 0;
	//控制变量i的定义和赋初值都在for循环中
	for(int i=1;i<=100;i++){
		if(i%2==0)
			s += -1.0/i; //s -= 1.0/i;
		else
			s += 1.0/i;
	}
	cout<<s; 
    return 0;
} 

上面的程序在循环体中出现了if语句,程序三大结构的使用是非常灵活的,可以相互嵌套。第二段程序将控制变量i的定义和赋初值都在for循环中,这时只能在循环中使用i,在for循环后的代码不能使用i(否则会出现编译错误)。

思路二:使用一个变量sign来记录第i项累加内容的符号,循环开始前sign = 1;在循环中使用sign *= -1;来实现sign在1和-1之间不断切换;累加的值就是 1.0*sign/i;

#include<iostream>
using namespace std;
int main()
{
	int i,sign = 1;
	double s = 0;
	for(i=1;i<=100;i++){
		s += 1.0*sign/i;
		sign *= -1;
	}
	cout<<s; 
    return 0;
}

这里使用赋初值int sign = 1;和多次执行sign *= -1;实现sign在1和-1之间不断切换。如果赋初值int f = 0;和多次执行f = 1-f;可以实现f在0和1之间不断切换。

3.计算\(\frac{1}{0.001}\)+\(\frac{1}{0.002}\)+\(\frac{1}{0.003}\)+...+\(\frac{1}{1.0}\)

初学者可能不假思索,直接使用double类型控制变量(后面会验证这里使用double类型控制变量是不可靠的):

#include<iostream>
using namespace std;
int main()
{
    //本程序使用double类型控制变量,计算结果与预期不一致
    double s = 0;
    for(double i=0.001;i<=1;i+=0.001){
        s += 1/i;
    }
	cout<<s<<endl;
    return 0;
}

程序输出结果:7484.47。是否正确呢?我们通过程序来验证。

可知原式=\(\frac{1000}{1}\)+\(\frac{1000}{2}\)+\(\frac{1000}{3}\)+...+\(\frac{1000}{999}\)+\(\frac{1000}{1000}\),这样可以使用int类型控制变量:

#include<iostream>
using namespace std;
int main()
{
    double s = 0;
    s = 0;
    for(int i=1;i<=1000;i++){
    	s += 1000.0/i;
	}
	cout<<s<<endl;
    return 0;
} 

程序输出结果:7485.47。和第一种解法结果不一致!我们修改第一段程序:

#include<iostream>
using namespace std;
int main()
{
    double s = 0;
    for(double i=0.001;i<=1;i+=0.001){
        s += 1/i;
        cout<<i<<endl;
    }
	cout<<s<<endl;
    return 0;
}

程序输出内容较多,这里给出后5行的输出:

0.996
0.997
0.998
0.999
7484.47

经过验证发现,循环累加并没有执行 s += 1/1.0;导致结果比预期少1。

特别注意的是,循环的控制变量尽量使用整数类型,使用浮点数类型的控制变量不可靠!我们来运行下面两段程序,会发现运行结果又相同(都是97876.1),用浮点类型的控制变量确实不可靠呀:

#include<iostream>
using namespace std;
int main()
{
    double s = 0;
    for(double i=0.0001;i<=1;i+=0.0001){
        s += 1/i;
    }
	cout<<s<<endl;
    return 0;
}
#include<iostream>
using namespace std;
int main()
{
    double s = 0;
    for(int i=1;i<=10000;i+=1){
        s += 10000.0/i;
    }
	cout<<s<<endl;
    return 0;
}

4.计算\(1\)+\(\frac{1}{2}\)+\(\frac{1}{3}\)+\(\frac{1}{4}\)+...问加到第几项累加和恰好超过5?

分析:仍然使用累加器求和,不过循环的条件不再是前面那样累加到多少项了,而是累加和\(s \le 5\):

#include<iostream>
using namespace std;
int main()
{
	int i;
	double s = 0;
	for(i=1;s<=5;i++){
		s += 1.0/i;
	}
	cout<<i; 
    return 0;
} 

程序运行结果输出84,其实应该是加到83项,累加和恰好超过5(5.00207),程序最后应该输出i-1而不是i。我们通过程序来加以验证(注意这里通过编写额外的程序来验证解决问题的程序是否正确的做法):

#include<iostream>
using namespace std;
int main()
{
	int i,n;
	cin>>n;
	double s = 0;
	for(i=1;i<=n;i++){
		s += 1.0/i;
	}
	cout<<s<<endl;	
	return 0;
}

输入84,输出5.01397;

输入83,输出5.00207;

输入82,输出4.99002。

通过程序验证,加到第i-1项(83项),累加和恰好超过5。

我们再来分析为什么程序应该输出i-1而不是i。for循环的条件是s<=5,那么出了循环后肯定有s>5,正是在循环里最后的一次累加导致s恰好超过了5,然后再次判断循环条件不成立退出了循环。但是要注意for循环的执行流程,每次执行了循环体(累加)后,还会自动执行i++,这就导致了本来最后一次循环累加的内容1.0/i是1.0/83,但累加后执行i++,i变成了84。也就是退出循环后累加项比i值少1,所以输出的结果应该是i-1:

#include<iostream>
using namespace std;
int main()
{
    int i;
    double s = 0;
    for(i=1;s<=5;i++){
        s += 1.0/i;
    }
    cout<<i-1; 
    return 0;
} 
#include<iostream>
using namespace std;
int main()
{
    double s = 0;
    //循环控制变量i在循环体中定义 
    for(int i=1;s<=5;i++){
        s += 1.0/i;
    }
    //出了循环后不能使用i,会出现编译错误 
    cout<<i-1; 
    return 0;
} 

5.给定n和k,计算1~n范围内的所有正整数能被k整除的数(第一类数)的和以及不能被k整除的数(第二类数)的和。

思路一:用两个累加器变量记录两类数的和,借助循环结构枚举处理1~n范围内的每个正整数i,在循环体中使用if语句来判断i能否被k整除进而将i加到对应累加器变量上。

#include<iostream>
using namespace std;
int main()
{
    int n,k,s1,s2;
	s1 = s2 = 0;
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		if(i%k==0) s1 += i;
		else s2 += i;
	}
	cout<<s1<<" "<<s2;
    return 0;
}

思路二:借助循环结构可以枚举出1~n范围内的每个能被k整除正整数i,使用累加器求和,而1~n范围内的所有正整数的和可以使用等差数列求和公式计算,后者与前者做减法可以得出第二类数的和。

#include<iostream>
using namespace std;
int main()
{
    int n,k,s,s1=0;
	cin>>n>>k;
	//枚举1~n范围内的每个能被k整除正整数i(1倍k、2倍k、3倍k、...)
	for(int i=k;i<=n;i+=k){
		s1 += i;
	}
	//利用等差数列求和公式求所有数的和(高中数学知识)
	s = (1+n)*n/2;
	cout<<s1<<" "<<s-s1;
    return 0;
} 

可知第2种算法比起第1种算法,减少了循环执行的次数,执行效率优于第1种算法。

6.计算输入的10个整数的和。

分析:使用累加器求和。不过要输入10个整数,高效的方法是将cin语句置于重复执行10次的循环体中。

#include<iostream>
using namespace std;
int main()
{
    int n,s = 0;
    for(int i=1;i<=10;i++){
    	//每次都将输入的数据存放到变量n中,前面输入到n的值会被覆盖掉 
		//这也没有影响,因为变量n累加到s上后,n的值就没有用了,可以丢弃 
    	cin>>n;
    	s += n;
	}
	cout<<s;
    return 0;
}

需要特别注意的是,如果循环中同时存在输入输出的话,在命令下运行时会出现输入与输出“混杂”在一起的情况。但是在OJ测评和竞赛测评时,会自动将输入输出分开,输入在输入流中,输出在输出流中,测评时输入输出不会“混杂”在一起。所以可以一边输入一边处理输出的问题,可以放心在循环中同时处理输入输出。

二、求最值

问题描述:求给定的n个整数的最小值

输入格式:输入内容有n+1行,第1行是一个正整数n,后面的n行每一行是一个整数

输出格式:一行一个整数,就是输入的n个整数的最小值

分析:通过打擂台的方式求极值,不过数据的输入是需要首先解决的问题。程序先输入要计算最小值的整数的个数n,然后要输入n个整数,我们可以将输入语句cin放在重复执行n次的循环体中。

#include<iostream>
using namespace std;
int main()
{
    int n,m,min;
    cin>>n;
    for(int i=1;i<=n;i++){
    	//每次输入的整数都放在变量m中,会自动覆盖掉上次输入的数(也保存在m中) 
		//因为每次处理完输入的整数后,这个数后面不会再用到,可以丢弃 
    	cin>>m;
    	
    	//第一个数 或者 m比最小值小 
    	if(i==1 || m<min) min = m;
	}
	cout<<min;
    return 0;
} 

大家要关注并掌握这样的输入数据的方式,在竞赛中常用到。

也可以将第一次输入放在循环外面作为特殊处理:

#include<iostream>
using namespace std;
int main()
{
    int n,m,min;
    cin>>n>>min;    //第1个数直接输入到min中
    for(int i=2;i<=n;i++){      //输入并处理第2~n个数
    	cin>>m;
    	if(m<min) min = m;
	}
	cout<<min;
    return 0;
} 

三、最大公约数

问题:求两个正整数m和n的最大公约数

思路一:判断从1~m(或者从1~n,其实最好是1~\(min(m,n)\))的每个数i,如果i能够同时整除m和n,那么i就是m、n的公约数,最后的一个公约数就是最大公约数。

#include<iostream>
using namespace std;
int main()
{
    int m,n,ans;
    cin>>m>>n;
    for(int i=1;i<=min(m,n);i++){
    	//i是m和n的公约数,更新保存公约数的变量ans为i
    	if(m%i==0 && n%i==0) ans = i;
	}
    //循环是从小到大查找公约数的,所以循环结束后最大公约数就保存在ans中
	cout<<ans;
    return 0;
} 

思路二:在\(min(m,n)\)~1范围内从大到小查找m和n的公约数,找到的第一个公约数就是最大公约数,找到后可以使用break;语句强制跳出循环。

#include<iostream>
using namespace std;
int main()
{
    int m,n,i;
    cin>>m>>n;
    for(i=min(m,n);i>=1;i--){ 
    	//找到第一个公约数就是最大公约数,使用break;语句强制退出循环
    	//break;的作用是强制从该点退出到循环外
   	 //如果是for循环甚至不会执行第③部分控制变量变化语句直接强制退出循环
    	if(m%i==0 && n%i==0) break;
	}
	cout<<i;
    return 0;
} 

思路三:相减法求最大公约数,算法描述如下:

  1. 输入\(m\)和\(n\)
  2. 如果\(m=n\),那么\(m\)(或\(n\))就是最大公约数,输出\(m\)(或\(n\)),算法结束
  3. 如果\(m>n\),执行\(m ← m - n\)
  4. 如果\(m<n\),执行\(n ← n - m\)
  5. 返回第2步

其实就是当\(m!=n\)时,执行如下操作:如果\(m>n\),执行\(m = m - n\);否则执行\(n = n - m\)。

#include<iostream>
using namespace std;
int main()
{
    int m,n;
    cin>>m>>n;
    while(m!=n){
    	if(m>n) m -= n;
    	else n -= m;
	}
	cout<<m;
    return 0;
} 

思路四:辗转相除法(欧几里得算法)求最大公约数,算法描述如下:

  1. 输入\(m\)和\(n\)
  2. \(r←m\%n\),若\( r==0\),输出\(n\),算法结束。
  3. 互换:执行赋值\(m←n,n←r\),并返回第2步。
#include<iostream>
using namespace std;
int main()
{
    int m,n,r;
    cin>>m>>n;
    r = m%n;
    while(r!=0){
    	m = n;
    	n = r;
    	r = m%n;
	}
	cout<<n;
    return 0;
} 

当然也可以用do...while循环或者for循环实现:

#include<iostream>
using namespace std;
int main()
{
    int m,n,r;
    cin>>m>>n;    
    do{
    	r = m%n;
    	m = n;
    	n = r;
	}while(r!=0);
	cout<<m;   //注意这里是输出m 
    return 0;
} 
#include<iostream>
using namespace std;
int main()
{
    int m,n,r;
    cin>>m>>n;    
    for(r=m%n;r!=0;r=m%n){
    	m = n;
    	n = r;
	}
	cout<<n;
    return 0;
} 

关于“欧几里得算法”详细内容,可以查阅 百度百科相关词条。如果要计算最小公倍数,可以利用数学公理:两数之乘积=两数最大公倍数×两数最小公倍数。

四、寻找公倍数

问题:给定m和n,找出1~100000能被m和n整除的数

分析:通过循环一一枚举1~100000范围内的每个整数i,如果i能够同时被m和n整除,那么i符合条件输出i。这里是通过循环穷举每一个可能的数字,在循环体中用if语句来“筛选”出符合条件的数字。大家注意体会并掌握这样的程序结构。

#include<iostream>
using namespace std;
int main()
{
	int m,n;
	cin>>m>>n;
	//通过循环枚举1~100000范围内的每个整数
	for(int i=1;i<=100000;i++){
		//循环体中使用if语句筛选出符合条件的情况
		if(i%m==0 && i%n==0){
			cout<<i<<" ";
		}
	}
    return 0;
}

考虑到能够被m和n整除的数就是m和n的公倍数,也就是m和n的最小公倍数的1倍、2倍、3倍……这些数,那么先求出最小公倍数,再输出最小公倍数的1倍、2倍、3倍……这些数,比较前面的算法,这样的算法减少了循环的次数,提高了执行效率:

#include<iostream>
using namespace std;
int main()
{
	int m,n,r,p,q;
	cin>>m>>n;
	
	p = m*n;	//p是m、n的积
	
	//辗转相除法求最大公约数 
	for(r=m%n;r!=0;r=m%n){
        m = n;
        n = r;
    }
    
    //最大公约数是n,求最小公倍数 
    q = p/n;
    
    //输出最小公倍数的1倍、2倍、3倍...这些数 
	for(int i=q;i<=100000;i+=q){
		cout<<i<<" ";
	}
    return 0;
}

输出的数据可能很多,为了美观,每10个数字一行,每个数字占6位。可以使用计数器tot来记录找到的数的数量,每10个数也就是tot%10==0,此时输出换行:

#include<cstdio>
int main()
{
	int m,n,r,p,q;
	scanf("%d%d",&m,&n);
	
	p = m*n;	//p是m、n的积
	
	//辗转相除法求最大公约数 
	for(r=m%n;r!=0;r=m%n){
        m = n;
        n = r;
    }
    
    //最大公约数是n,求最小公倍数 
    q = p/n;
    
    int tot=0;
    //输出最小公倍数的1倍、2倍、3倍...这些数 
	for(int i=q;i<=100000;i+=q){
		printf("%6d ",i);
		tot++;
		if(tot%10==0) printf("\n");
		//上面一句也可以写成:
		//if(tot==10) tot=0,printf("\n");
	}
    return 0;
}

五、逆序数

问题:计算非负整数n的逆序数。例如123的逆序数是321,1200的逆序数是21。

分析:前面顺序结构我们计算过五位数的逆序数,使用的是/和%运算,使用整除10的某次方将指定位变到结果的个位数,再%10求出某位上的数字。现在输入的正整数n的位数不定,那应该如何处理呢?我们以一个数36495为例:

n%10取出了n个位上的数,n=n/10相当于把个位直接“砍掉”,通过这两个操作的重复执行,能够将整数n每位上的数字取出来。在拆分取数的同时,右边m是在“组装”逆序数,m初值为0,每次执行m=m*10+n%10;作用是将m扩大10倍然后将取出来的n个位上的数放到m个位数,通过这样的操作就能将逆序数“组装”到m中:

#include<iostream>
using namespace std;
int main()
{
	int n,m=0;
	cin>>n;
	while(n!=0){
		m = m*10+n%10;
		n /= 10;
	}
	cout<<m;
    return 0;
}

其实也可以不“组装”逆序后的整数,直接输出每次“拆”出来的个位部分,不过得想办法确保“高位”多余的0不输出。经过分析可知,只有“当前拆出来的个位数是0”的时候才考虑可能不用输出;但是并不是所有拆出来的0都不输出,如果之前拆出来过非0的数,那么当前拆出来的0也要输出。综上分析可知,只有“当前拆出来的数是0并且之前没有拆出来非0的数”的情况下不输出,其它情况都要输出拆出来的数:

#include<iostream>
using namespace std;
int main()
{
	//注意标记变量flag的使用 
	int n,flag = 0;
	cin>>n;
	//对于0,特判 
	if(n==0){
		cout<<0<<endl;
		return 0;    //提前结束程序 
	}
	while(n>0){
		//标记拆分过程中出现了非0的数 
		if(n%10 != 0) flag = 1;
		//注意这里条件的描述:只有“当前拆出来的数是0并且之前没有拆出来非0的数”的情况下不输出,其它情况都要输出拆出来的数
		//仔细琢磨,其实只要前面拆出来了非0的数,接下来拆出来的数都要输出,所以条件可以直接用 flag==1
		if(!(n%10==0 && flag==0))
			cout<<n%10;
		n /= 10;
	}    
	return 0;
}

巧妙解法:低位上的数会被先拆分出来,那么可以先把低位上连续的0全部拆出来,剩余的数正常拆分就行:

#include<iostream>
using namespace std;
int main()
{
    int n;
    cin>>n;

    //对于0,特判 
    if(n==0){
        cout<<0<<endl;
        return 0;    //提前结束程序 
    }

    //先把低位上连续的0全部拆出来:拆出来的个位数是0的时候,一直“切掉”个位上的0
    while(n!=0 && n%10==0) n /= 10;

    //拆分剩余的
    while(n!=0){
        cout<<n%10;
        n /= 10;
    }
    return 0;
}

六、判断素数

问题:判断正整数n是否为素数。素数就是质数,也就是只能被1和自身整除的整数。

思路一:利用计数器统计正整数n的约数的数量,根据数量来判断。

#include<iostream>
using namespace std;
int main()
{
    int n,tot=0;
	cin>>n;
	if(n==1){       //特判1不是素数 
		cout<<"NO";
		return 0;   //main中运行到return语句整个程序结束 
	}
	//统计除1和自身外,n约数的数量 
	for(int i=2;i<n;i++){
		if(n%i==0) tot++;
	} 
	if(tot==0) cout<<"YES";
	else cout<<"NO";
    return 0;
} 

思路二:在2~n-1范围内找到任意一个n的约数,那么n就不是素数,可以使用break语句强制退出循环。

#include<iostream>
using namespace std;
int main()
{
	//变量find记录是否找到除1和自身外的约数
	//开始赋值为0,表示没有找到 
    int n,find = 0;
	cin>>n;
	if(n==1){       //特判1不是素数 
		cout<<"NO";
		return 0;   //main中运行到return语句整个程序结束 
	}
 
	for(int i=2;i<n;i++){
		if(n%i==0) {
			find = 1;  //赋值为1,表示找到 
			break;
		}
	}
	
	//判断find的值是否仍然为0(如果为0说明循环体中的if语句没有进去过) 
	if(find==0) cout<<"YES";
	else cout<<"NO";
    return 0;
} 

程序使用了一个特殊的变量find来标记是否找到了除1和自身外的其它约数,并人为约定find值为0表示未找到,值为1表示找到。那么在开始查找前,还没有找到,所以find赋初值0,在循环体中如果找到了一个约数,那么find赋值为1。出循环后只需要判断find的值是否仍然为0(0表示未找到),就能判断n是否为素数。像find这样的变量称为标记变量,用来标记一些特殊的状态(本例中用两个值0和1标记了未找到和找到两种状态)。标记变量的使用首先要人为约定特殊值表示的状态,并且在后面的操作中满足约定。

思路三:其实可以进一步缩小查找约数的范围,只需要在2~\(\sqrt n\)查找n的约数即可(根据数学知识可知,如果在2~\(\sqrt n\)找到一个i能够整除n,那么在\(\sqrt n\)~n范围内肯定还有一个整数n/i也能整除n;反之如果在2~\(\sqrt n\)找不到任何一个整数能够整除n,那么在\(\sqrt n\)~n范围内肯定也没有任何整数能整除n)。

#include<iostream>
using namespace std;
int main()
{
	//变量find记录是否找到除1和自身外的约数
	//开始赋值为0,表示没有找到 
    int n,find = 0;
	cin>>n;
	if(n==1){       //特判1不是素数 
		cout<<"NO";
		return 0;   //main中运行到return语句整个程序结束 
	}
 
	for(int i=2;i*i<=n;i++){     //使用i*i<=n巧妙地表示了i<=sqrt(n)这个循环条件
		if(n%i==0) {
			find = 1;  //赋值为1,表示找到 
			break;
		}
	}
	
	//判断find的值是否被修改成了1 
	if(find==0) cout<<"YES";
	else cout<<"NO";
    return 0;
} 

循环语句正常结束后循环的条件肯定不成立,而break强制退出循环后循环的条件仍然成立,根据这两点,不使用标记变量也能解决问题:

#include<iostream>
using namespace std;
int main()
{
    int n;
	cin>>n;
	if(n==1){       //特判1不是素数 
		cout<<"NO";
		return 0;   //main中运行到return语句整个程序结束 
	}
	
	int i;
	for(i=2;i*i<=n;i++){ //因为后面还要使用i值,这里不能用for(int i=2;i*i<=n;i++)
		if(n%i==0) break;
	}
	
	//如果前面循环正常结束,循环条件不成立,肯定有i*i>n
	//如果循环体中的break被执行,循环被强制结束,出了循环后循环条件依然成立
	//这里判断前面的循环条件不成立,那么break未被执行,也就是没有找到1和自身外的其他约数 
	if(i*i>n) cout<<"YES";
	else cout<<"NO";
    return 0;
} 

还可以借助return 0;语句来处理:

#include<iostream>
using namespace std;
int main()
{
    int n;
    cin>>n;
    if(n==1){       //特判1不是素数 
        cout<<"NO";
        return 0;   //main中运行到return语句整个程序结束 
    }
    
    for(int i=2;i*i<=n;i++){
        if(n%i==0){
        	cout<<"NO";
        	return 0;	//main中运行到return语句整个程序结束 
        }
    }
    
    //如果能执行到这里,那么意味着前面循环中没有找到任何约数i
	//也就意味着n是素数 
    cout<<"YES";
    return 0;
} 

七、分解质因数

问题:输入一个正合数n(合数就是除了1和本身外还有其它约数的整数),输出其分解质因数的结果。例如2520,分解质因数的结果是:\(2 \times 2 \times 2 \times 3 \times 3 \times 5 \times 7\)

分析:模拟数学中的短除法分解质因数。i初始值为2,判断n能否被i整除:如果能那么i就是n分解质因数的一个因子,n /= i,(这里不执行i++,保证n能够被i彻底分解),然后继续判断n能否被i整除;如果不能被整除,那么i++,然后n能否被i整除。直到n==1为止:

nin%i==0?输出执行n/=i?
n值
执行i++?
i值
25202Y21260N
12602Y2630N
6302Y2315N
3152NNN3
3153Y3105N
1053Y335N
353NNN4
354NNN5
355Y57N
75NNN6
76NNN7
77Y71N

从表格描述的过程分析可知,像4这样的约数不会出现在分解质因数结果中,因为肯定在前面会被分解成 \(2 \times 2\)。

#include<iostream>
using namespace std;
int main()
{
    int n,i=2;
    cin>>n;
    while(n!=1){
    	if(n%i==0){
			n /= i;
			cout<<i<<" ";
		}
    	else i++;
	}
    return 0;
} 

运行程序,输入2520,输出结果如下:

2 2 2 3 3 5 7

如果要输出乘号,如何修改程序呢?如果直接用cout<<i<<"×";可以猜想到肯定会多输出一个乘号。因为不是找到一个i就要输出i和乘号,找到的最后一个i不能输出乘号,所以这里要使用if语句分情况讨论。那么怎么描述是不是最后一个质因子呢?试试判断n与1的关系?

学习和循环语句的嵌套后,我们还会用其他方法来分解质因数。

八、break和continue

循环中break语句的作用是强制退出循环,要注意for循环中执行break后程序直接跳转到for循环后面的语句,不会执行for循环的第③部分修改程序控制变量的语句。

循环正常退出后,循环的条件肯定不再成立了;如果循环是被break强制退出的,出了循环后循环的条件依然成立。

除了break语句能够影响循环的执行流程,还有continue语句也能改变循环的执行流程,与break不同的是,continue只是不再执行本次循环体中continue后面的语句,直接进入下一次循环(如果是for循环会执行第③部分修改程序控制变量的语句)。一般情况下,break和continue都位于循环体的if语句中。

通过一个简单的例子来说明continue语句的使用,计算1~100范围内所有偶数的和,用continue语句来实现:

#include<iostream>
using namespace std;
int main()
{
    int s = 0;
	for(int i=1;i<=100;i++){
		//发现i是奇数,continue不作处理直接进入下次循环 
		if(i%2==1) continue;
		s += i;
	} 
	cout<<s;
    return 0;
} 

需要注意的是,这里只是演示需要使用了continue来解决问题,针对本问题更简洁的程序如下:

#include<iostream>
using namespace std;
int main()
{
    int s = 0;
	for(int i=2;i<=100;i+=2){
		s += i;
	} 
	cout<<s;
    return 0;
} 

循环的嵌套

tangyj626阅读(1082)

正如选择结构if语句的if子句和else子句可以嵌套if语句一样,循环语句的循环体中也能出现循环语句,这就是循环的嵌套,也称为多重循环。就以钟表的分针和秒针为例,分针每走一格,秒针都要走完整一圈。分针走动多格可以用循环来描述,这是“大操作”,“大操作”的每一步(分针走一格)都是重复的小操作(秒针走一格重复60次实现的分针走一圈效果),这就是典型的循环嵌套(双重循环)。如果再考虑上时针,那么就是三重循环。

一、结构化程序设计

我们解决的问题会越来越复杂,写出的程序结构同样如此。随着我们对算法和程序设计越来越熟悉,分析问题、理清思路后,往往会预先结合程序的框架来描述设计的算法,这里体现的是结构化程序设计的思想。

结构化程序设计(structured programming)是进行以模块功能和处理过程设计为主的详细设计的基本原则。结构化程序设计采用自顶向下、逐步求精的设计方法,各个模块通过“顺序、选择、循环”的控制结构进行连接,并且只有一个入口、一个出口。

以前面的累加求和(\(1+2+3+...+n\))为例,结构化程序设计首先是自顶向下搭建以功能模块为基本单位的框架:

  1. 输入n
  2. 利用循环结构和累加器求和
  3. 输出结果

然后再逐步求精,细化各个功能模块,完善整个程序:

#include<iostream>
using namespace std;
int main()
{
    int n,s=0;
    cin>>n;
    for(int i=1;i<=n;i++){
    	s += i;
	}
	cout<<s;
    return 0;
} 

只要我们分析设计出程序的框架(描述出要实现的各个功能模块),接下来要做的就是将框架中的各个功能模块逐步细化实现。在循环结构特别是循环嵌套的时候,往往要用循环的控制变量来抽象化地描述程序的框架。这里通过几个经典的问题,在说明循环嵌套如何使用的同时,阐述如何用循环的控制变量来抽象化地描述程序的框架的解题思想。

二、双重循环实现图形打印

首先来看一个简单的问题,输出3行2列由字符'*'拼成的矩形,效果如下:

**
**
**

一共要输出3行,可以用一个重复执行3次的循环实现,并且每行输出的内容都相同cout<<"**"<<endl;,初步实现的代码如右侧所示:

#include<iostream>
using namespace std;
int main()
{
	//重复3次,一共输出3行 
    for(int i=1;i<=3;i++){
    	//每次都输出"**"并换行 
    	cout<<"**"<<endl;
	}
    return 0;
}

考虑到循环体中的cout<<"**"<<endl;语句,其实也可以用一个重复执行2次,每次输出一个"*"的循环语句来实现,可以将程序如下修改:

#include<iostream>
using namespace std;
int main()
{
	//重复3次,一共输出3行 
    for(int i=1;i<=3;i++){
    	//每行都输出2个"*" 
    	for(int j=1;j<=2;j++){
    		cout<<"*";
		}
		//再换行 
		cout<<endl;
	}
    return 0;
}

仔细观察程序的结构,会发现for循环的循环体中又出现了for循环,这就是循环的嵌套,这里是双重循环。外层控制输出3行的for循环称为外层循环,内层控制每行输出2个*的for循环称为内层循环。

在循环嵌套结构中,内层循环控制变量一般与外层循环不同,否则可能会干扰外层循环的执行流程。

关于字符图形打印可以得出结论:外层循环控制图形行数,内层循环控制每行打印的内容(每行字符个数)。

我们先从细节入手来分析双重循环的执行流程,帮助我们进一步熟悉掌握循环结构的执行流程。我们现在应该得具备将流程图和程序互相转换的能力,也就是看到三大结构的程序,我们能够很快想到执行流程图;有了流程图,我们能够快速写出对应的三大结构程序代码。

图中绿色部分是内层循环,我们仍然可以借助表格来追踪控制变量值的变化情况来分析执行细节:

i值i<=3?j值j<=2?执行循环体循环结束后
1Y进入内层循环
1Y输出*j++,j为2
2Y输出*j++,j为3
3N退出内层循环
输出换行i++,i为2
2Y进入内层循环
1Y输出*j++,j为2
2Y输出*j++,j为3
3N退出内层循环
输出换行i++,i为3
3Y进入内层循环
1Y输出*j++,j为2
2Y输出*j++,j为3
3N退出内层循环
输出换行i++,i为4
4N退出外层循环

再从结构化程序设计的思想来分析程序,相比较前面的借助表格微观分析执行流程的细节,这里就是宏观描述程序的框架,程序的框架如下:

//重复3次,一共输出3行 
for(int i=1;i<=3;i++){
    //输出2个"*" 
    //换行
}

编写复杂程序时,我们往往通过算法最先联想到的是程序的框架,甚至先书写框架再逐步细化实现每个模块功能

再来看复杂点的字符图形输出,要输出如下边长为5的等腰直角字符三角形:

*
**
***
****
*****

容易想到,这里仍然可以使用双重循环实现,外层循环控制行数,所以外层循环可以用一个重复执行5次的for循环。内层循环控制每行打印的字符数量,但是每行的字符数量不同,但存在一定的规律。

行编号行内字符数量
11
22
33
44
55

通过左边表格分析寻找规律,可知行内打印字符的数量和行编号一致,可以更为抽象地描述为:第i行输出i个字符。那么程序的框架可以描述为:

//重复5次,一共输出5行 
for(int i=1;i<=5;i++){
    //循环体内语句的功能可以描述为:输出第i行的内容
    //第①步:输出i个"*" 
    //第②步:换行
}

注意体会并学会这样的抽象描述循环体功能并进一步描述整个程序框架的方法。往往会借助循环的控制变量来抽象描述,就像这里的“循环体的功能是输出第i行的内容”、“第①步是要输出i个*”。有了这样描述的程序框架,完整的程序就能很容易写出来了:

#include<iostream>
using namespace std;
int main()
{
	//重复5次,一共输出5行 
	for(int i=1;i<=5;i++){
	    //输出i个"*"(循环体只有一句代码,省略了{}并写到了一行中)
	    for(int j=1;j<=i;j++) cout<<"*";
	    //换行
	    cout<<endl;
	}
    return 0;
}

再来看一个更复杂的字符图形输出,要输出如下边长为5的直角在右下角的等腰直角字符三角形:

    *
   **
  ***
 ****
*****

通过分析可知,仍然是5行,仍然是第i行i个*,不过内容“居右对齐”了,其实可以通过在输出i个*之前输出若干个空格将*挤到右侧。还是来分析寻找每行空格数和字符数的规律:

行编号行内空格数行内字符数量
141
232
323
414
505

通过左边表格分析寻找规律,可知第i行先输出5-i个空格,再输出i个字符。那么程序的框架可以描述为:

//重复5次,一共输出5行 
for(int i=1;i<=5;i++){
    //循环体内语句的功能可以描述为:输出第i行的内容
    //第①步:输出5-i个" " 
    //第②步:输出i个"*" 
    //第③步:换行
}

有了程序框架,完整的程序很容易写出来:

#include<iostream>
using namespace std;
int main()
{
	//重复5次,一共输出5行 
	for(int i=1;i<=5;i++){
		//输出5-i个" "
		for(int j=1;j<=5-i;j++){
	    	cout<<" ";
		}  
	    //输出i个"*" 
	    for(int k=1;k<=i;k++){
	    	cout<<"*";
		} 
	    //换行
	    cout<<endl;
	}
    return 0;
}
#include<iostream>
using namespace std;
int main()
{
    //重复5次,一共输出5行 
    for(int i=1;i<=5;i++){
        //输出5-i个" "
        for(int j=1;j<=5-i;j++){
            cout<<" ";
        }  
        //控制变量仍然可以用j 
        for(int j=1;j<=i;j++){
            cout<<"*";
        } 
        //换行
        cout<<endl;
    }
    return 0;
}

会发现我们现在写的程序结构越来越复杂,上面程序循环体中出现了先后顺序的循环。注意第二段程序,即使是将控制变量放在main开始处定义,外层循环体的第二个循环仍然可以用第一个循环相同的控制变量:

#include<iostream>
using namespace std;
int main()
{
	int i,j;    //循环控制变量在循环前定义
	for(i=1;i<=5;i++){
		for(j=1;j<=5-i;j++) cout<<" ";
	    //上面循环结束后,j的值已经没有任何作用了,所以后面的循环仍然用j作为控制变量
	    //并且for循环开始前会执行控制变量赋初值,
	    //所以上一个循环结束后的j值会被覆盖,不会影响到后面的循环  
	    for(j=1;j<=i;j++) cout<<"*";
	    cout<<endl;
	}
    return 0;
}

习题:尝试使用双重循环打印以下字符图形:

*****
****
***
**
*
*****
 ****
  ***
   **
    *

    *
   ***
  *****
 *******
*********
*********
 *******
  *****
   ***
    *

三、打印九九乘法表

问题:打印如下九九乘法表

1*1=1
1*2=2  2*2=4
1*3=3  2*3=6  3*3=9
1*4=4  2*4=8  3*4=12 4*4=16
1*5=5  2*5=10 3*5=15 4*5=20 5*5=25
1*6=6  2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
1*7=7  2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
1*8=8  2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64
1*9=9  2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81

分析:其实和前面打印字符三角形一样,这里一共要打印9行,第 \(i\) 行有 \(i\) 个算式,每个算式都是cout<<A<<"*"<<B<<"="<<A*B;这样的形式(可以猜想到A、B可能和循环的控制变量相关),得出程序的框架:

#include<iostream>
using namespace std;
int main()
{
	for(int i=1;i<=9;i++){
		for(int j=1;j<=i;j++){
			//输出第i行第j个算式,形式是cout<<A<<"*"<<B<<"="<<A*B; 
		}
		cout<<endl;
	}
    return 0;
}

接下来,通过找规律来抽象归纳cout<<A<<"*"<<B<<"="<<A*B;中A、B究竟是什么?

先来看B,以第6行为例:

1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36

我们会发现第 \(i\) 行的每个算式乘号*右边的数B就是外层循环控制变量 \(i\) 。

再来看A,仍然以第6行为例:

1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36

第1个算式的A是1,第2个算式的A是2,……,可知行内第 \(j\) 个算式乘号*左边的数A正好是内层循环控制变量 \(j\)。

那么输出的算式应该是:cout<<j<<"*"<<i<<"="<<j*i;,进一步考虑结果是1位数和2位数的不同处理,完整的程序代码如下:

#include<iostream>
using namespace std;
int main()
{
	for(int i=1;i<=9;i++){
		for(int j=1;j<=i;j++){
			cout<<j<<"*"<<i<<"="<<j*i<<" ";
			if(j*i<10) cout<<" ";
		}
		cout<<endl;
	}
    return 0;
}

习题:尝试打印下面风格的九九乘法表

1*1=1
2*1=2  2*2=4
3*1=3  3*2=6  3*3=9
4*1=4  4*2=8  4*3=12 4*4=16
5*1=5  5*2=10 5*3=15 5*4=20 5*5=25
6*1=6  6*2=12 6*3=18 6*4=24 6*5=30 6*6=36
7*1=7  7*2=14 7*3=21 7*4=28 7*5=35 7*6=42 7*7=49
8*1=8  8*2=16 8*3=24 8*4=32 8*5=40 8*6=48 8*7=56 8*8=64
9*1=9  9*2=18 9*3=27 9*4=36 9*5=45 9*6=54 9*7=63 9*8=72 9*9=81
                                                        1*1=01
                                                 1*2=02 2*2=04
                                          1*3=03 2*3=06 3*3=09
                                   1*4=04 2*4=08 3*4=12 4*4=16
                            1*5=05 2*5=10 3*5=15 4*5=20 5*5=25
                     1*6=06 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
              1*7=07 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
       1*8=08 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64
1*9=09 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81

三、百钱买百鸡

问题:公鸡5文钱一只,母鸡3文钱一只,小鸡1文钱三只,用100文钱买100只鸡(每种鸡都得有)。如何购买?

我国古代数学家张丘建在《算经》一书中曾提出过著名的“百钱买百鸡”问题,该问题叙述如下:鸡翁一,值钱五;鸡母一,值钱三;鸡雏三,值钱一;百钱买百鸡,则翁、母、雏各几何?

数学列方程求解,设公鸡数量\(x\)只,母鸡数量\(y\)只,小鸡数量\(z\)只(\(z\)必须是3的倍数),根据题意,可列出方程组:

$$\left\{ \begin{aligned} x+y+z=100 \\ 5x+3y+z/3=100\end{aligned} \right. $$

可知这是一个不定方程,应该有多组解。

分析:来看最简单的暴力求解法,就是一一列举公鸡、母鸡、小鸡数量所有的情况,从中筛选符合条件的组合(下面列举的组合情况只是粗略地考虑):

公鸡数量母鸡数量小鸡数量总数钱数
11353
11684
119115
…………………………
11300302102
12364
12695
…………………………
1100300401201
21364
21695
…………………………
41878100100
…………………………
10013104102
10016107103
…………………………
100100300500300

分析左边的表格,粗略列举了公鸡、母鸡、小鸡数量的组合情况,公鸡数量取值范围1~100,母鸡数量取值范围1~100,小鸡数量取值范围3~300中所有3的倍数(其实三种鸡数量的取值范围都可以再缩小)。

具体组合规则是公鸡1只,母鸡1只,小鸡3~300中所有3的倍数这样有100种组合;然后公鸡1只,母鸡2只,小鸡3~300中所有3的倍数这样也有100种组合……

就像钟表中时针、分针、秒针的走动关系一样,上面的穷举是典型的三重循环:

for(int i=1;i<=100;i++){
	for(int j=1;j<=100;j++){
		for(int k=3;k<=300;k+=3){
			//公鸡i只,母鸡j只,小鸡k只
		}
	}
}

这就是程序的框架,看到最内层循环中的注释,这里也是抽象地用循环控制变量说明了“公鸡i只,母鸡j只,小鸡k只”。

如果使用前面介绍的用表格来追踪循环的控制变量值变化的情况,会发现和左边的表格中的描述是一致的。

进一步需要判断“公鸡i只,母鸡j只,小鸡k只”是否满足“百钱买百鸡”:

#include<iostream>
using namespace std;
int main()
{
	for(int i=1;i<=100;i++){
		for(int j=1;j<=100;j++){
			for(int k=3;k<=300;k+=3){
				//筛选出“百钱买百鸡”的组合
				if(i+j+k==100 && 5*i+3*j+k/3==100){
					cout<<i<<" "<<j<<" "<<k<<endl;
				}
			}
		}
	}
    return 0;
} 

因为一共要买100只鸡,所以只需要穷举出公鸡和母鸡的组合情况,小鸡的数量可以用减法求出来,这样只需要双重循环就能解决问题。比起前面的三重循环,这样做大大减少了循环次数,提高了程序执行效率:

#include<iostream>
using namespace std;
int main()
{
	for(int i=1;i<20;i++){		         //公鸡最多买19只(20只公鸡100文) 
		for(int j=1;j<(100-5*i)/3;j++){	//母鸡数量不超过(100-5*i)/3
			//公鸡i只,母鸡j只 
			int k = 100-i-j;	           //小鸡数量
			//筛选出“小鸡数量必须是3的倍数,并且一共花100文钱”的组合
			if(k%3==0 && 5*i+3*j+k/3==100){
				cout<<i<<" "<<j<<" "<<k<<endl;
			} 
		}
	}
    return 0;
} 

阅读下面解决百钱买百鸡的程序,理清解决问题的思路:

#include<iostream>
using namespace std;
int main()
{
	for(int k=1;k<33;k++){
		for(int i=1;i<(100-3*k)/5;i++){
			int j=100-3*k-i;
			if(k+5*i+3*j==100){
				cout<<i<<" "<<j<<" "<<3*k<<endl;
			}	
		}
	}
	return 0;
}

四、分解质因数

还是来看分解质因数,从\(2\)开始的整数\(i\)判断能否整除\(n\),如果能够整除,那么借助while循环做到一次性“除尽”:

#include<iostream>
using namespace std;
int main()
{
    int n,i=2;
    cin>>n;
    while(n!=1){
    	//i能整除n,那么一次性“除尽” 
        while(n%i==0){
        	cout<<i<<" ";
        	n /= i;
		}
		i++;
    }
    return 0;
} 

还可以把外层的while循环改成for循环来精简程序代码:

#include<iostream>
using namespace std;
int main()
{
    int n,i=2;
    for(cin>>n;n!=1;i++){
    	//i能整除n,那么一次性“除尽” 
        while(n%i==0){
        	cout<<i<<" ";
        	n /= i;
		}
    }
    return 0;
} 

五、强制退出多重循环

通过前面的学习,我们已经知道,在循环体中可以通过break语句强制退出循环。但是在多重循环结构中,break语句只能强制退出当前层级循环,特别是break语句在内层循环体中时,是不能强制退出到外层循环外的。

例如要找到不大于\(n\)的最大合数,下面的程序是有问题的(会输出不大于\(n\)的所有合数):

break语句只能强制退出当前层级循环,将程序流程转移到当前层级循环外

有两种方法来实现效果,第一中方法是使用return 0;语句来强制结束整个程序:

#include<iostream>
using namespace std;
int main()
{
	int n;
	cin>>n;
	for(int i=n;i>=2;i--){
		for(int j=2;j*j<=i;j++){
			if(i%j==0) {
				cout<<i<<endl;
				return 0;
			}
		}
		
	}
    return 0;
}

此外还可以使用标记变量的方法来强制退出多重循环:

这里甚至可以不用break;语句
#include<iostream>
using namespace std;
int main()
{
	int n,flag = 1;
	cin>>n;
	for(int i=n;flag && i>=2;i--){
		for(int j=2;flag && j*j<=i;j++){
			if(i%j==0) {
				cout<<i<<endl;
				flag = 0;
			}
		}
		
	}
    return 0;
}

注意掌握上面这种方法,特别是在退出循环后还要运行其它程序代码的时候(此时就不能简单地用return 0;语句来强制结束整个程序 )。

循环结构例题(二)

tangyj626阅读(957)

一、阶乘累计求和

问题:已知\(n\)的阶乘: \(n! = 1 \times 2 \times 3 \times ... \times n\)(也就是 \(n! = \prod\limits_{i=1}^{n}i\)),输入正整数\(n\),计算 \(1!+2!+3!+...+n!\)

分析:累计求和需要使用重复n次的循环,循环体内需要嵌套一个循环使用累乘结构计算\(i!\),那么程序的框架可以是:

#include<iostream>
using namespace std;
int main()
{
	long long t,s = 0;
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		// 计算 i!保存到t中(待实现)
		s += t; 
	}
	cout<<s;
    return 0;
} 

进一步实现框架中计算 \(i!\) 的功能模块:

#include<iostream>
using namespace std;
int main()
{
	long long t,s = 0;
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		t = 1;
		for(int j=1;j<=i;j++) t *= j;
		s += t; 
	}
	cout<<s;
    return 0;
} 

上面的程序使用了双重循环,可以计算出内层循环语句重复执行的次数(最能体现循环执行效率的指标)是:\(\sum\limits_{i=1}^{n}i = 1+2+3+...+n = \frac{n \times (n+1)}{2}\) 次,其实可以使用简单的循环解决问题:

#include<iostream>
using namespace std;
int main()
{
	long long t = 1,s = 0;
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		t *= i;	//t是i! 
		s += t; 
	}
	cout<<s;
    return 0;
}

上面的程序减少了循环的层次,大大减少了循环重复执行的次数(只需要n次),提高了程序运行效率。

二、水仙花数

问题:如果一个三位数每位上的数字的立方和等于这个三位数本身,那么它就是一个“水仙花数”,编程输出所有的“水仙花数”。

思路一:使用循环枚举所有的三位数(100~999),对于每个三位数\(i\),使用数的拆分的方法拆解出个位数、十位上、百位上的数,筛选出每位上的数字的立方和等于\(i\)的数即可。

#include<iostream>
using namespace std;
int main()
{
	for(int i=100;i<=999;i++){
		int g = i%10;
		int s = i/10%10;
		int b = i/100%10;
		if(g*g*g+s*s*s+b*b*b==i) cout<<i<<" ";
	}
    return 0;
} 

思路二:使用三重循环分别枚举三位数百位、十位、个位上的数字\(b,s,g\),那么三位数\(n\)就是\(b \times 100+s \times 10+g\),筛选出每位上的数字的立方和\(b^3+s^3+g^3\)等于\(n\)的数即可:

#include<iostream>
using namespace std;
int main()
{
	for(int b=1;b<=9;b++){
		for(int s=0;s<=9;s++){
			for(int g=0;g<=9;g++){
				//百位上的数b,十位上的数s,个位上的数g
				int n = b*100+s*10+g;
				if(g*g*g+s*s*s+b*b*b==n) cout<<n<<" ";
			}
		}
	}
    return 0;
} 

这一种解法虽然是三重循环,但是循环执行的总次数和第一种解法是一致的,甚至效率会稍微高一些(计算机内部乘法运算速度快于除法)。所以不能简单地看循环层次来判断程序的执行效率!

习题:如果一个四位数每位上的数字的四次方和等于这个四位数本身,那么它就是一个“四叶草数”,编程输出所有的“四叶草数”。

三、形如aabb的完全平方数

问题:形如aabb的完全平方数指的是四位数前两位数字相同,后两位数字相同,并且这个四位数是一个完全平方数(开平方结果是整数),编写程序输出所有形如aabb的完全平方数。

思路一:使用双重循环分别枚举高两位的数字\(i\)和低两位的数字\(j\),那么四位数就是\(i \times 1000+i \times 100 + j \times 10 + j\),筛选出其中的完全平方数即可,由此可以得出程序的框架:

#include<iostream>
using namespace std;
int main()
{
	for(int i=1;i<=9;i++){		//枚举高两位 
		for(int j=0;j<=9;j++){	//枚举低两位 
			int n = i*1000+i*100+j*10+j;	//iijj形式的四位数n 
			//if(n是完全平方数) cout<<n<<" ";
		}
	}
    return 0;
} 

但是会发现要判断“n是完全平方数”不容易实现,可以换一种思路。

思路二:枚举所有四位完全平方数,筛选出其中形如aabb的数:

#include<iostream>
using namespace std;
int main()
{
	//条件为空,相当于for(int x=32;true;x++),也就是死循环
	for(int x=32; ;x++){
		int n = x*x;
		if(n>=10000) break;			//强制跳出循环 
		int hi = n/100,lo = n%100;	//计算高两位hi,低两位lo
		if(hi/10==hi%10 && lo/10==lo%10) cout<<n<<" "; 
	} 
    return 0;
}

四、P1980 计数问题

分析:使用循环枚举1~n的每个整数i,对于i,使用while循环拆分出它每位上的数字,如果位上数字等于x则计数:

#include<iostream>
using namespace std;
int main()
{
	int n,x,tmp,ans = 0;
	cin>>n>>x;
	for(int i=1;i<=n;i++){
		tmp = i;   //tmp赋值成i
		//拆分tmp,不会修改i值
		while(tmp){
			if(tmp%10==x)
				ans++;
			tmp /= 10;
		}
	}
	cout<<ans; 
    return 0;
} 

下面的程序会出现“死循环”:

#include<iostream>
using namespace std;
int main()
{
	int n,x,ans = 0;
	cin>>n>>x;
	for(int i=1;i<=n;i++){
		//拆分i,导致i值被修改
		while(i){
			if(i%10==x) ans++;
			i /= 10;
		}
		//拆分结束后i值为0
		//导致循环出不来
	}
	cout<<ans; 
    return 0;
}

五、P5723质数口袋

分析:累加器s记录素数和,借助循环处理从2开始的每个整数i,如果i是素数,那么累加到s,累加后如果发现s>L,使用break强制跳出循环。

#include<iostream>
using namespace std;
int main()
{
	int L,s = 0,tot = 0;
	cin>>L;
	for(int i=2;;i++){      //没有写循环条件,则是“死循环”
		//判断i是否为素数
		int find = 0;
		for(int j=2;j*j<=i;j++){
			if(i%j==0){
				find = 1;
				break;
			}
		}
		if(find) continue;     //不是素数continue不执行后面的语句直接进入下次循环
		s += i;
		if(s>L) break;         //口袋装不下了,break强制跳出循环
		cout<<i<<endl;
		tot++;
	}
	cout<<tot;
    return 0;
} 

六、P1217 [USACO1.5]回文质数

思路一:直接使用循环枚举\([a,b]\)范围内的每个整数\(i\),筛选出既是回文数又是质数的i即可,由此可以得出程序的框架:

for(int i=a;i<=b;i++){
    //判断i是回文数
    if(i是回文数){
        //判断i是质数
        if(i是质数){
            //输出i
        }
    }
}

在框架基础上完善各个功能模块,得出程序:

#include<iostream>
using namespace std;
int main()
{
	int a,b;
	cin>>a>>b;
	if(a==1) a++;    //避免a==1时后面代码也会输出1
	//循环枚举[a,b]范围内每个整数i 
	for(int i=a;i<=b;i++){
		//判断i是否是回文数? 
		int n = i,m = 0;   //n记录i值,后面破坏n的值不破坏i值,这样不会影响外层循环
		while(n){          //也就是while(n!=0){
			m = m*10+n%10;
			n /= 10;
		}
		if(m==i){	      //i是回文数,进一步判断i是否是质数? 
			int find = 0;
			for(int j=2;j*j<=i;j++){
				if(i%j==0){
					find = 1;
					break;
				}
			}
			//i也是质数 
			if(find==0) cout<<i<<endl;
		}
	} 
    return 0;
} 

提交代码,会发现最后一个测试点超时:

下载该点的测试数据,会发现该点\(a=5,b=100000000\)(其实题目里数据规模也提到了\(b \le 100,000,000\)),外层循环次数接近1亿次,再加上循环体内部的判断回文和判断质数的循环,实际循环重复执行的次数更多!在1s内无法执行完毕导致超时(TLE)。需要优化程序才能通过该测试点。

思路二:优化的策略就是减少循环执行的次数,前面的方法对于\([a,b]\)范围内的每个整数\(i\)都做了测试,其实这里面有大量的非回文数,我们可以通过构造\([a,b]\)范围内回文数的方式来减少循环的执行次数。

例如要构造5位回文数,其实我们只需要使用三重循环枚举个位、十位、百位上的数字即可,因为千位上的数字和十位上数字相同,万位上的数字和个位数的数字相同:

for(int d1=1;d1<=9;d1+=2)     //只考虑个位上的数是奇数的情况(个位数是偶数肯定不是质数)
	for(int d2=0;d2<=9;d2++)
		 for(int d3=0;d3<=9;d3++){
		 	int n = d1*10000+d2*1000+d3*100+d2*10+d1;
		 }

并且这里只需要构造3位、5位、7位回文数并考查是否在\([a,b]\)范围内且是质数,还需要额外考查1位回文质数(5、7)和2位回文质数(11)是否在\([a,b]\)范围内(题目指出\(5 \le a < b \le 100,000,000\),所以不用考虑2)。不需要考虑偶数位的回文数(11除外)是否为质数,因为通过数学知识可知偶数位的回文数都是11的倍数,除11外都不是质数。

#include<iostream>
using namespace std;
int main()
{
	int a,b;
	cin>>a>>b;
	//特殊考虑5、7、11是否在[a,b]范围内 
	if(a<=5 && b>=5) cout<<5<<endl;
	if(a<=7 && b>=7) cout<<7<<endl;
	if(a<=11 && b>=11) cout<<11<<endl;
	
	//构造3位回文数
	for(int d1=1;d1<=9;d1+=2)//只考虑个位上的数是奇数(个位数是偶数肯定不是质数)
		for(int d2=0;d2<=9;d2++){
			int n = d1*100+d2*10+d1;
			if(n<a) continue;	//n<a,不考虑 
			if(n>b) return 0;	//n>b,直接退出程序
			int find = 0;
			for(int j=2;j*j<=n;j++){
				if(n%j==0){
					find = 1;
					break;
				}
			} 
			if(!find) cout<<n<<endl;    //if(find==0) cout<<n<<endl;
		} 
	
	//构造5位回文数 
	for(int d1=1;d1<=9;d1+=2)
		for(int d2=0;d2<=9;d2++)
			 for(int d3=0;d3<=9;d3++){
			 	int n = d1*10000+d2*1000+d3*100+d2*10+d1;
			 	if(n<a) continue;
				if(n>b) return 0;
				int find = 0;
				for(int j=2;j*j<=n;j++){
					if(n%j==0){
						find = 1;
						break;
					}
				} 
				if(!find) cout<<n<<endl;
			 }
	
	//构造7位回文数 
	for(int d1=1;d1<=9;d1+=2)
		for(int d2=0;d2<=9;d2++)
			 for(int d3=0;d3<=9;d3++)
			 	for(int d4=0;d4<=9;d4++){
				 	int n = d1*1000000+d2*100000+d3*10000+d4*1000+d3*100+d2*10+d1;
				 	if(n<a) continue;
					if(n>b) return 0;
					int find = 0;
					for(int j=2;j*j<=n;j++){
						if(n%j==0){
							find = 1;
							break;
						}
					} 
					if(!find) cout<<n<<endl;
				}			 
    return 0;
} 

思路三:筛选法求质数。在后面章节数组的内容中会详细介绍。

使用循环输入输出多个数据

tangyj626阅读(728)

编写程序解决问题,往往涉及到大量数据的处理,可能要输入批量的数据,也有可能要批量输出大量数据,这个时候往往要借助循环结构来处理。

将输入输出语句放在循环体中,可以很容易实现批量数据的输入输出。这里给出几种竞赛常用的输入输出结构,首先介绍输入输出重定向到文件(从文件读入数据和将结果输出到文件)的方法。

一、竞赛时输入输出重定向到文件

到前面为止,介绍并使用的输入输出方式都是标准输入输出(stdin、stdout),运行程序时会弹出命令行窗口,在命令行窗口中输入数据,运行过程中会在命令行窗口中显示程序输出的结果。包括我们推荐使用的洛谷平台在内,大多数OJ都是使用这样的方式进行程序测评的(只不过测评过程是由测评程序通过调用相应的命令自动完成的)。

但是在程序竞赛中,特别以NOI系列比赛(NOIP、NOI等)为例,要求的是使用文件输入输出——也就是程序要处理的数据从文件读进去,程序运行的结果也输出到文件中保存起来。参加这样的竞赛时,如果没有按照要求使用文件输入输出,即使算法、程序完全正确能够很好地解决问题,也不会获得分数!

首先来看NOIP2017竞赛原题扉页内容:

还有问题描述页:

可以看到图中红色标识出的内容,提出了程序输入文件名和输出文件名要求。CSP J/S、NOIP测评和洛谷不同,需要选手在源程序中通过程序代码从指定文件读取数据(也就是cin/scanf等输入语句输入的数据来自文件,而不是在命令行里输入)并将结果输出到指定文件(也就是cout/printf等输出语句输出到文件中,而不是打印在命令行里)。如果源代码中没有这样的语句,那么测评会出现问题,选手也不能获得分数。

竞赛现场会说明选手目录的位置,进入选手目录,会发现里面按照题目名称已经建立好了子目录,题目子目录下还提供有输入输出样例文件(或者需要按比赛现场要求自行建立选手目录):

↑ 选手目录下按照题目组织的子目录 ↑
↑ 题目子目录中提供的输入输出样例文件 ↑

上面的竞赛真题我们现在解决还有一定的难度,下面通过一个简单的问题,说明NOIP竞赛时的解题过程:

1.新建并保存源代码文件

新建源代码,输入程序框架,然后保存文件。保存文件的时候要保存到选手目录要求的子目录下(本题应该保存到选手目录的min子目录下),并且使用题目要求的文件名保存文件(本题应该保存为min.cpp)

2.新建输入数据文件并在文件中填写好输入样例数据

进入选手目录的题目子目录(本题进入选手目录min子目录),按照题目要求新建一个与题目名称相同的.in文件(本题文件名应该是min.in):其实就是新建一个记事本文本文件,然后重命名文件名即可(注意记事本文件默认后缀是.txt,重命名文件的时候需要去掉.txt修改成.in)。用文本编辑器(Windows下用记事本程序就行)打开新建好的.in文件和提供的测试样例文件(例如本题中的min1.in),将测试样例文件内容拷贝到新建的.in文件并保存。当然也可以拷贝一份提供的样例文件并按题目要求重命名。

3.理清问题、分析问题、设计算法

仔细阅读问题,首先理清输入数据和输出数据的格式和要求(重点阅读“输入格式”和“输出格式”部分内容);然后理清究竟要求解什么问题(有些问题的描述内容很多,需要一步步理清);接下来是分析问题、设计算法,就像求解数学题一样,理清问题后,我们应该从自己接触、掌握的知识和题型库中去寻找要解决问题的模型,根据相关知识和已有算法去设计解决问题的算法。本例就可以使用“打擂台”求极值的算法来计算最小的圆形纸板的半径,然后根据最小半径计算最小面积。

4.根据设计的算法编写程序

注意:这里要在程序里通过程序代码从指定文件读取数据和将结果输出到指定文件中,这样的操作称为输入输出重定向到文件,实现的方法很多,这里介绍一种最简单的(使用freopen函数重定向输入输出):

#include<cstdio>
int main()
{
	//从指定文件(min.in)读取输入数据的最简单写法 
	freopen("min.in","r",stdin);     //固定写法,不同的问题仅第一部分文件名不同
	//将结果输出到指定文件(min.out)的最简单写法 
	freopen("min.out","w",stdout);   //固定写法,不同的问题仅第一部分文件名不同
	
	int n;
	double r,min;
	scanf("%d%lf",&n,&min);
	for(int i=2;i<=n;i++){
		scanf("%lf",&r);
		if(r<min) min = r;
	}
	printf("%.4f",3.14*min*min);
	return 0;
}

如上面示例程序,竞赛时只需要在平时编程的基础上,在main开头部分使用freopen函数就能很方便地指定程序从文件读数据和将结果输出到文件,其他内容不用做任何修改。其实也有其它的处理方法,要繁琐一些,感兴趣的话可以自行百度。

5.编译运行程序,测试程序

编译运行程序,在Dev C++平台会发现运行情况如下图所示:

只有Dev C++平台的运行提示信息,没有任何输入输出的迹象。这是因为我们已经用代码重定向输入输出到指定文件了,这个时候程序中的输入会自动从文件中去“读”数据,我们打开之前自己准备好的输入文件(min.in,内容其实是从输入样例文件中拷贝来的),会发现以下内容:

如果没有重定向从文件输入数据,在运行程序时,这些其实就是需要我们在命令行输入的数据。现在我们把要输入的数据组织在文件中,让程序从文件中自行读数据。

注意:接触到循环语句后,我们编写的程序往往需要输入大量的数据,测试的时候使用上面重定向从文件输入数据的策略,可以大大提高程序运行时输入数据的效率。

在运行结果命令行窗口中,没有看到任何输出的数据。我们回到选手目录问题子目录下,会发现多了一个.out文件(本例是min.out文件):

用记事本打开.out文件,可知原来程序所有输出的数据都输出到了这个文件中(这就是重定向输出到文件的效果):

我们可以对比.out文件中的输出结果和输出样例.ans文件中的内容,如果相同那么可以初步认为程序没有问题。当然还需要对其他样例进行测试(将其它样例输入文件内容复制粘贴到min.in文件中,再次运行程序,对比.out文件和输出样例.ans文件的内容),甚至还需要自己设计输入输出样例进行更有针对性的测试。

其实NOIP测评的时候,也是按照上面的环节来测试每个数据点的,测试点的输入数据通过文件输入到程序,运行后输出结果会自动保存到输出文件中,再将输出文件与标准答案文件内容对比判断是否通过该点。只不过整个过程是用测评程序自动完成的。

注意:有些选手为了能够更方便看到程序运行结果,会注释掉程序中的重定向输出的语句。但是在竞赛结束前的检查环节,一定要重点检查输入输出重定向语句,如果没有按要求重定向输入输出或者语句被注释,那么该题肯定没有得分!

兴趣扩展:为什了洛谷平台不需要像NOIP那样重定向输入输出,也能对选手提交的程序进行测试呢?其实可以通过命令来实现输入输出重定向。我们编译好程序自动生成exe后,进入cmd命令行环境,使用cd命令进入到源程序目录,试试在cmd中输入命令:min.exe < min.in > min.out(注意根据实际情况修改本命令中的文件名,min.exe也可以不写.exe直接用可执行文件的文件名),发现也能从min.in中读取数据并将结果输出到min.out文件。但是注意这个时候程序中不能有freopen语句来重定向输入输出。自动测评程序将提交的程序编译后(也能通过命令来编译),通过类似前面的命令来测评每个点并比较程序输出文件和标准结果文件内容判断是否通过该点。

二、常见的循环输入模式

1.输入\(C\)个用空格隔开的数据(或者输入\(C\)行每行只有一个数据),\(C\)是常数:

int m;
for(int i=1;i<=10;i++){
	cin>>m; 
}

输入多个数据时,多个数据间的分隔符可以是空格,也可以是换行,所以左边的程序适用于输入用空格隔开的10个整数,也适用于输入10行每行一个整数。

如果每行输入多个,直接修改循环体中的cin语句即可:

int a,b,c;
for(int i=1;i<=10;i++){
	cin>>a>>b>>c; 
}

2.先输入正整数\(n\),再输入\(n\)个用空格隔开的数据(或者\(n\)行每行只有一个数据):

int n,m;
cin>>n;
for(int i=1;i<=n;i++){
	cin>>m;
}
int n,m;
cin>>n;
while(n--){ //用n--,不能用--n
	cin>>m;
}
//循环结束后n==0,这样的结构适用于循环结束后不会使用n原来值的情况

3.先输入正整数\(n,m\),紧接着要输入\(n\)行数据,每行是\(m\)个用空格隔开的数据:

int n,m,p;
cin>>n>>m;
for(int i=1;i<=n;i++){
	for(int j=1;j<=m;j++){
		cin>>p;
	}
}

4.先输入一个正整数\(n\),紧接着要输入\(n\)组数据,每组数据占一行是用空格隔开的若干数据,每行第一个正整数是该组数据数量\(m\),后面有\(m\)个数据:

int n,m,p;
cin>>n;
for(int i=1;i<=n;i++){
	cin>>m;
	for(int j=1;j<=m;j++){
		cin>>p;
	}
}

5.输入数据直到碰到一个特殊值结束(例如输入-1表示处理结束):

int n;
cin>>n;
while(n!=-1){
	//处理语句 
	cin>>n;
}

int n;
for(cin>>n;n!=-1;cin>>n){
	//处理语句 
	
}
int n;
while(true){
	cin>>n;
	if(n==-1) break;
	//处理语句 
}

6.输入数据直到没有数据为止(或者到文件末尾):

int n;
while(cin>>n){
	 	
}
int n,m;
while(cin>>n>>m){
	 	
}
int n;
while(scanf("%d",&n)!=EOF){
	 	
}
int n,m;
while(scanf("%d%d",&n,&m)==2){
	 	
}

此时,如果是将输入重定向到了文件,那么会读到文件末尾没有数据为止。如果在命令行运行,会发现无法正常结束输入。在要处理的数据完全输入后,先按住CTRL+Z,然后再按回车,就能退出用于输入数据的while循环,执行后面的语句了。

三、常见的循环输出模式

同循环输入一致,将输出语句置于循环体中就能实现重复输出大量数据。这里需要强调的是,当输出的内容是多个用空格隔开的数据或者是多行数据时,测评时每行末尾的空格和最后额外的空行会被自动忽略。

举例说明:依次输出1~10每个整数的平方,输出时每个数据用一个空格隔开。

我们直接这样编程即可:

#include<iostream>
using namespace std;
int main()
{
	for(int i=1;i<=10;i++){
		cout<<i*i<<" ";
	}
    return 0;
}

仔细分析可知,其实输出了10个整数10个空格,也就是末尾多输出了一个空格,但是测评的时候会自动忽略每行末尾的空格,不用担心测评通不过。

当然就完全没有必要像下面这样编程:

#include<iostream>
using namespace std;
int main()
{
	for(int i=1;i<=10;i++){
		cout<<i*i;
		//避免末尾没有多余空格
		//其实这样做没有必要
		if(i!=10) cout<<" ";
	}
    return 0;
}

同样的,我们打印九九乘法表的程序:

#include<iostream>
using namespace std;
int main()
{
    for(int i=1;i<=9;i++){
        for(int j=1;j<=i;j++){
            cout<<j<<"*"<<i<<"="<<j*i<<" ";
            if(j*i<10) cout<<" ";
        }
        cout<<endl;
    }
    return 0;
}

其实在每一行末尾都有多余的空格,并且在最后还有一个多余的空行,这些在测评的时候都会被忽略,不用担心测评通不过。

算法时间复杂度

tangyj626阅读(501)

在计算机科学中,算法的时间复杂度是一个函数,它定性描述该算法的运行时间。时间复杂度常用大O符号表述,一般描述为与问题规模\(n\)相关的函数\(O(f(n))\),但不包括这个函数的低阶项和最高阶系数。

一、时间复杂度的定义

一个算法花费的时间与算法中基本语句的执行次数成正比例,哪个算法中基本语句执行次数多,它花费时间就多。一个算法中的基本语句执行次数称为语句频度或时间频度,记为\(T(n)\)。

一般情况下,算法中基本操作重复执行的次数是问题规模\(n\)的某个函数,用\(T(n)\)表示,若有某个辅助函数\(f(n)\),使得当\(n\)趋近于无穷大时,\(T(n)/f (n)\)的极限值为不等于零的常数,则称\(f(n)\)是\(T(n)\)的同数量级函数(大学高等数学知识),而\(O(f(n)) \)为算法的渐进时间复杂度,简称时间复杂度。

在各种不同算法中,若算法中语句执行次数为一个常数,则时间复杂度为\(O(1)\)或\(O(c)\)(\(c\)为常量),另外,在时间频度\(T(n)\)不相同时,时间复杂度\(O(f(n))\)有可能相同,如\(T(n)=n^2+3n+4\)与\(T(n)=4n^2+2n+1\)它们的频度不同,但时间复杂度相同(时间复杂度不考虑\(T(n)\)的低阶项和最高阶项系数),都为\(O(n^2)\)。

常见的时间复杂度有:\(O(1)\)、\(O(c)\)(\(c\)为常量)、\(O(\log{n})\)、\(O(n)\)、\(O(n\log{n})\)、\(O(m*n)\)、\(O(n^2)\)、\(O(n^3)\)、\(O(2^n)\)、\(O(n!)\)等。解决同一个问题的不同算法,时间复杂度越低,则算法效率越高。

二、时间复杂度的计算方法

根据定义,可以归纳出时间复杂度的基本计算步骤:

  1. 计算出基本操作的执行次数\(T(n)\):
    基本操作即算法中的每条语句(注意调用函数很特别,调用函数的语句往往不能简单认为是1次基本操作),语句的执行次数也叫做语句的频度。在做算法分析时,一般要考虑最坏的情况
  2. 计算出\(T(n)\)的数量级:
    求\(T(n)\)的数量级,只要将\(T(n)\)进行如下一些操作:忽略常量、低次幂,去掉最高次幂的系数,得到\(T(n)\)的数量级函数\(f(n)\)。
  3. 用\(O(f(n))\)来表示时间复杂度。

简而言之:如果一个算法的执行次数是 \(T(n)\),那么只保留最高次项,同时忽略最高项的系数后得到函数\( f(n)\),此时算法的时间复杂度就是 \(O(f(n))\)。

举一个例子来说明:

#include<iostream>
using namespace std;
int main()
{
	int s1,s2,n,i,j;
	s1 = 0;
	s2 = 0;
	cin>>n;
	for(i=1;i<=n;i++){
		s1 += i;
		for(j=1;j<=n;j*=2){
			s2 += j;
		}
	}
	cout<<s1<<" "<<s2<<endl;			 
    return 0;
} 

程序5、6、7、8行语句的频度均为1,外层循环赋初值i=1频度为1,这部分总频度为5;

循环体中i<=n、s1+=i、j=1、i++,这4句执行次数均为\(n\)(严格计算的话,条件判断语句执行次数是\(n+1\)次),这部分总频度为\(4n\);

内层循环中j<=n、s2+=j、j++,这3句执行次数均为\(n\log_2{n}\)(高中数学对数知识),这部分总频度为\(3n\log_2{n}\);

第15、16行语句的频度为1,这部分总频度为2。

经过上面的分析,可知本程序\(T(n)=5+4n+3n\log_2{n}+2\),整理得\(T(n)=3n\log_2{n}+4n+7\),忽略掉\(T(n)\)中的常量、低次幂和最高次幂的系数,得到\(T(n)\)的数量级函数\(f(n) = n\log_2{n}\),所以算法的时间复杂度是\(O(n\log_2{n})\),可以再进一步简记为\(O(n\log{n})\)。

决定算法复杂度的是执行次数最多的语句,一般也是最内层循环的语句,上述步骤可以简化为:

  1. 找到执行次数最多的语句(要考虑最坏的情况
  2. 计算该语句执行次数的数量级函数\(f(n)\)
  3. 用\(O(f(n))\)来表示算法时间复杂度

仍然以上述程序为例,执行次数最多的语句是内层循环体中的:s2 += j;,执行次数的数量级\(f(n)=n\log_2{n}\),所以算法时间复杂度为:\(O(n\log_2{n})\)。

此外要注意:算法时间复杂度分析要考虑最坏的情况,以下面程序为例:

#include<iostream>
using namespace std;
int main()
{
    int n,f;
	cin>>n>>f;
	for(int i=1;i<=n;i++){
		if(f==0){
			for(int j=1;j<=n;j++){
				//...考虑最坏情况,这里是执行次数最多的语句 
			}
		}else{
			//...
		}
	}             
    return 0;
}

最坏情况时(输入的f值为0),执行次数最多的是双重循环体中的语句,算法的时间复杂度为\(O(n^2)\)。

但是要注意,并不是出现循环,算法的时间复杂度就是\(O(n)\)、\(O(n*m)\)、\(O(n^2)\)之类的,也有可能是\(O(c)\)(\(c\)为常量),来看前面求“水仙花数”的程序:

#include<iostream>
using namespace std;
int main()
{
    for(int i=100;i<=999;i++){
        int g = i%10;
        int s = i/10%10;
        int b = i/100%10;
        if(g*g*g+s*s*s+b*b*b==i) cout<<i<<" ";
    }
    return 0;
} 
#include<iostream>
using namespace std;
int main()
{
    for(int b=1;b<=9;b++){
        for(int s=0;s<=9;s++){
            for(int g=0;g<=9;g++){
                //百位上的数b,十位上的数s,个位上的数g
                int n = b*100+s*10+g;
                if(g*g*g+s*s*s+b*b*b==n) cout<<n<<" ";
            }
        }
    }
    return 0;
} 

两段程序执行次数最多的语句都只执行900次,因此时间复杂度都是\(O(c)\)。

三、常见时间复杂度举例

1.\(O(1)\)或\(O(c)\)

前面计算\(\sum\limits_{i=1}^{n}i=1+2+3+...+n\)的结果,如果直接使用高中数学知识的等差数列求和公式\(\sum\limits_{i=1}^{n}i=\frac{n \times (n+1)}2\),算法的时间复杂度是\(O(c)\):

#include<iostream>
using namespace std;
int main()
{
    int n;
	cin>>n;
	cout<<n*(n+1)/2<<endl;   //n和n+1肯定有一个是偶数,n*(n+1)能被2整除      
    return 0;
}

2.\(O(\log{n})\)

求解方法是用变量\(a\)追踪木棍的长度,循环中每次折半,那么算法的时间复杂度是\(O(\log_2{n})\):

#include<iostream>
using namespace std;
int main()
{
    int a,days = 1;
    cin>>a;
    while(a>1){
        days ++;
        a /= 2;
    }
    cout<<days;
    return 0;
} 

3.\(O(n)\)

仍然是上面累加求和的例子\(\sum\limits_{i=1}^{n}i\),如果使用一重循环来累加求和,那么算法的时间复杂度是\(O(n)\):

#include<iostream>
using namespace std;
int main()
{
    int n,s = 0;
	cin>>n;
	for(int i=1;i<=n;i++) s += i;
	cout<<s<<endl;          
    return 0;
}

4.\(O(n\log{n})\)

对于1~\(n\)的每个正整数\(n_i\),计算\(2^{x_i} \ge n_i\)的最小值\(x_i\),如果不用\(cmath\)中的\(log2\)函数,可以这样编程:

#include<iostream>
using namespace std;
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        int j = 0,t = 1;    //t用来保存\(2^j\)的结果
        while(t<i){
            t *= 2;
            j++;
        }
        cout<<j<<endl;
    }   
    return 0;
}

执行次数最多的语句是内层循环的语句(并且是外层循环最后1次时),可知最大执行次数是\(n*\log_2{n}\) 次,那么算法的时间复杂度是\(O(n\log_2{n})\)。可以进一步优化算法,将时间复杂度降低到\(O(n)\):

#include<iostream>
using namespace std;
int main()
{
    int n,j = 0,t = 1;     //t用来保存\(2^j\)的结果
    cin>>n;
    for(int i=1;i<=n;i++){
    	cout<<j<<endl;
    	if(i>=t) j++,t *= 2;
	}
    return 0;
}

5.\(O(n*m)\)

输出\(n\)行\(m\)列由字符*拼成的字符矩形。借助双重循环实现,外层\(n\)次循环控制行数,内层\(m\)次循环控制每行字符数,那么算法的时间复杂度是\(O(n*m)\):

#include<iostream>
using namespace std;
int main()
{
    int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++) cout<<"*";
		cout<<endl;
	}        
    return 0;
}

6.\(O(n^2)\)

前面章节“阶乘累计求和”的例子,外层\(n\)次循环用来累加,内层\(i\)次循环计算\(i!\):

#include<iostream>
using namespace std;
int main()
{
    long long t,s = 0;
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        t = 1;
        for(int j=1;j<=i;j++) t *= j;
        s += t; 
    }
    cout<<s;
    return 0;
} 

执行次数最多的语句是内层循环的语句,可知执行次数是\(\sum\limits_{i=1}^{n}i = 1+2+3+...+n = \frac{n \times (n+1)}{2}\) 次,那么算法的时间复杂度是\(O(n^2)\)。当然可以进一步优化程序,降低算法的时间复杂度到\(O(n)\):

#include<iostream>
using namespace std;
int main()
{
    long long t = 1,s = 0;
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        t *= i;    //t是i! 
        s += t; 
    }
    cout<<s;
    return 0;
}

7.\(O(n^3)\)

团队里有\(n\)位选手,每位选手都参加了\(n\)次训练,每次训练分为\(n\)次单元,现在给定每位选手每次训练每个单元的持续时间,计算所有选手平均单元训练时间。很显然,使用三重循环,在最内层循环体中输入数据并累加,那么算法的时间复杂度是\(O(n^3)\):

#include<cstdio>
int main()
{
    int n,m,s = 0;
    scanf("%d",&n);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			for(int k=1;k<=n;k++){
				scanf("%d",&m);
				s += m;
			}
		}
	}
	printf("%.3f",1.0*s/(n*n*n));
    return 0;
}

数组

tangyj626阅读(786)

前面的例题,特别是循环结构中的例题,涉及到读取大量的数据进行处理,但都是读入数据保存到变量后立即使用变量处理数据,处理结束后也不再使用这些数据了,所以往往在循环体中都是将数据输入到同一个变量就行。但是有时候,我们读入若干数据后,还需要将这些数据都保存下来,以便于后面再次使用。这个时候如果数据量少还好处理,但是数据多的话使用很多变量来存储每一个数据是不现实的,幸好C++中有一个强有力的工具——数组,使用数组能够很好地解决上述问题。

一、引子

回看前面的一个例题,计算输入的\(n\)个整数的最小值,使用“打擂台”的方式,将最小值存放到变量\(min\)中,利用\(n\)次循环,每次输入一个整数\(m\),输入后和\(min\)打擂并将新的最小值保存到\(min\)中。可见每输入一个整数\(m\),就能立即处理,处理后本次输入的\(m\)就没有任何作用了。对于这个问题,我们不需要想办法把所有输入的整数都保存下来:

#include<iostream>
using namespace std;
int main()
{
    int n,m,min;
    cin>>n;
    for(int i=1;i<=n;i++){
        //每次输入的整数都放在变量m中,会自动覆盖掉上次输入的数(也保存在m中) 
        //因为每次处理完输入的整数后,这个数后面不会再用到,可以丢弃 
        cin>>m;
        
        //第一个数 或者 m比最小值小 
        if(i==1 || m<min) min = m;
    }
    cout<<min;
    return 0;
} 

再来看另外一个例题:输入10个整数,计算所有整数的平均值\(avg\),输出10个整数中比平均值\(avg\)小的数。不难得出程序的框架:

#include<iostream>
using namespace std;
int main()
{
    int m;
    double avg = 0; 
    for(int i=1;i<=10;i++){
        cin>>m;
        avg += m;
    }
    avg /= 10;
    
    //接下来需要从10个输入的整数中找出小于avg的数
	//但是现在只有最后一个输入的数保存在m中,其它数都已经被丢弃了 
    return 0;
} 

很显然,我们需要想办法将输入的10个整数都保存下来,计算了平均之后,再将保存下来的10个整数和平均值比较。现在的问题是,如何保存这10个整数呢?用10个变量?只能说可行但是很不方便,如果问题规模扩大,要处理100个整数,难道要手写100个变量、100个几乎相同的处理过程,显然是不可取的。如果是输入\(n\)个整数,输出\(n\)个整数中小于平均值的所有整数,那么要“动态地定义\(n\)个变量”,这是无法实现的。

二、数组的用途

要解决前面的问题,简而言之就是需要有一种高效的方法,能够存储若干的数据,并且还能将每个数据取出来使用。C++中的数组就具备这样的功能。借助数组,可以在内存中开辟足够的连续的存储空间,将大量相同数据类型的数据存储到数组内部,并且又能像使用单个变量一样的方式去取出存储在数组中的数据参加运算,还能对存储在数组中的数据重新赋值。存储在数组中的每个数据称为数组的元素

三、数组的使用方法

解决问题时,数据的规模一般是已知的,所以要借助数组保存的数据的个数(或者说最大个数)是已知的。使用数组就像使用变量一样,需要先定义,再使用。

1.数组的定义

(一维)数组的定义方式是: 数组类型 数组变量名称[元素数量];,例如:

int a[10];      //定义了一个名为a、一共有10个int类型元素的数组
long long b[100],c[1000];
double arr[100001];
char str[1001];

一句简单的定义,就已经将数组的核心内容完全体现了:

  1. 数组也是一种特殊的变量,所以定义中有数组变量名称
  2. 数组中的所有元素都必须是相同的数据类型,定义中有数组类型,体现了数组中每个元素的数据类型
  3. 数组中可以存储很多相同数据类型的元素,在定义时需要指定元素的数量,也称为数组的长度。特别注意的是,竞赛编程定义数组时,数组的长度(也就是[]中的内容)一般设置成常量(字面常量或符号常量)。

在定义数组的时候,甚至还可以为数组的每个元素赋初值:

int a[10] = {1,2,3,4,5,6,7,8,9,10};

定义并初始化数组,可以省略长度,此时数组长度会被自动设置成初始化语句中数组元素的数量。

int a[] = {1,2,3,4,5,6,7,8,9,10};  

2.数组元素的使用

使用数组,一般是使用数组中的元素,这些元素就像普通的变量一样,可以为其赋值,可以参加运算,可以将数据输入到数组的某个元素中。只不过由于它们是数组的元素,所以书写的时候比起前面普通变量的书写要特殊一些。例如int a[5];,定义了一个名称为a、拥有5个int类型元素的数组。数组a中的元素的书写方法如下:

第1个元素第2个元素第3个元素第4个元素第5个元素
a[0]a[1]a[2]a[3]a[4]

上面书写数组元素时,[]号中的内容称为下标。可见数组中元素的使用方法是:数组名[下标]。并且数组的下标从0开始,最后一个下标应该是数组长度-1。正是因为数组下标从0开始,所以我们以后描述数组的元素,一般这样来:

0个元素第1个元素第2个元素第3个元素第4个元素
a[0]a[1]a[2]a[3]a[4]

前面已经提出,数组的元素就是数组内的特殊“变量”,变量怎么用,数组的元素就能怎么用,就是书写时要用数组名[下标]的特殊形式:

int x,y,z;
x = 123;
y = x-1;
cin>>z;
cout<<x<<endl;
cout<<x+y+z<<endl;
int a[10];
a[0] = 123;
a[1] = a[0]-1;
cin>>a[2];
cout<<a[0]<<endl;
cout<<a[0]+a[1]+a[2]<<endl;

回到前面的问题,现在我们可以将10个输入的整数存放到数组中解决问题了:

#include<iostream>
using namespace std;
int main()
{
	int a[10];
	double avg = 0;

	for(int i=0;i<10;i++){
		cin>>a[i];       //第i个数输入到数组元素a[i]中
		avg += a[i];
	}

	avg /= 10;

	for(int i=0;i<10;i++){
		if(a[i]<avg){    //判断已经存储到数组的第i个元素a[i]是否小于avg
			cout<<a[i]<<" ";
		}
	}
    return 0;
} 

这里理解起来有一点难度,但是将循环“翻译”成顺序结构就容易理解了:

cin>>a[0];
avg += a[0];

cin>>a[1];
avg += a[1];

//...

cin>>a[9];
avg += a[9];
if(a[0]<avg) {
	cout<<a[0]<<" ";
}
if(a[1]<avg){
	cout<<a[1]<<" ";
}
//...
if(a[2]<avg){
	cout<<a[2]<<" "; 
}

从上面的例子可以看出,数组元素的使用往往是结合循环来批量处理的。在循环体中我们会用类似这样的功能性语句来描述:使用数组的第i个元素a[i]完成什么操作。还要特别注意循环体初值(一般用0)、终值(一般是数组元素数量-1)与数组下标的关系!

要使用数组的第i个元素,直接使用a[i]就行,非常简单高效,这样的存取方式称为“随机存取”,也就是可以通过仅仅一次基本语句操作就能对一堆数据中指定序号的那个数进行存取操作(取出值、存入新值)。数组是典型的随机存取结构。


注意:定义数组和使用数组元素都使用了[],但定义数组的时候[]中的内容是常量,指明数组的长度;使用数组元素的时候[]中是下标,可以是常量,也可以是变量,并且往往是循环的控制变量。此外要注意下标是变量的时候,在运算的过程中要避免下标溢出(出现负数或者等于或超出了数组元素的数量)。

此外,数组的元素在内存中是连续存储的,所以使用数组比起手写若干变量解决问题,不仅仅是编程更加简洁方便,而且程序的执行效率也更高!


四、竞赛时数组使用技巧

1.使用全局数组

竞赛往往要借助数组处理大量的数据,这个时候数组需要“开”很大(定义数组时数组的长度值很大),为了避免开大数组的程序运行时被操作系统拒绝分配内存出现运行时错误,可以将数组定义为全局数组。具体方法是将数组的定义语句放到main函数的外部且在main函数的前面即可:

#include<iostream>
using namespace std;
int a[1000000];     //大数组建议定义成全局数组
int main()
{

    return 0;
}

知识点:使用全局数组,在定义全局数组后,数组的每个元素都会有默认值,例如最常用的int类型的全局数组元素的默认值是0。

2.定义数组时使用符号常量指定数组长度

在定义数组的时候,建议使用符号常量来指定数组的长度,并且在程序中要使用数组的长度的地方都用该符号常量,这样做的优势通过下面程序对比可知:

#include<iostream>
using namespace std;
//要修改数据规模,处理100个整数
int a[10];      //这里要修改
int main()
{
    double avg = 0;
    //下面语句要修改
    for(int i=0;i<10;i++){
        cin>>a[i]; 
        avg += a[i];
    }
    avg /= 10;   //这里要修改
    //下面语句要修改
    for(int i=0;i<10;i++){
        if(a[i]<avg){
            cout<<a[i]<<" ";
        }
    }
    return 0;
} 
#include<iostream>
using namespace std;
//如果要修改数据规模,修改下面一处即可
const int N = 10; //赋值成100即可
int a[N];
int main()
{
	double avg = 0;
    for(int i=0;i<N;i++){
        cin>>a[i];
        avg += a[i];
    }
    avg /= N;
    for(int i=0;i<N;i++){
        if(a[i]<avg){
            cout<<a[i]<<" ";
        }
    }
    return 0;
}

3.为了和自然序列一致,数组长度额外+1

数组的下标从0开始,现实生活中自然序列从1开始。例如上面的问题,修改为输出小于平均值的数的序号(从1开始计数),那么就需要这样处理:

#include<iostream>
using namespace std;
const int N = 10;
int a[N];
int main()
{
	double avg = 0;
    for(int i=0;i<N;i++){
        cin>>a[i];
        avg += a[i];
    }
    avg /= N;
    for(int i=0;i<N;i++){
        if(a[i]<avg){
            cout<<i+1<<" ";    //下标比自然序号小1,所以输出i+1 
        }
    }
    return 0;
}

像这样下标和自然序号有关联的问题,处理的时候需要格外注意。其实我们还可以“改造”一下数组,让存储数据的下标和自然序号一致。具体做法是定义数组的时候至少额外多开一个,存取数据就从数组的1号元素开始,不用0号元素:

#include<iostream>
using namespace std;
const int N = 10;
int a[N+1];    //至少额外多开1个 
int main()
{
	double avg = 0;
	//只使用a[1]~a[N],不使用a[0] 
    for(int i=1;i<=N;i++){
        cin>>a[i];
        avg += a[i];
    }
    avg /= N;
    //只使用a[1]~a[N],不使用a[0]
    for(int i=1;i<=N;i++){
        if(a[i]<avg){ 
            cout<<i<<" ";
        }
    }
    return 0;
}

4.数组长度设置为数据规模最大值

将上面的问题变形:首先输入一个正整数\(n\)(\(1 \le n \le 10000\)),然后输入\(n\)个整数,计算所有整数的平均值\(avg\),输出\(n\)个整数中比平均值\(avg\)小的数。初学者可能不假思索,写出下面的程序:

#include<iostream>
using namespace std; 
int main()
{
	int n;
	cin>>n;
	int a[n];   //不建议这样写,即使编译能通过,甚至运行也没有问题,但是有风险 
	double avg = 0; 
    for(int i=0;i<n;i++){
        cin>>a[i];
        avg += a[i];
    }
    avg /= n;
    for(int i=0;i<n;i++){
        if(a[i]<avg){ 
            cout<<a[i]<<" ";
        }
    }
    return 0;
}

注意:上面的程序存在风险,前面指出定义数组的时候数组长度应该是常量,上面程序使用了变量。随着编译器的不断优化,高版本的编译器已经能够支持上面程序中那样的动态定义数组(数组的长度是一个变量),但是存在较大的风险,不推荐使用。

那么该如何处理呢?我们注意到了题目中给出了数据规模——\(1 \le n \le 10000\),所以最多只会输入10000个整数,那么我们就按照数据规模的极限值开数组,这样肯定能满足所有情况。但是这样做有一个缺点就是如果运行时输入的\(n\)值远小于数据规模,数组大量的存储空间没有使用起来,浪费了存储空间。不过竞赛时推荐使用这样的方式,即使C++中也支持其它动态开数组的方法(感兴趣的同学可自行百度)。

#include<iostream>
using namespace std;
int a[10000];     //按照数据规模的极限值开数组
int main()
{
	int n;
	cin>>n;
	double avg = 0; 
    for(int i=0;i<n;i++){
        cin>>a[i];
        avg += a[i];
    }
    avg /= n;
    for(int i=0;i<n;i++){
        if(a[i]<avg){ 
            cout<<a[i]<<" ";
        }
    }
    return 0;
}

竞赛时更一般的做法是,定义数组时,在数据规模的基础上再多开几个长度(例如10个),这样可以有效避免一些边界问题!

数组元素的操作

tangyj626阅读(458)

通过前面的学习,在解决问题时根据数据规模开一个很大的数组,并不意味着所有的元素都会被用来存储数据,本小节一起来学习数组元素的操作,包括数组末尾追加元素、插入元素和删除元素。

前面我们使用过数组来存储输入的数据:

#include<iostream>
using namespace std;
int main()
{
	int a[10];
	for(int i=0;i<10;i++) cin>>a[i];
	return 0; 
}

实际操作中,并非数组的每个元素都全部用来存储数据,一般做法是将要存储的数据按照数组下标从小到大依次存储:

一系列存储操作后,如果还要存储新数据,追加新元素的位置(其实就是下标)就很重要。例如我们通过循环枚举2~1000所有整数,在循环体中筛选出其中的素数存储到数组中,可以使用下面的方法:

#include<iostream>
using namespace std;
int a[1000];
int main()
{
	int cnt = 0;  //cnt变量就是依次存入数组的元素的下标 
	for(int i=2;i<=1000;i++){
		int find = 0;
		for(int j=2;j*j<=i;j++){
			if(i%j==0){
				find = 1;
				break;
			}
		}
		//追加到数组实际存储数据区域的下一个位置 
		if(find==0) a[cnt++] = i; 
	}
	//处理结束后,数组实际使用长度是cnt,从0下标开始存储
	for(int i=0;i<cnt;i++) cout<<a[i]<<" ";
	return 0; 
}

如果要将某个数据\(p\)插入到数组实际存储区a[j]元素之前,保持存储数据相对位置不变:

#include<iostream>
using namespace std;
int a[1000];
int main()
{
	int cnt = 0;  //cnt变量就是依次存入数组的元素的下标 
	for(int i=2;i<=1000;i++){
		int find = 0;
		for(int j=2;j*j<=i;j++){
			if(i%j==0){
				find = 1;
				break;
			}
		}
		//追加到数组实际存储数据区域的下一个位置 
		if(find==0) a[cnt++] = i; 
	}
	
	for(int i=0;i<cnt;i++) cout<<a[i]<<" ";
	cout<<endl;
	
	//将数据p存储到a[j]之前
	int p,j;
	cin>>p>>j;      //输入数据时注意 0 < j < cnt
	//元素后移一位(从最后一个元素开始) 
	for(int i=cnt;i>j;i--) a[i] = a[i-1];
	a[j] = p;
	cnt++;
	
	for(int i=0;i<cnt;i++) cout<<a[i]<<" ";
	cout<<endl;
	return 0; 
}

如果要删除元素a[j],保持存储数据相对位置不变:

#include<iostream>
using namespace std;
int a[1000];
int main()
{
	int cnt = 0;  //cnt变量就是依次存入数组的元素的下标 
	for(int i=2;i<=1000;i++){
		int find = 0;
		for(int j=2;j*j<=i;j++){
			if(i%j==0){
				find = 1;
				break;
			}
		}
		//追加到数组实际存储数据区域的下一个位置 
		if(find==0) a[cnt++] = i; 
	}
	
	for(int i=0;i<cnt;i++) cout<<a[i]<<" ";
	cout<<endl;
	
	//删除a[j] 
	int j;
	cin>>j;     //输入数据时注意 0 < j < cnt
	//元素前移一位(从a[j+1]开始) 
	for(int i=j;i<cnt-1;i++) a[i] = a[i+1];
	cnt--;
	
	for(int i=0;i<cnt;i++) cout<<a[i]<<" ";
	cout<<endl;
	return 0; 
}

实际操作时,要尽量避免数组内元素大量的移动,这样会耗费不少时间!

数组例题——使用数组下标查询结果

tangyj626阅读(277)

先来看一个问题:输入整数 \(y\) 和 \(m\),输出公元 \(y\) 年 \(m\) 月的天数。

首先想到的是使用多分支结构来进行分类讨论:

#include<iostream>
using namespace std;
int main()
{
	int y,m;
	cin>>y>>m;
	if(m==1 || m==3 || m==5 || m==7 || m==8 || m==10 || m==12){  //大月 
		cout<<31<<endl;
	}else if(m==4 || m==6 || m==9 || m==11){  //小月 
		cout<<30<<endl;
	}else{  //2月 
		if(y%4==0 && y%100!=0 || y%400==0){  //闰年 
			cout<<29<<endl;
		}else{  //平年 
			cout<<28<<endl;
		}
	}
    return 0;
}

其实还可以将一年里\(12\)个月的天数存放到一个数组a里:\(1\)月的天数存放到\(a[1]\),\(2\)月的天数存放到\(a[2]\),……,\(12\)月的天数存放到\(a[12]\)(抽象概括就是 \(i\)月的天数存放在数组 \(a[i]\)元素中),这样月份的序号和存放该月天数的数组元素下标一一对应。那么\(m\)月的天数就在\(a[m]\)中,直接输出\(a[m]\)即可。需要注意的是,\(2\)月的天数要分闰年、平年处理。

#include<iostream>
using namespace std;
int main()
{
	int y,m;
	cin>>y>>m;
	//定义数组并为所有元素赋初值
	//a[0]不用;a[2]值为28,默认是平年 
	int a[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
	//如果是闰年,修改2月天数 
	if(y%4==0 && y%100!=0 || y%400==0){
		a[2]++;
	}
	//输出m月天数(就存放在a[m]中) 
	cout<<a[m]<<endl; 
    return 0;
}

将每月天数巧妙地存放到数组中,月份的序号和存放该月天数的数组元素下标一一对应,这样通过下标直接就能查到指定月份的天数。这种根据问题构造与结果相关的数组,通过下标查询结果的方法,大家注意体会。类似于数学中的映射、现实生活中的根据索引查表。

再来看一个例题,输入\(n\)个\(0~10000\)范围内的整数,找到出现次数最多的整数,输出它出现的次数。

现实生活中,可以在一张纸上写下所有的整数,然后用“画正字”的方法来统计每个整数出现的次数,其实就是为每个整数设计了一个计数器来记录该整数出现的次数。那么在程序中可以开一个数组,数组的每个元素都是计数器,来记录每个整数出现的次数。

这里输入的整数集中在一个区间\(0~10000\),那我们开一个数组a[10001]用来存放\(0~10000\)每个整数出现的次数,a[0]存放的是0出现的次数,a[1]存放的是1出现的次数,……,a[10000]存放的是10000出现的次数(抽象概括就是数组 \(a[i]\)元素存放的是整数 \(i\) 出现的次数),这样整数和存放整数出现次数的数组元素下标一一对应。那么输入一个整数k,意味着k出现了一次,只需要a[k]++即可更新它出现的次数。所有数据输入完毕后,数组中存放了这个范围内的每个整数出现的次数,只需要计算数组所有元素的最大值即可。

#include<iostream>
using namespace std;
//开全局int数组,数组的每个元素初始值为0
//正好符合开始时,每个数出现的次数为0 
int a[10001];
int main()
{
	int n,k;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>k;
		a[k]++;		//k出现次数+1 
	}
	
	//打擂求数组所有元素最大值 
	int max = 0;
	for(int i=0;i<=10000;i++){
		if(a[i]>max) max = a[i];
	}
	cout<<max<<endl;
    return 0;
}

进一步,还是输入\(n\)个\(0~10000\)范围内的整数,如何将这些整数去重后按照从小到大的顺序输出呢?还是采用上面的策略,所有数据输入完毕后,数组中存放了这个范围内的每个整数出现的次数,我们只需要从小到大依次考察\(0~10000\)范围内的每个整数,只要出现过,输出即可,这样就实现了去重+排序的效果。

#include<iostream>
using namespace std;
//开全局int数组,数组的每个元素初始值为0
//正好符合开始时,每个数出现的次数为0 
int a[10001];
int main()
{
	int n,k;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>k;
		a[k]++;		//k出现次数+1 
	}
	
	//从小到大依次考察每个整数 
	for(int i=0;i<=10000;i++){
		//i出现过,输出i 
		if(a[i]>0) cout<<i<<" ";
	}
    return 0;
}

那如果输入的整数范围是\(-10000~10000\),该如何处理呢?

如果还是 cin>>k; a[k]++; 很显然,输入负数的时候,下标k是负数,这不符合语法,会出现运行时错误。那怎么避免下标出现负数呢?开一个数组a[20001]用来存放\(-10000~10000\)每个整数出现的次数,a[0]存放的是-10000出现的次数,a[1]存放的是-9999出现的次数,……,a[10000]存放的是0出现的次数,a[10001]存放的是1出现的次数,……,a[20000]存放的是10000出现的次数(抽象概括就是数组 \(a[i+10000]\)元素存放的是整数 \(i\) 出现的次数),这样整数和存放整数出现次数的数组元素下标仍然一一对应。那么输入一个整数k,意味着k出现了一次,只需要a[k+10000]++即可更新它出现的次数。

整数-10000-9999-9998...01...i...10000
存放整数出现次数
的数组元素
a[0]a[1]a[2]...a[10000]a[10001]...a[i+10000]...a[20000]
#include<iostream>
using namespace std;
const int N = 10000;
//开全局int数组,数组的每个元素初始值为0
//正好符合开始时,每个数出现的次数为0 
int a[2*N+1];
int main()
{
    int n,k;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>k;
        a[k+N]++;        //k出现次数+1 
    }
    
    //打擂求数组所有元素最大值 
    int max = 0;
    for(int i=-N;i<=N;i++){
        if(a[i+N]>max) max = a[i];
    }
    cout<<max<<endl;
    return 0;
}
#include<iostream>
using namespace std;
const int N = 10000; 
//开全局int数组,数组的每个元素初始值为0
//正好符合开始时,每个数出现的次数为0 
int a[2*N+1];
int main()
{
	int n,k;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>k;
		a[k+N]++;		//k出现次数+1 
	}
	
	//从小到大依次考察每个整数 
	for(int i=-N;i<=N;i++){
		//i出现过,输出i 
		if(a[i+N]>0) cout<<i<<" ";
	}
    return 0;
}

数组例题——宾馆房间的门

tangyj626阅读(302)

宾馆里有一百个房间,从1-100编了号。第一个服务员把所有的房间门都打开了,第二个服务员把所有编号是2的倍数的房间门作“相反处理”,第三个服务员把所有编号是3的倍数的房间门作“相反处理”…,以后每个服务员都是如此。编写程序计算当第100个服务员来过后,哪几扇门是打开的。所谓“相反处理”就是:原来开着的门关上,原来关上的门打开。

分析:使用数组来记录门的状态,使用双重循环来模拟每个服务员的处理过程。

#include<iostream>
using namespace std;
const int N = 100;
//数组doorStatus每个元素记录对应门的状态
//doorStatus[i]记录编号为i的门的状态:0表示关闭,1表示打开
//全局数组doorStatus所有元素会被自动初始化成0,正好表示最开始时所有门是关闭的 
int doorStatus[N+1];
int main()
{
	//100个服务员依次操作 
	for(int i=1;i<=N;i++){
		//第i个服务员要把编号是i的倍数的房间门做相反处理
		for(int j=i;j<=N;j=j+i){
			doorStatus[j] = 1-doorStatus[j];	//相反处理:1→0、0→1 
		} 
	}
	
	//筛选出数组中值是1的元素的下标
	for(int i=1;i<=N;i++){
		if(doorStatus[i]) cout<<i<<" ";
	}
	return 0; 
}
#include<iostream>
using namespace std;
const int N = 100;
//也可以使用bool数组doorStatus每个元素记录对应门的状态
//doorStatus[i]记录编号为i的门的状态:false表示关闭,true表示打开
//全局bool数组doorStatus所有元素会被自动初始化成false,正好表示最开始时所有门是关闭的 
bool doorStatus[N+1];
int main()
{
    //100个服务员依次操作 
    for(int i=1;i<=N;i++){
        //第i个服务员要把编号是i的倍数的房间门做相反处理
        for(int j=i;j<=N;j=j+i){
            doorStatus[j] = !doorStatus[j];//相反处理:true→false、false→true 
        } 
    }
    
    //筛选出数组中值是true的元素的下标
    for(int i=1;i<=N;i++){
        if(doorStatus[i]) cout<<i<<" ";
    }
    return 0; 
}

运行程序,输出结果如下:

1 4 9 16 25 36 49 64 81 100

分析结果才发现原来有规律,就是完全平方数呀!那么我们还可以修改程序提高运行效率(当然竞赛的时候也可以采用这样取巧的方法):

#include<iostream>
using namespace std;
int main()
{
	for(int i=1;i<=10;i++)
		cout<<i*i<<" ";
	return 0; 
}

如果门和服务员的数量不是固定值100,而是输入,那么可以这样编写程序:

#include<iostream>
using namespace std;
int main()
{
	int n,i = 1,t = i*i;
	cin>>n;
	while(t<=n){
		cout<<t<<" ";
		i++;
		t = i*i;
	}
	return 0; 
}

其实也可以用for循环,因为for循环的第①、③部分可以是包含多条语句的逗号表达式,程序可以精简如下:

#include<iostream>
using namespace std;
int main()
{
	int n;
	cin>>n;
	for(int i=1,t=i*i;t<=n;i++,t=i*i)
		cout<<t<<" "; 
	return 0; 
}

数组例题——筛选法求素数

tangyj626阅读(406)

前面介绍过如何判断一个正整数是否是素数,如果我们要筛选出\([1,n]\)范围内的所有素数,前面介绍的方法是使用循环枚举每一个整数\(i\)并在循环体中使用循环来判断\(i\)是否为素数:

#include<iostream>
using namespace std; 
int main()
{
	int n;
	cin>>n;
	for(int i=2;i<=n;i++){
		int find = 0;
		for(int j=2;j*j<=i;j++){
			if(i%j==0){
				find = 1;
				break;
			}
		}
		if(find==0) cout<<i<<" ";
	} 
    return 0;
}

其实还有一种效率更高的方法——筛选法。具体步骤如下:

首先将\([1,n]\)(这里以\([1,100]\)为例)的所有整数书写出来:

首先划去2的倍数(不包括2):

然后划去3的倍数(同样不包括3):

接下来,因为4已经被划去了,不用考虑,然后划去5的倍数(不包括5):

接一下依次考虑后面的数,只要没被划掉,那就把这个数的倍数划去(不包括自身)。最后一个考虑的数是\(\sqrt{100}\),也就是10(不用考察到最后一个数100)。全部处理后结果如下:

可以使用数组来记录整数是否被划掉,根据上述步骤可以得出参考程序:

#include<iostream>
using namespace std;
const int N = 10000; 
int a[N+1];   //全局数组元素a[i]记录整数i是否被划掉,默认值为0,正好表示没有被划掉 
int main()
{
	int n;
	cin>>n;
	a[1] = 1;    //1不是素数,划掉
	for(int i=2;i*i<=n;i++){   //从2开始考察,一直到sqrt(n)
		//考察整数i
		if(a[i]==0){	//数字i没有被划掉 
			//把i的2倍,3倍,4倍,…,的数都划掉 
			for(int j=i+i;j<=N;j+=i){
				a[j]=1;
			}
		}
	}
	for(int i=1;i<=n;i++){
		if(a[i]==0) cout<<i<<" ";    //如果i没有被划掉,输出i
	}
    return 0;
}

其实还可以通过线性筛(欧拉筛)算法来提高程序执行效率,线性筛算法的思路是:从2到\(n\)的每一个整数(无论质数合数)\(i\),筛掉 小于\(i\)的最小质因子的所有质数 的\(i\)倍的数(由此可见数学理论对算法设计的重要性)。

例如筛选过程中考察到整数77,77分解质因数是\(7 \times 11\),最小质因子是7,那么需要筛掉所有小于7的质数(2、3、5)的77倍的那些整数,也就是筛掉\(2 \times 77\)、\(3 \times 77\)、\(5 \times 77\)。

#include<iostream>
using namespace std;
const int N = 10000; 
int a[N+1];   //全局数组元素a[i]记录整数i是否被筛去
int b[N+1];   //全局数组b记录所有素数 
int main()
{
	//cnt计数器,记录找到素数的个数
	int n,cnt = 0;
	cin>>n;
	for(int i=2;i<=n;i++){
		if(a[i]==0) b[cnt++] = i;  //i没有被筛掉,是素数,记录到数组b中
		
		//将记录素数的数组b每个元素的i倍筛掉 
		for(int j=0;j<cnt && i*b[j]<=n;j++){
			a[b[j]*i] = 1;        //筛掉b[j]的i倍
			if(i%b[j]==0) break;  //只考虑数组b中小于i的最小质因子的那些素数 
		}
	}
	for(int i=0;i<cnt;i++){
		cout<<b[i]<<" ";
	}
    return 0;
}

数组例题——斐波那契数列

tangyj626阅读(344)

在700多年前,意大利有一位著名数学家斐波那契在他的《算盘全集》一书中提出了这样一道有趣的兔子繁殖问题:兔子在出生两个月以后,就具有生殖后代的能力。假设一对兔子,每月都能生一对兔子,生出来的每一对小兔子,在出生两个月后,也每月生一对兔子。那么,由一对刚出生的小兔子开始,连续不断地繁殖下去,在某个指定的月份有多少对兔子?

通过表格来分析:

通过最后一行的分析,可知,第1、2个月后,兔子的对数都是1,从第3个月开始,兔子的对数是前面两个月的和。如果第\(n\)个月后兔子的对数用\(a_n\)来表示,用高中数列的知识来描述,有:

\( a_n=\left\{ \begin{aligned} 1(n=1,2) \\ a_{n-1}+a_{n-2}(n>=3) \end{aligned} \right. \)

我们可以借助三个变量来依次推导计算:

开始时:\(f1=1,f2=1\)

在计算过程中重复执行以下步骤:

  1. \(f3=f2+f1\)
  2. \(f1=f2\)
  3. \(f2=f3\)

注意第2步和第3步顺序不能交换!

这样依次推导的算法,称为递推算法

#include<iostream>
using namespace std;
int main()
{
	int n;
	long long f1,f2,f3;     //值增加较快,使用long long
	cin>>n;
	f1 = f2 = 1;
	
	if(n>=1) cout<<1<<":"<<f1<<endl;
	if(n>=2) cout<<2<<":"<<f2<<endl;
	
	for(int i=3;i<=n;i++){
		f3 = f2+f1;
		cout<<i<<":"<<f3<<endl;
		f1 = f2;
		f2 = f3;
	}
    return 0;
}

当然可以使用数组来解决问题:

#include<iostream>
using namespace std;
long long a[93] = {0,1,1};  //数组元素部分赋值 
int main()
{
	int n;
	cin>>n;
	for(int i=3;i<=n;i++) a[i] = a[i-1]+a[i-2];
	for(int i=1;i<=n;i++) cout<<i<<":"<<a[i]<<endl;
    return 0;
}

其实斐波那契数列有通项公式:

\(a_n = \dfrac{1}{\sqrt{5}}\left[\left({\dfrac{1+\sqrt{5}}{2}}\right)^n-\left({\dfrac{1-\sqrt{5}}{2}}\right)^n\ \right]\)

又称为“比内公式”,是用无理数表示有理数的一个范例。

数组例题——进制转换

tangyj626阅读(308)

以我们熟悉的十进制数为例,十进制数只有0、1、2、3、4、5、6、7、8、9这10个基本数字(也称为数码)。同样的,二进制数只有0、1两个数码,八进制数只有0、1、2、3、4、5、6、7这八个数码。

对于 \(n\) 进制数 \(m\),书写时如果某位上数字超过10,为了避免混淆,用大写字母来表示这个位上的数。A表示10、B表示11、C表示12,...,以此类推。那么十六进制数有0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F这16个数码。

一、十进制数转其他进制

十进制数转换成其他进制可以使用短除法:

↑ 十进制数53转二进制 ↑
↑ 十进制数51994转十六进制 ↑

输入\(m,n\)(\(0 \le m \le 1,000,000,2 \le n \le 16\)),输出十进制数\(m\)转\(n\)进制的结果。

分析:十进制数\(m\)转\(n\)进制,依据短除法,\(m\)每次除以\(n\),写下余数,将商重新赋值给\(m\),一直重复上面的操作直到\(m\)为0。将所余数反向书写就是结果。可以将余数依次存入到数组中,最后逆序输出即可。

#include<iostream>
using namespace std;
//char数组s用于输出短除法的余数对应的数码
//如果余数为0,那么对应的数码是s[0]——'0';
//如果余数为10,那么对应的数码是s[10]——'A' 
//如果余数为i,那么对应的数码是s[i]
char s[16] = {
	'0','1','2','3','4',
	'5','6','7','8','9',
	'A','B','C','D','E','F'
};
//数组r记录所有余数 
int r[1000];
int main()
{
    int m,n,cnt = 0;
    cin>>m>>n;
    
    while(m){       //while(m!=0){的简写
    	r[cnt++] = m%n;
    	m /= n;
	}
	
	if(cnt==0) r[cnt++] = 0;   //m为0特殊处理
	 
	for(int i=cnt-1;i>=0;i--){
		cout<<s[r[i]];    //注意这里的经典写法
		/*如果不理解上面的写法,可以看下面和上一句等效但简单一些的拆解写法
				int p = r[i];     //p是一个余数
				cout<<s[p]; 	  //输出p在s数组中对应的数码
				*/	 
	}
    return 0; 
}

二、其他进制数转十进制

可以使用“按权展开求和”将 \(n\) 进制数转换成十进制数:

例如二进制数10101,其实是:\(1 \times 2^4 + 0 \times 2^3+1 \times 2^2+0 \times 2^1+1 \times 2^0\) = 21;

又例如八进制数127,其实是:\(1 \times 8^2 + 2 \times 8^1 + 7 \times 8^0\) = 87;

又例如十六进制数A1F,其实就是:\(10 \times 16^2 + 1 \times 16^1 + 15 \times 16^0\) = 2591;

甚至对于十进制数12365,其实就是:\(1 \times 10^4 +2 \times 10^3 +3 \times 10^2 + 6 \times 10^1 + 5 \times 10^0\) = 12365。

问题:输入正整数 \(n\)(\(2 \le n \le 16\)),以及\(n\)进制数\(K\),输出其十进制数\(m\)。例如输入 16 FF,那么应该输出255。

解法一:将\(n\)进制数\(K\)按照字符的形式输入,每位是一个字符,存入到char数组中,使用按权展开求和的方法计算对应的十进制数(需要注意的是权值的处理):

#include<iostream>
using namespace std;
char num[1010];
int main()
{
    int n,cnt = 0;
    char one;
    cin>>n;
    
    //将n进制数每位当作字符存入字符数组 
    while(cin>>one){
        num[cnt++] = one;
    }
    
    long long m = 0,p = 1;
    //按权展开求和(低位 → 高位依次处理) 
    for(int i=cnt-1;i>=0;i--){
    	if(num[i]>='0' && num[i]<='9'){
    		m += p*(num[i]-'0');
		}else if(num[i]>='A' && num[i]<='F'){
    		m += p*(10+num[i]-'A');
		}
		//处理权值p 
		p *= n;
	}
	
    cout<<m;
    return 0; 
}

解法二:可以使用while(cin>>char)的方式输入\(n\)进制数\(K\)的每一位(当作字符输入),一边输入,一边组装十进制数\(m\):

#include<iostream>
using namespace std;
int main()
{
    int n;
    long long m = 0;
	char one;
	cin>>n;
	while(cin>>one){
		m = m*n;     //m放大n倍
		if(one>='0' && one<='9'){        //输入的是'0'~'9'中的某个字符
			m += one - '0';              //one-'0'就是该位数字
		}else if(one>='A' && one<='F'){  //输入的是'A'~'F'中的某个字符
			m += 10 + one - 'A';         //10+one-'A'就是该位数字
		}
	}
	cout<<m;
    return 0; 
}

二维数组和多维数组

tangyj626阅读(548)

前面介绍的都是一维数组,一维数组就像“一排容器”,能够存储若干相同数据类型的数据。对应的还有二维数组、三维数组……多维数组相当于一片容器的矩阵。

一、二维数组

前面介绍了一维数组就像“一排容器”,可以描述为一种“线性”的结构,那么二维数组就像一个有行有列的阵列(矩阵)。

1.二维数组的定义

定义二维数组的方法很简单:数组类型 数组变量名[第一维长度][第二维长度];,例如:

int a[3][5];
long long b[4][100],c[10000][100000];
double d[1001];
char strs[1001][101];

int a[3][5];为例,二维数组中的元素往往描述成有行有列的矩阵(虽然在内存中仍然是线性存储的),每个元素按照矩阵方式排列如下:

第0列第1列第2列第3列第4列
第0行a[0][0]a[0][1]a[0][2]a[0][3]a[0][4]
第1行a[1][0]a[1][1]a[1][2]a[1][3]a[1][4]
第2行a[2][0]a[2][1]a[2][2]a[2][3]a[2][4]

由此可知,定义二维数组时数组类型 数组变量名[第一维长度][第二维长度];,第一维长度对于矩阵的行,第二维长度对应矩阵的列。二维数组元素的使用仍然是使用下标,不过有两个[],两个[]中内容依次是第一维下标和第二维下标。

2.二维数组元素的使用

二维数组中的元素和一维数组的元素一样,可以理解成二维数组中的特殊变量,除了书写时比较特殊外,使用方法和普通变量一致:

int a[10][5];

a[0][0] = 100;
cin>>a[0][1];
a[0][2] = a[0][0]*a[0][1];
cout<<a[0][1]<<endl;
cout<<a[0][1]+a[0][2]<<endl;

//使用双重循环输入数据到数组中
for(int i=0;i<10;i++){
	for(int j=0;j<5;j++){
		cin>>a[i][j];
	}
}

//使用双重循环按照行列形式输出数组中的每个元素
for(int i=0;i<10;i++){
	for(int j=0;j<5;j++){
		cout<<a[i][j]<<" ";
	}
	cout<<endl;
}

二、二维数组例题

1. 鞍点的数量

分析:用二维数组来存储矩阵,使用双重循环遍历二维数组的每个元素,判断元素是否是鞍点(是所在行的最大值,也是所在列的最大值)并计数。

那么对于第\(i\)行\(j\)列的元素,如何判断是否是鞍点呢?可以扫描第\(i\)行的所有元素,看看能否找到比它大的元素,如果没有,那这个元素就是所在行的最大值。同理扫描第\(j\)列的所有元素,看看能否找到比它大的元素,如果没有,那这个元素就是所在列的最大值。

#include<iostream>
using namespace std;
int a[1010][1010];
int main()
{
	int N,M;

	cin>>N>>M;
	for(int i=1;i<=N;i++){
		for(int j=1;j<=M;j++){
			cin>>a[i][j];
		}
	}

	int ans = 0;
	//使用双重循环遍历二维数组,依次判断每个元素是否是鞍点
	for(int i=1;i<=N;i++){
		for(int j=1;j<=M;j++){
			//判断a[i][j]是第i行最大值
			bool f1 = true;
			for(int k=1;k<=M;k++){
				if(a[i][j]<a[i][k]){
					f1 = false;
					break;
				}
			}
			//判断a[i][j]是第j列最大值
			bool f2 = true;
			for(int k=1;k<=N;k++){
				if(a[i][j]<a[k][j]){
					f2 = false;
					break;
				}
			}
			//a[i][j]是鞍点
			if(f1==true && f2==true){
				ans++;
			}
		}
	}
	cout<<ans<<endl;
	return 0;
}

分析上面程序,可知算法时间复杂度是\(O(N*M*(N+M))\)。还有更高效的做法。可以计算出每行、每列的最大值存放到一维数组中,那么对于第\(i\)行\(j\)列的元素,如果值与第\(i\)行最大值相等并且值与第\(j\)列最大值相等,那么它就是一个鞍点。这样可以将算法时间复杂度优化到\(O(N*M)\)。

#include<iostream>
using namespace std;
int a[1010][1010];
//rowm记录每行最大值,colm记录每列最大值
int rowm[1010],colm[1010];
int main()
{
	int N,M;
	cin>>N>>M;
	for(int i=1;i<=N;i++){
		for(int j=1;j<=M;j++){
			cin>>a[i][j];
		}
	}
	//计算每行最大值(行优先遍历)
	for(int i=1;i<=N;i++){
		rowm[i] = a[i][1];
		for(int j=2;j<=M;j++){
			if(a[i][j]>rowm[i]) rowm[i] = a[i][j];
		}
	}
	//计算每列最大值(列优先遍历)
	for(int j=1;j<=M;j++){
		colm[j] = a[1][j];
		for(int i=2;i<=N;i++){
			if(a[i][j]>colm[j]) colm[j] = a[i][j];
		}
	}
	//使用双重循环遍历二维数组,依次判断每个元素是否是鞍点
	int ans = 0;
	for(int i=1;i<=N;i++){
		for(int j=1;j<=M;j++){
			if(a[i][j]==rowm[i] && a[i][j]==colm[j]){
				ans++;
			}
		}
	}
	cout<<ans<<endl;
	return 0;
}

2.买钢笔

分析:要买尽量多的铅笔,那么应该优先考虑多买 4 元钱的钢笔,剩余的钱数可能是0,1,2,3,可以使用多分支语句或者switch...case语句来进行分类讨论:

#include<iostream>
using namespace std;
int main()
{
    int x,a,b,c;
    cin>>x;
    c = x/4;
    a = b = 0;
    int mod = x%4;
    if(mod == 1){
    	b++;c--;
	}else if(mod == 2){
    	a++,c--;
	}else if(mod == 3){
    	a++,b++,c-=2;
	}
    cout<<a<<" "<<b<<" "<<c<<endl;
    return 0;
} 
#include<iostream>
using namespace std;
int main()
{
	int x,a,b,c;
	cin>>x;
	c = x/4;
	a = b = 0;
	switch(x%4){
		case 1:b++;c--;break;
		case 2:a++,c--;break;
		case 3:a++;b++;c-=2;break;
	}
	cout<<a<<" "<<b<<" "<<c<<endl;
	return 0;
} 

这里也可以使用数组来解决问题,此时甚至可以不使用分支结构(大家自行分析下面的程序):

#include<iostream>
using namespace std;
int main()
{
	int x;
	cin>>x;
	int ans[3] = {0,0,x/4};
	int change[4][3] = {
		{0,0,0},
		{0,1,-1},
		{1,0,-1},
		{1,1,-2},
	};
	int mod = x%4;
	for(int i=0;i<3;i++)
		cout<<ans[i]+change[mod][i]<<" ";
	cout<<endl;
	return 0;
} 

3.阵列方格

来看一个例题:给定正整数\(n,m,i,j\)(\(1 \le n,m \le 1000\),\(1 \le i \le n\),\(1 \le j \le m\)),在\(n\)行\(m\)列的方格阵列中,找出与第\(i\)行第\(j\)列方格(记为\((i,j)\),行列序号均从1开始计数)同行、同列、同对角线的所有方格(不包括\((i,j)\)自身),这样的方格很多,要求按照行列的顺序输出。

如左侧图解,\(n=9,m=7\),图中标识出了与\((3,6)\)这个方格同行、同列、同对角线的所有方格,那么输出的内容应该是:\((1,4)\)、\((1,6)\)、\((2,5)\)、\((2,6)\)、…、\((8,1)\)、\((8,6)\)、\((9,6)\)。

如果问题要求是先输出所用同行、然后输出同列,然后输出同对角线的方格,其实是可以使用先后的四个循环依次输出这些方格。但是题目要求的是按照行列的顺序输出,也就是四类方格是混在一起的,直接使用循环暂时没有思路,可以试试使用二维数组来处理。

我们可以使用二维数组int a[1001][1001];中的元素a[x][y]来标记方格阵列中第\(x\)行第\(y\)列方格\((x,y)\)是否满足条件。开数组的时候直接使用了最大数据规模,并且考虑了要使用的下标与自然序列一致(第一维长度、第二维长度都比数据规模多开1)。

最开始的时候将数组a每个元素都赋值为0,表示每个方格都不符合条件,然后将与方格\((i,j)\)同行、同列、同对角线的方格对应的数组元素都标记成1。最后使用双重循环按照行列的顺序依次筛选输出值为1的数组元素对应的方格坐标即可。

同行同列的方格容易找出来,对角线上的方格如何找出来呢?

思路一:分别查找标记方格\((i,j)\)左侧对角线和右侧对角线上的方格:

#include<iostream>
using namespace std;
int a[1001][1001]; //全局int类型数组,默认每个元素值都是0,正好表示筛选前都不符合条件 
int main()
{
    int n,m,i,j;
    cin>>n>>m>>i>>j;
    
    //标记同行方格满足条件
    for(int y=1;y<=m;y++) a[i][y] = 1;
    
    //标记同列方格满足条件
    for(int x=1;x<=n;x++) a[x][j] = 1;
    
    //左侧对角线上的方格(从第j-1列依次到第1列)
	for(int y=j-1;y>=1;y--){
		int d = j-y;
		int x = i+d;	//第【y】列对角线【左下方位】的方格x坐标 
		if(x>=1 && x<=n) a[x][y] = 1;
        x = i-d;		//第【y】列对角线【左上方位】的方格x坐标 
        if(x>=1 && x<=n) a[x][y] = 1;
	}

    //右侧对角线上的方格(从第j+1列依次到第m列)
	for(int y=j+1;y<=m;y++){
		int d = y-j;
		int x = i+d;	//第【y】列对角线【右下方位】的方格x坐标 
		if(x>=1 && x<=n) a[x][y] = 1;
        x = i-d;		//第【y】列对角线【右上方位】的方格x坐标
        if(x>=1 && x<=n) a[x][y] = 1;
	}
    
    a[i][j] = 0;     //本身不输出,排除掉(前面被标记过)
    
    //查找数组值为1的元素,输出对应方格坐标 
    for(int x=1;x<=n;x++){
        for(int y=1;y<=m;y++){
            if(a[x][y]==1) cout<<"("<<x<<","<<y<<")"<<" ";
        }
        cout<<endl;
    } 
    return 0;
}

思路二:从第一列开始依次查找标记每列上对角线上的方格:

#include<iostream>
using namespace std;
int a[1001][1001]; //全局int类型数组,默认每个元素值都是0,正好表示筛选前都不符合条件 
int main()
{
    int n,m,i,j;
    cin>>n>>m>>i>>j;
    
    //标记同行方格满足条件
    for(int y=1;y<=m;y++) a[i][y] = 1;
    
    //标记同列方格满足条件
    for(int x=1;x<=n;x++) a[x][j] = 1;
    
    //从第1列开始查找每一列上满足条件的方格(在对角线上) 
    for(int y=1;y<=m;y++){
    	int d = y-j;
    	int x = i+d; 
    	if(x>=1 && x<=n) a[x][y] = 1;
    	x = i-d;
    	if(x>=1 && x<=n) a[x][y] = 1;
	}
    
    a[i][j] = 0;     //本身不输出,排除掉(前面被标记过)
    
    //查找数组值为1的元素,输出对应方格坐标 
    for(int x=1;x<=n;x++){
        for(int y=1;y<=m;y++){
            if(a[x][y]==1) cout<<"("<<x<<","<<y<<")"<<" ";
        }
        cout<<endl;
    } 
    return 0;
}

其实思路一仍然是依次查找了每列对角线上的方格,只不过分成了左侧和右侧两部分来分别查找。思路二则将两部分整合,直接从第一列依次查找每列对角线上的方格。可以先看右侧对角线第\(y\)列的两个坐标点:

上面的计算方法其实对于左边对角线上的点同样适用。

可以通过右侧的图解分析可知:

设列标为\(y\),记\(d = y-j\),那么可知第\(y\)列\((i,j)\)对角线上的两个方格的行坐标应该是\(i+d\)和\(i-d\),还需要考虑行坐标是否在\([1,n]\)范围内。

这个时候我们猛然发现,每一列上满足条件的方格其实是可以计算出来的!同样地,每一行上满足条件的方格也能计算出来。

通过上面的分析可知这个问题不用数组也能解决,通过查找规律可以借助循环一行一行地找出每行符合条件的方格:

#include<iostream>
using namespace std;
int main()
{
	int n,m,i,j;
	cin>>n>>m>>i>>j;
	
	//依次查找每一行符合条件的方格 
	for(int x=1;x<=n;x++){         //x是行标 
		if(x!=i){				  //不是第i行 
			int d = i-x;
			if(d<0) d = -d;		//d是第i行与第x行的距离
			//通过分析查找规律可知第x行在(x,j)左侧且在(i,j)对角线上的方格是(x,j-d)
			//第x行在(x,j)右侧且在(i,j)对角线上的方格是(x,j+d)

			//考查对角线上的方格(x,j-d)是否在阵列范围内(没有越界) 
			if(j-d>=1) cout<<"("<<x<<","<<j-d<<") ";
			
			cout<<"("<<x<<","<<j<<") ";    //(x,j)与(i,j)同列 
			
			//考查对角线上的方格(x,j+d)是否在阵列范围内(没有越界)
			if(j+d<=m) cout<<"("<<x<<","<<j+d<<") "; 
		}else{					//第i行
			//除了(x,j)都输出:因为x==i,(x,j)就是(i,j) 
			for(int y=1;y<=m;y++){
				if(y!=j) cout<<"("<<x<<","<<y<<") "; 
			}
		}
		cout<<endl;
	}
    return 0;
}

4.洛谷P5728 旗鼓相当的对手

分析:使用二维数组int a[1000][4];保存每位同学三门科目的考试成绩和总成绩,每位同学的成绩占二维数组一行。同学两两组合考虑是否是旗鼓相当的对手,为了避免重复考虑,对于第0~\(N-2\)的第\(i\)位同学,考虑并统计他与第\(i+1\)~\(N-1\)的每位同学是否是旗鼓相当的对手即可。

#include<iostream>
#include<cmath>
using namespace std;
int a[1000][4]; 
int main()
{
    int N,s1,s2,ans = 0;
    cin>>N;
    for(int i=0;i<N;i++){
    	cin>>a[i][0]>>a[i][1]>>a[i][2];
    	a[i][3] = a[i][0]+a[i][1]+a[i][2];	//计算总分 
	}
	for(int i=0;i<N-1;i++){
		for(int j=i+1;j<N;j++){
			bool f = abs(a[i][3]-a[j][3])<=10;       //总分之差不大于10
			for(int k=0;f && k<3;k++){               //借助循环判断3门学科
				f = f && (abs(a[i][k]-a[j][k])<=5);    //每门学科成绩之差不大于5
			}
			if(f) ans++;
		}
	}
	cout<<ans;
    return 0;
}

代码中判断“旗鼓相当”的语句大家可以仔细琢磨:

bool f = abs(a[i][3]-a[j][3])<=10;           //总分之差不大于10
for(int k=0;f && k<3;k++){                   //借助循环判断3门学科
	f = f && abs(a[i][k]-a[j][k])<=5;        //每门学科成绩之差不大于5
}
if(f) ans++;

甚至还可以进一步让该“旗鼓相当”的判断更加工整:

#include<iostream>
#include<cmath>
using namespace std;
int a[1000][4];
int b[4] = {5,5,5,10};     //记录每门学科最大差值和总分最大差值
int main()
{
    int N,s1,s2,ans = 0;
    cin>>N;
    for(int i=0;i<N;i++){
    	cin>>a[i][0]>>a[i][1]>>a[i][2];
    	a[i][3] = a[i][0]+a[i][1]+a[i][2];	//计算总分 
	}
	for(int i=0;i<N-1;i++){
		for(int j=i+1;j<N;j++){
			bool f = true;
			for(int k=0;f && k<4;k++){
				f = f && (abs(a[i][k]-a[j][k])<=b[k]);
			}
			if(f) ans++;
		}
	}
	cout<<ans;
    return 0;
}

如果不易理解,其实上面判断“旗鼓相当”的代码也可以写成下面虽然繁琐但简单的代码:

if(abs(a[i][0]-a[j][0])<=5 && abs(a[i][1]-a[j][1])<=5 && abs(a[i][2]-a[j][2])<=5 && abs(a[i][3]-a[j][3])<=10) ans++;

三、多维数组

回顾一维数组、二维数组的定义和元素的使用方法,不难得出更高维数组的定义和元素的使用方法。

一维数组的使用举例

int a[11];
a[1] = 123;
cin>>a[2];
a[3] = a[1]*a[2];
cout<<a[3];
cout<<a[3]*a[1];

//使用循环输入数据到数组中
for(int i=1;i<=10;i++){
	cin>>a[i];
}

//使用循环输出数组中的每个元素
for(int i=1;i<=10;i++){
	cout<<a[i]<<" ";
}

二维数组的使用举例

int a[10][5];

a[0][0] = 100;
cin>>a[0][1];
a[0][2] = a[0][0]*a[0][1];
cout<<a[0][1]<<endl;
cout<<a[0][1]+a[0][2]<<endl;

//使用双重循环输入数据到数组中
for(int i=0;i<10;i++){
    for(int j=0;j<5;j++){
        cin>>a[i][j];
    }
}

//使用双重循环按照行列形式输出数组中的每个元素
for(int i=0;i<10;i++){
    for(int j=0;j<5;j++){
        cout<<a[i][j]<<" ";
    }
    cout<<endl;
}

通过一个例子来看三维数组的使用:洛谷P5729 工艺品制作

分析:使用三维数组记录每个小方块是否被蒸发(最开始都没有被蒸发,元素都标记为0),对于每一次操作,相当于把一个小的立方体中的每个小方块蒸发掉(标记为1)。所有操作完成后,统计三维数组中值为0的元素的个数即可。

#include<iostream>
#include<cmath>
using namespace std;
//三维全局数组v,元素v[i][j][k]记录坐标点(i,j,k)小方块是否被蒸发掉
//全局数组所有元素默认值为0,正好表示开始时都没有被蒸发
int v[21][21][21]; 
int main()
{
	int w,x,h,q,ans = 0;
	int x1,y1,z1,x2,y2,z2;
	cin>>w>>x>>h;
	cin>>q;
	while(q--){		//q次重复操作 
		cin>>x1>>y1>>z1>>x2>>y2>>z2;
		//坐标范围描述的小立方体中的每个小方块都被蒸发掉,使用三重循环处理
		for(int i=x1;i<=x2;i++){
			for(int j=y1;j<=y2;j++){
				for(int k=z1;k<=z2;k++){
					v[i][j][k] = 1;     //标记(i,j,k)小方块被蒸发掉 
				}
			}
		}
	}
	for(int i=1;i<=w;i++){
		for(int j=1;j<=x;j++){
			for(int k=1;k<=h;k++){
				ans += 1-v[i][j][k];    //统计未蒸发小方块数量 
			}
		}
	}
	cout<<ans;
    return 0;
}

数组例题——杨辉三角

tangyj626阅读(309)

杨辉三角,是二项式系数在三角形中的一种几何排列。在欧洲,这个表叫做帕斯卡三角形。帕斯卡(1623——1662)是在1654年发现这一规律的,比杨辉要迟393年,比贾宪迟600年。杨辉三角是中国古代数学的杰出研究成果之一,它把二项式系数图形化,把组合数内在的一些代数性质直观地从图形中体现出来,是一种离散型的数与形的优美结合。

现在要输出杨辉三角形前\(n\)行(\(1 \le n \le 20\))。

分析:杨辉三角形每一行第1个和最后一个数都是1,中间的数是上一行“肩上”两数的和。可以使用二维数组a存储杨辉三角形,第\(i\)行第\(j\)个数字存储在数组元素a[i][j]中,可知当j==1 或者 j==i时a[i][j]=1;否则a[i][j] = a[i-1][j-1]+a[i-1][j];

#include<iostream>
using namespace std;
int a[21][21];
int main()
{
	int n;
	cin>>n;
	 
    a[1][1] = 1;	//第1行
	//使用循环计算第2~n行 
    for(int i=2;i<=n;i++){
    	//每行第1个和最后一个数
    	a[i][1] = a[i][i] = 1;
    	//使用循环计算第i行第2个~倒数第二个数 
    	for(int j=2;j<i;j++){
    		a[i][j] = a[i-1][j-1] + a[i-1][j];
		}
	}
	
	//输出保存在二维数组a中的结果 
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			cout<<a[i][j]<<" ";
		}
		cout<<endl;
	}
    return 0; 
}

其实每行第1个和最后一个数的计算也能放到内层循环中,原因通过下图分析可知:

因为a[i-1][0]是0,所以a[i][1] = a[i-1][0]+a[i-1][1]也能用来计算a[i][1];同样的,因为a[i-1][i]是0,所以a[i][i] = a[i-1][i-1]+a[i-1][i]也能用来计算a[i][i]。上面的程序可以精简为:

#include<iostream>
using namespace std;
int a[21][21];
int main()
{
	int n;
	cin>>n;
	 
    a[1][1] = 1;	//第1行
	//使用循环计算第2~n行 
    for(int i=2;i<=n;i++){
    	//使用循环计算第i行每个数 
    	for(int j=1;j<=i;j++){
    		a[i][j] = a[i-1][j-1] + a[i-1][j];
		}
	}
	
	//输出保存在二维数组a中的结果 
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			cout<<a[i][j]<<" ";
		}
		cout<<endl;
	}
    return 0; 
}

再来看例题:T137077 杨辉三角

不少同学马上想到的是模拟整个过程:

#include<iostream>
using namespace std;
long long a[91][91];   //long long 
int main()
{
	int N,n;
	cin>>N;
	while(N--){         //N次循环
		cin>>n;
		
		a[1][1] = 1;    //第1行
	    //使用循环计算第2~n行 
	    for(int i=2;i<=n;i++){
	        //使用循环计算第i行每个数 
	        for(int j=1;j<=i;j++){
	            a[i][j] = a[i-1][j-1] + a[i-1][j];
	        }
	    }
	    
	    //输出第n行 
	    for(int i=1;i<=n;i++){
	    	cout<<a[n][i]<<" ";
		}
		cout<<endl;
	} 
    return 0; 
}

这样做算法时间复杂度是\(O(n^3)\),并且出现了大量重复的计算:就拿测试样例来说,第1次计算第1行,第2次计算第2行,第3次计算第3行,第4次计算第4行,第5次计算第5行,每次计算都是从第1行到要求输出的行,出现了大量重复的计算。怎么来避免重复计算呢?

可以先输入所有要计算的行存储在数组b中,并求出最大值\(max\),那么只需要用一个双重循环计算杨辉三角的第1~\(max\)行即可,计算完成后,杨辉三角的第1~\(max\)行都已经计算出来保存到二维数组a中了,那么根据数组b中的元素输出二维数组a中对应行的元素即可,这样做可以将算法时间复杂度降低到是\(O(n^2)\):

#include<iostream>
using namespace std;
const int MAX = 91; 
long long a[MAX][MAX];
int b[MAX];
int main(){
	int N,max = 0;
	cin>>N;
	
	//计算出要输出的所有行编号的最大值为max 
	for(int i=1;i<=N;i++){
		cin>>b[i];		//行编号保存到数组b中 
		if(b[i]>max) max = b[i];
	}
	
	//计算杨辉三角的1~max行 
	a[1][1] = 1;
	for(int i=2;i<=max;i++){   //从第2行起开始计算每一行的数据
		for(int j=1;j<=i;j++) a[i][j] = a[i-1][j-1]+a[i-1][j];
	}
	
	//打印杨辉三角要输出的行(要输出的行编号在数组b里存储) 
	for(int i=1;i<=N;i++){
		//b[i]是要输出的行编号,这一行一共有b[i]个整数 
		for(int j=1;j<=b[i];j++) cout<<a[b[i]][j]<<" ";
		//注意体会上面的 a[b[i]][j]。相当于 int n = b[i];cout<<a[n][j]<<" "; 
		cout<<endl;
	}
	return 0;
} 

其实还可以在处理输入的每一个行数的过程中,“动态地”生成杨辉三角:

#include<iostream>
using namespace std;
const int MAX = 91;
long long a[MAX][MAX];
int main()
{
	a[1][1] = 1;
	int n,m,line = 1;	//line记录杨辉三角计算到第几行了 
	cin>>n;
	for(int k=1;k<=n;k++){
		cin>>m;
		if(m>line){		//要输出的行数m>line,表明第m行还没有计算过
			//依次计算杨辉三角的第(line+1)行到第m行 
			for(int i=line+1;i<=m;i++){
				for(int j=1;j<=i;j++){
					a[i][j] = a[i-1][j-1]+a[i-1][j];
				}
			}
			//已经计算到了第m行,更新line 
			line = m;
		}
		//输出杨辉三角第m行内容 
		for(int j=1;j<=m;j++) cout<<a[m][j]<<" ";
		cout<<endl;
	}
	return 0;
}

兴趣扩展:杨辉三角形与二项展开式系数的关系

二项展开式

\((x+y)^n = C_0x^n+C_1x^{n-1}y+C_2x^{n-2}y^2+...+C_ix^{n-i}y^i+...+C_{n-1}xy^{n-1}+C_ny^n\)

等式右边的常数\(C_0,C_1,C_2,...,C_i,...,C_{n-1},C_n\)称为二项式系数,这些常数正好是上述杨辉三角第\(n+1\)行的内容(如果杨辉三角行数从0开始计数,那么正好是杨辉三角第\(n\)行的内容)。

\((x+y)^0=1\)
\((x+y)^1=x+y\)
\((x+y)^2=x^2+2xy+y^2\)
\((x+y)^3=x^3+3x^2y+3xy^2+y^3\)
\((x+y)^4=x^4+4x^3y+6x^2y^2+4xy^3+y^4\)
\((x+y)^5=x^5+5x^4y+10x^3y^2+10x^2y^3+5xy^4+y^5\)

数组例题——蛇形填充方阵

tangyj626阅读(343)

给定一个\(n \times n\)的方阵,从右上角开始,依次用1、2、3、…按照蛇形填充方阵的每个方格。以\(n=3,4,5\)为例,填充结果如下:

n=3
n=4
n=5

输入正整数\(n\)(\(1 \le n \le 9\)),输出\(n \times n\)方阵蛇形填充结果。

分析:要通过找规律的方式得出阵列第\(x\)行第\(y\)列的值太困难,可以通过模拟蛇形填充的过程将阵列每个方格的数填充上,当然需要将阵列中的数存储到二维数组中。

一共需要将1~\(n \times n\)一共\(n^2\)个整数填充到二维数组中,所以循环的条件不难得出。填充由坐标点\((1,n)\)开始,下一个坐标点应该由当前坐标点推导出来,填充的数字用计数器记录。

再来细看蛇形填充的过程,填充过程是有规律地重复,最基本的一个轮次是“向下填写→向左填充→向上填充→向右填充”,剩下的都是这样的重复过程,只不过最后一个轮次可能不是完整的四个方向填充都有。

那么朝一个方向持续填充的条件是什么呢?首先想到的是填充的方格的个数,但是分析发现个数的上限并不好找,并且在同一轮次四个方向填充的个数可能都不同。我们先不看第一轮次四个方向的填充,考查第二轮次的填充,以向下填充为例,如下图所示,填充的条件是填充方向上遇到的方格的值是默认值0,只要遇到了非0值(表示这个格子之前已经填充过),那么就要“拐弯”向另一个方向填充了:

可知从第二轮次开始都是这样的填充规则,只有最外面的第一轮次不满足(其实是第一轮次的前三个方向不满足),但是我们可以人为设置三个关键点的值来让第一轮次的填充也满足填充规则:

经过上面的分析:可以得出参考程序代码:

#include<cstdio>
const int N = 9;
int a[N+2][N+2];   //思考为什么不是N+1? 
int main()
{
    int n;
    scanf("%d",&n);
    //标记三个特殊点的值为非0 
    a[n+1][n] = a[n][0] = a[0][1] = -1;
    
    int cnt = 0;	  //计数器记录要填充的数
    
	//x,y是填充点的坐标:x行y列,填充过程中一直动态记录填充点的坐标
	//最开始时在第0行n列,那么进入循环首先向下填充第一个填充点正好是方阵右上角方格
	int x = 0,y = n;
    while(cnt<n*n){
    	//向下填充:注意++x和++cnt是先自加再使用变量 
    	while(!a[x+1][y]) a[++x][y] = ++cnt;    //相当于cnt++;x++;a[x][y]=cnt;
    	//向左填充
    	while(!a[x][y-1]) a[x][--y] = ++cnt;
    	//向上填充
    	while(!a[x-1][y]) a[--x][y] = ++cnt;
    	//向右填充
    	while(!a[x][y+1]) a[x][++y] = ++cnt;
	}
	
	//输出结果 
	for(int x=1;x<=n;x++){
		for(int y=1;y<=n;y++){
			printf("%2d ",a[x][y]);
		}
		printf("\n");
	}
    return 0; 
}

循环体中有四个先后顺序的循环来实现四个方向的依次填充。前面分析最后一个轮次可能不是完整的四个方向的填充,程序这里也能做到,这个时候后面方向实现填充功能的while循环条件一上来循环条件就不成立。

拓展练习

  1. T137970 考场座位编排
  2. T137880 蛇形方阵升级版
  3. T137973 考场座位编排Plus

数组例题——约瑟夫问题

tangyj626阅读(281)

据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个山洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

我们将问题简化,\(n\)个人围成一个圆圈,每个人的编号依次是1、2、3、……、\(n\)。从第一个人开始数数,数到\(m\)的人出圈。然后由出圈的下一个人重新数数,数到\(m\)的人又出圈……输入\(n\)和\(m\)(\(1 \le n \le 100,1 \le m \le 1,000,000\)),依次输出出圈人的编号。

一、借助数组记录圈里人的编号

最朴素的做法,借助数组记录圈里人的编号。模拟整个过程,有人出圈就将数组对应位置元素删除,可知模拟过程中整个数组实时记录了圈里人的所有编号。此外还需要借助一个变量模拟数数的过程,特别是有人出圈后,下一个人继续数数(而不是重新数数)。这里给出比较原始的参考程序:

#include<iostream>
using namespace std;
const int N = 100;
//数组a记录圈里人的编号 
int a[N+1];
int main()
{
	int n,m,i,j,p;
	cin>>n>>m;

	//开始时n个人都在圈里,第i个人的编号i存储在a[i]中 
	for(i=1;i<=n;i++) a[i] = i;

	//变量p用来记录数数的人对应数组中的下标
	//初始值设置为0(进入模拟过程执行p++,正好第1个人数1) 
	p = 0;

	//n次循环n个人依次出圈,控制变量i正好是圈里人的数量 
	for(i=n;i>=1;i--){
		//当前圈里有i个人,从编号p的人开始,m个人依次数数 
		for(j=1;j<=m;j++){
			p++;
			//发现是第(i+1)个人数数,修正成第1个人数数(构成一个圈) 
			if(p==i+1) p=1; 
		}
		
		cout<<a[p]<<" ";
		
		//删除数组元素a[p](数组依次前移1位,覆盖掉a[p]) 
		for(j=p;j<i;j++) a[j] = a[j+1];
		
		//出圈后,下次数数要从新的a[p]开始,执行p--
		//这样下次数数第一次执行p++时,正好这个位置的人数1 
		p--;
	}
	return 0; 
}

很容易发现,每次m个人数数,其实可以不用一个循环来模拟,这里是可以直接计算出来出圈人的下标:

#include<iostream>
using namespace std;
const int N = 100;
//数组a记录圈里人的编号 
int a[N+1];
int main()
{
    int n,m,i,j,p;
    cin>>n>>m;

    //开始时n个人都在圈里,第i个人的编号i存储在a[i]中 
    for(i=1;i<=n;i++) a[i] = i;

    //变量p用来记录数数的人对应数组中的下标
    //初始值设置为0(进入模拟过程执行p++,正好第1个人数1) 
    p = 0;

    //n次循环n个人依次出圈,控制变量i正好是圈里人的数量 
    for(i=n;i>=1;i--){
        //当前圈里有i个人,从编号p的人开始,m个人依次数数
		//计算本次出圈(数到m的人)的下标p 
        p = (p+m)%i;
        if(p==0) p = i;
        
        cout<<a[p]<<" ";
        
        //删除数组元素a[p](数组依次前移1位,覆盖掉a[p]) 
        for(j=p;j<i;j++) a[j] = a[j+1];
        
        //出圈后,下次数数要从新的a[p]开始,执行p--
        //这样下次数数第一次执行p++时,正好这个位置的人数1 
        p--;
    }
    return 0; 
}

上面的代码计算本次出圈人的下标使用的核心代码p = (k+m)%i; 使用了模运算,原因是随着出列人数的增多,\(k+m\)会大于\(i\)。其实原始数据输入时也有可能\(m\)远大于\(n\),即使是第1段示例代码借助循环来模拟每次数数,也完全不用每次数到\(m\)。

二、借助数组记录每个人的状态(是否出圈)

上面使用数组来记录圈里人的编号,当有人出圈时,要借助大量的数组元素前移来删除数组元素,降低了效率。其实我们还可以用数组来记录每个人的状态,a[p]记录编号为p的人是否出圈,0表示没有出圈,1表示出圈。此时要模拟整个过程,对于数组里的每个元素都要考察,发现没有出圈才数数。可以在循环里嵌套一个循环,一次性把这一轮出圈的人计算出来:

#include<iostream>
using namespace std;
const int N = 100;
//a[p]记录编号为p的人是否已经出圈:0-没有出圈,1-已经出圈
//全局数组a所有元素会被自动初始化成0,也就表示最开始时所有人都没有出圈(都在圈里) 
int a[N+1];
int main()
{
    int n,m;
    cin>>n>>m;

    //变量p记录正在考察的人的编号
    //注意每个人(不管有没有出圈)都要考察,但是只有还在圈里的人才数数
    //p初始值设置成0,后面一进循环p++,正好考察编号为1的人
    int p = 0;

	for(int i=n;i>=1;i--){    //n次循环,每次出圈一人
        int tot = 0;    //tot记录数数数到几
        while(tot!=m){ 
            p++;                //考察下一个人 
            if(p>n) p=1;        //回到编号为1的人
            if(a[p] == 0) {     //编号为p的人还在圈里
                tot++;
            }
        }
        //出循环后,编号为p的人数到了m 
        a[p] = 1;            //记录编号为p的人出圈
        cout<<p<<" ";        //编号为p的人出圈,输出p
    }
    return 0; 
}

m如果比当前圈内剩余人数(n-p)大很多,这里会数好多圈,效率低下,可以使用模运算来提高效率:

#include<iostream>
using namespace std;
const int N = 100;
//a[p]记录编号为p的人是否已经出圈:0-没有出圈,1-已经出圈
//全局数组a所有元素会被自动初始化成0,也就表示最开始时所有人都没有出圈(都在圈里) 
int a[N+1];
int main()
{
    int n,m;
    cin>>n>>m;

    //变量p记录正在考察的人的编号
    //注意每个人(不管有没有出圈)都要考察,但是只有还在圈里的人才数数
    //p初始值设置成0,后面一进循环p++,正好考察编号为1的人
    int p = 0;

	for(int i=n;i>=1;i--){    //n次循环,每次出圈一人
		//md是修正后的本轮次要数到多少结束 
        int md = m%i;		//i是当前还在圈里的人数 
        if(md==0) md = i; 
        
        int tot = 0;    //tot记录数数数到几
        while(tot!=md){ 
            p++;                //考察下一个人 
            if(p>n) p=1;        //回到编号为1的人
            if(a[p] == 0) {     //编号为p的人还在圈里
                tot++;
            }
        }
        //出循环后,编号为p的人数到了m 
        a[p] = 1;            //记录编号为p的人出圈
        cout<<p<<" ";        //编号为p的人出圈,输出p
    }
    return 0; 
}

这样的处理方式避免了数组大量元素的移动,但是数组的每个元素(即使对应的人已经出圈)都要考虑是否数数,同样会耗费大量的时间。如果不用考察已经出圈的人,程序的效率还可以进一步提升,更优化的程序我们在后面的章节会介绍到。