第1章 程序设计入门

292 阅读37分钟

第1章 程序设计入门

学习目标

  • 熟悉C语言程序的编译和运行
  • 学会编程计算并输出常见的算术表达式的结果
  • 掌握整数和浮点数的含义和输出方法
  • 掌握数学函数的使用方法
  • 初步了解变量的含义
  • 掌握整数和浮点数变量的声明方法
  • 掌握整数和浮点数变量的读入方法
  • 掌握变量交换的三变量法
  • 理解算法竞赛中的程序三步曲:输入、计算、输出
  • 记住算法竞赛的目标及其对程序的要求

计算机速度快,很适合做计算和逻辑判断工作。本章首先介绍顺序结构程序设计,其基本思路是:把需要计算机完成的工作分成若干个步骤,然后依次让计算机执行。注意这里的“依次”二字——步骤之间是有先后顺序的。这部分的重点在于计算。

接下来介绍分支结构程序设计,用到了逻辑判断,根据不同情况执行不同语句。本章内容不复杂,但是不容忽视。

注意:编程不是看会的,也不是听会的,而是练会的,所以应尽量在计算机旁阅读本书,以便把书中的程序输入到计算机中进行调试,顺便再做做上机练习。千万不要图快——如果没有足够的时间用来实践,那么学得快,忘得也快。

1.1 算术表达式

提示1-1: 整数值用%d输出,实数用%f输出。

运算符“/”其实是“多面手”,它既可以做整数除法,又可以做浮点数除法

(1) 但也有不少语言会严格区分整数除法和浮点数除法。

提示1-2: 整数/整数=整数,浮点数/浮点数=浮点数。


整数-浮点数=浮点数。但其实这个说法并不准确。确切的说法是:整数先“变”成浮点数,然后浮点数-浮点数=浮点数

1.2 变量及其输入

#include<stdio.h>
int main()
{
  int a, b;
  scanf("%d%d", &a, &b);
  printf("%d\n", a+b);
  return 0;
}

第一条语句“int a, b”声明了两个整型(即整数类型)变量a和b,然后读取键盘输入,并放到a和b中。

注意a和b前面的“&”符号——千万不要漏掉,不信可以试试

(2) 在学习编程时,“明知故犯”是有益的:起码你知道了错误时的现象。这样,当真的不小心犯错时,可以通过现象猜测到可能的原因。

提示1-3: scanf中的占位符和变量的数据类型应一一对应,且每个变量前需要加“&”符号。

可以暂时把变量理解成“存放值的场所”,或者形象地认为每个变量都是一个盒子、瓶子或箱子。在C语言中,变量有自己的数据类型


==大多数的算法竞赛包含如下一些相同的“游戏规则”。==

  • 首先,选手程序的执行是自动完成的,没有人工干预。不要在用户输入之前打印提示信息(例如“Please input n:”)

  • 其次,不要让程序“按任意键退出”(例如,调用system("pause"),或者添加一个多余的getchar()),因为不会有人来“按任意键”的。

提示1-4: 在算法竞赛中,输入前不要打印提示信息。输出完毕后应立即终止程序,不要等待用户按键,因为输入输出过程都是自动的,没有人工干预。

提示1-5: 在算法竞赛中不要使用头文件conio.h,包括getch()、clrscr()等函数。

  • 最后,最容易忽略的是输出的格式:在很多情况下,输出格式是非常严格的,多一个或者少一个字符都是不可以的!C 语言格式说明符

提示1-6: 在算法竞赛中,每行输出均应以回车符结束,包括最后一行。除非特别说明,每行的行首不应有空格,但行末通常可以有多余空格。另外,输出的每两个数或者字符串之间应以单个空格隔开。

总结一下,算法竞赛的程序应当只做3件事情:读入数据、计算结果、打印输出。不要打印提示信息,不要在打印输出后“暂停程序”,更不要尝试画图、访问网络等与算法无关的任务。

const double pi = acos(-1.0);。这里也声明了一个叫pi的“符号”,但是const关键字表明它的值是不可以改变的——pi是一个真正的数学常数

(3) 有的读者可能会有math.h中定义的常量M_PL,但其实这个常数不是ANSIC标准的。不信可以用gcc-ansi编译试试。

提示1-7: 尽量用const关键字声明常数。

提示1-8: 赋值是个动作,先计算右边的值,再赋给左边的变量,覆盖它原来的值。

printf("Area = %.3f\n", s),该语句的用法很容易被猜到:只有以“%”开头的部分才会被后面的值替换掉,其他部分原样输出。

提示1-9: printf的格式字符串中可以包含其他可打印符号,打印时原样输出。

1.3 顺序结构程序设计

例题1-2 三位数反转

输入一个三位数,分离出它的百位、十位和个位,反转后输出。

样例输入:

127

样例输出:

721

程序1-6 三位数反转(1)

#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
    cout << n % 10 << n / 10 % 10 << n / 100;
    return 0;
}

此题有一个没有说清楚的细节,即:如果个位是0,反转后应该输出吗?例如,输入是520,输出是025还是25?如果在算法竞赛中遇到这样的问题,可向监考人员询问

(4) 如果是网络竞赛,还可以向组织者发信,在论坛中提问或者拨打热门电话。

。但是在这里,两种情况的处理方法都应学会。

提示1-10: 算法竞赛的题目应当是严密的,各种情况下的输出均应有严格规定。如果在比赛中发现题目有漏洞,应向相关人员询问,尽量不要自己随意假定。

上面的程序输出025,但要改成输出25似乎会比较麻烦——必须判断n%10是不是0,但目前还没有学到“根据不同情况执行不同指令”(分支结构程序设计是1.4节的主题)。

一个解决方法是在输出前把结果存储在变量m中。这样,直接用%d格式输出m,将输出25。要输出025也很容易,把输出格式变为%03d即可。

程序1-7 三位数反转(2)

#include<stdio.h>
int main()
{
  int n, m;
  scanf("%d", &n);
  m = (n%10)*100 + (n/10%10)*10 + (n/100);
  printf("%03d\n", m);
  return 0;
}

例题1-3 交换变量

输入两个整数a和b,交换二者的值,然后输出。

样例输入:

824 16

样例输出:

16 824

程序1-8 变量交换(1)

如何交换两个变量呢?最经典的方法是三变量法

#include<stdio.h>
int main()
{
  int a, b, t;
  scanf("%d%d", &a, &b);
  t = a;
  a = b;
  b = t;
  printf("%d %d\n", a, b);
  return 0;
}

提示1-11: 赋值a=b之后,变量a原来的值被覆盖,而b的值不变。

另一个方法没有借助任何变量,但是较难理解:

程序1-9 变量交换(2)

#include<stdio.h>
int main()
{
  int a, b;
  scanf("%d%d", &a, &b);
  a = a + b;
  b = a - b;
  a = a - b;
  printf("%d %d\n", a, b);
  return 0;
}

在顺序结构程序中,程序一条一条依次执行。为了避免值和变量名混淆,假定用户输入的是a0和b0,因此scanf语句执行完后a=a₀,b=b₀。

执行完a=a+b后:a=a₀+b₀,b=b₀。

执行完b=a-b后:a=a₀+b₀,b=a₀。

执行完a=a-b后:a=b₀,b=a₀。

这样,就不难理解两个变量是如何交换的了。

提示1-12: 可以通过手工模拟的方法理解程序的执行方式,重点在于记录每条语句执行之后各个变量的值。

这个方法看起来很好(少用一个变量),但实际上很少使用,因为它的适用范围很窄:只有定义了加减法的数据类型才能采用此方法

(5) 这个方法还有一个“变种”:用异或运算“^”代替加法和减法,还可以进一步简写成a^=b^=a^=b,但不建议使用。

提示1-13: 交换两个变量的三变量法适用范围广,推荐使用。

那么是不是说,三变量法是解决本题的最佳途径呢?答案是否定的。

==多数算法竞赛采用黑盒测试,即只考查程序解决问题的能力,而不关心采用了什么方法。==

对于本题而言,最合适的程序如下:

#include<stdio.h>
int main()
{
  int a, b;
  scanf("%d%d", &a, &b);
  printf("%d %d\n", b, a);
  return 0;
}

换句话说,我们的目标是解决问题,而不是为了写程序而写程序,同时应保持简单(Keep It Simple and Stupid,KISS),而不是自己创造条件去展示编程技巧。

提示1-14: 算法竞赛是在比谁能更好地解决问题,而不是在比谁写的程序看上去更高级。

1.4 分支结构程序设计

例题1-4 鸡兔同笼

已知鸡和兔的总数量为n,总腿数为m。输入n和m,依次输出鸡的数目和兔的数目。如果无解,则输出No answer。

样例输入:

14 32

样例输出:

12 2

样例输入:

10 16

样例输出:

No answer

【分析】

设鸡有a只,兔有b只,则a+b=n,2a+4b=m,联立解得a=(4n-m)/2,b=n-a。在什么情况下此解“不算数”呢?首先,a和b都是整数;其次,a和b必须是非负的。可以通过下面的程序判断:

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int a, b, n, m;
	cin >> n;
	cin >> m;

	a = (4 * n - m) / 2;
	b = n - a;
	
	if(m % 2 == 1 || a < 0 || b < 0)
	{
		cout<<"No answer"<<endl;
	}
	else
	{
		cout << a << " " << b << endl;
	}
}

上面的程序用到了if语句,其一般格式是:

if(条件)  
	语句1;
else  
	语句2;

注意语句1和语句2后面的分号,以及if后面的括号。

“条件”是一个表达式,当该表达式的值为“真”时执行语句1,否则执行语句2。

另外,“else语句2”是可以省略的。

语句1和语句2前面的空行是为了让程序更加美观,并不是必需的,但强烈推荐读者使用。

提示1-15: if语句的基本格式为:if(条件) 语句1; else 语句2

m % 2 == 1 || a < 0 || b < 0 是一个表达式,其字面意思是“m是奇数,或者a小于0,或者b小于0”。这句话可能正确,也可能错误。因此这个表达式的值可能为真,也可能为假,取决于m、a和b的具体数值。

这样的表达式称为逻辑表达式。和算术表达式类似,逻辑表达式也由运算符和值构成,例如“||”运算符称为“逻辑或”,a||b表示a为真,或者b为真。

提示1-16: if语句的条件是一个逻辑表达式,它的值可能为真,也可能为假。单个整数值也可以表示真假,其中0为假,其他值为真。

细心的读者也许发现了,如果a为真,则无论b的值如何,a||b均为真。换句话说,一旦发现a为真,就不必计算b的值。C语言正是采取了这样的策略,称为短路(short-circuit)。

提示1-17: C语言中的逻辑运算符都是短路运算符。一旦能够确定整个表达式的值,就不再继续计算。

例题1-5 三整数排序

输入3个整数,从小到大排序后输出。

样例输入:

20 7 33

样例输出:

7 20 33

【分析】

abc这3个数一共只有6种可能的顺序:abcacbbacbcacabcba,所以最简单的思路是使用6条if语句。

#include<stdio.h>
int main()
{
  int a, b, c;
  scanf("%d%d%d", &a, &b, &c);
  if(a < b && b < c)printf("%d %d %d\n", a, b, c);
  if(a < c && c < b)printf("%d %d %d\n", a, c, b);
  if(b < a && a < c)printf("%d %d %d\n", b, a, c);
  if(b < c && c < a)printf("%d %d %d\n", b, c, a);
  if(c < a && a < b)printf("%d %d %d\n", c, a, b);
  if(c < b && b < a)printf("%d %d %d\n", c, b, a);
  return 0;
}

输入“111”将得不到任何输出!这个例子说明:即使通过了题目中给出的样例,程序仍然可能存在问题。

提示1-18: 算法竞赛的目标是编程对任意输入均得到正确的结果,而不仅是样例数据。

将程序稍作修改:把所有的小于符号“<”改成小于等于符号“<=”

#include<stdio.h>
int main()
{
  int a, b, c;
  scanf("%d%d%d", &a, &b, &c);
  if(a <= b && b <= c)printf("%d %d %d\n", a, b, c);
  if(a <= c && c <= b)printf("%d %d %d\n", a, c, b);
  if(b <= a && a <= c)printf("%d %d %d\n", b, a, c);
  if(b <= c && c <= a)printf("%d %d %d\n", b, c, a);
  if(c <= a && a <= b)printf("%d %d %d\n", c, a, b);
  if(c <= b && b <= a)printf("%d %d %d\n", c, b, a);
  return 0;
}

对于“111”,6种情况全部符合,但是程序一共输出了6次“111”。

每个if语句的条件都会被独立判断,可能会有多个条件满足时都执行对应的代码块。

一种解决方案是人为地让6种情况没有交叉:把所有的if改成else if。这样可以确保只有一个条件满足时执行对应的代码块。

else if的解释

  • if-else if语句用于在满足多个条件时执行不同的代码块。
  • 当if条件为真时,执行if后面的代码块;当if条件为假时,继续判断下一个else if条件,如果为真则执行对应的代码块;如果所有的else if条件都为假,则执行最后的else分支。
  • if-else if语句可以有多个if条件和多个else if条件,但只能有一个else分支。
#include<stdio.h>
int main()
{
  int a, b, c;
  scanf("%d%d%d", &a, &b, &c);
  if(a <= b && b <= c) printf("%d %d %d\n", a, b, c);
  else if(a <= c && c <= b) printf("%d %d %d\n", a, c, b);
  else if(b <= a && a <= c) printf("%d %d %d\n", b, a, c);
  else if(b <= c && c <= a) printf("%d %d %d\n", b, c, a);
  else if(c <= a && a <= b) printf("%d %d %d\n", c, a, b);
  else if(c <= b && b <= a) printf("%d %d %d\n", c, b, a);
  //最后一条语句还可以简化成单独的else
  return 0;
}

提示1-19: 如果有多个并列、情况不交叉的条件需要一一处理,可以用else if语句。

另一种思路是把abc这3个变量本身改成abc的形式。首先检查ab的值,如果ab,则交换ab(利用前面讲过的三变量交换法);接下来检查ac,最后检查bc,程序如下:

#include<stdio.h>
int main()
{
  int a, b, c, t;
  scanf("%d%d%d", &a, &b, &c);
  if(a > b) { t = a; a = b; b = t; } //执行完毕之后a≤b
  if(a > c) { t = a; a = c; c = t; } //执行完毕之后a≤c,且a≤b依然成立
  if(b > c) { t = b; b = c; c = t; }
  printf("%d %d %d\n", a, b, c);
  return 0;
}

在C语言中,单行注释从 // 开始直到行末为止;多行注释用 /**/ 包围起来

(6) 单行注释原先只有C++支持,后来已成为C99的标准的一部分。

提示1-20: 适当在程序中编写注释不仅能让其他用户更快地搞懂你的程序,还能帮你自己理清思路。

提示1-21: 可以用花括号把若干条语句组合成一个整体。这些语句仍然按顺序执行。

1.5 注解与习题

​ 经过前几个小节的学习,相信读者已经初步了解顺序结构程序设计和分支结构程序设计的核心概念和方法,然而对这些知识进行总结,并且完成适当的练习是很必要的。

​ 为了突出实践的重要性,本章从一开始就不加解释地给出了一段程序,并鼓励读者暂时忽略不理解的细节,把注意力集中在变量、表达式、赋值等核心内容。然而,实践的步伐也不是越快越好,因此笔者在每章的最后加入一些理论知识,供读者在实践之余稍加注意。也可以直接跳到第2章继续阅读,以后再阅读(并且实践)这些文字。

1.5.1 C语言、C99、C11及其他

本书的前4章介绍C语言,更具体地说是介绍C99标准中对算法竞赛而言最核心的部分。C语言的历史和特点不难在网上以及其他书籍中找到,并且本书的前言中也详细叙述了为什么要介绍C语言,因此这里唯一想讲的是C99和编译器。

什么是编译器?简单地说,编译器的任务就是把人类可以看懂的源代码变成机器可以直接执行的指令

......

1.5.2 数据类型与输入格式

数据类型实验。

输入格式实验。

如果真的要输出斜线“\”和字符n,怎么办?

方法是 printf("\\n");,编译器会把双斜线 \\ 理解成单个字符 \

如何连续输出“%”和d两个字符?

要输出 % 字符,可以使用转义字符 %% 来表示。而要输出 d 字符,则直接输出即可。

1.5.3 习题

习题1-1 平均数(average)

输入3个整数,输出它们的平均值,保留3位小数。

#include <bits/stdc++.h>
using namespace std;

float average(int a, int b, int c)
{
	return (a+b+c)/3.0;
}

int main()
{
    int a, b, c;
    cin >> a >> b >> c;
    printf("%.3f\n", average(a,b,c));
	return 0;
}

习题1-2 温度(temperature)

输入华氏温度f,输出对应的摄氏温度c,保留3位小数。提示:c=5(f-32)/9。

#include <bits/stdc++.h>
using namespace std;

int main()
{
	double f, c;
	scanf("%lf", &f);
	c = 5.0 * (f - 32) / 9;
	printf("%.3lf\n", c);
	return 0;
}

习题1-3 连续和(sum)

输入正整数n,输出1+2+…+n的值。提示:目标是解决问题,而不是练习编程。

#include <bits/stdc++.h>
using namespace std;

int main()
{
	int n;
	int sum = 0;
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		sum += i;
	}
	cout << sum << endl;
	return 0;
}

习题1-4 正弦和余弦(sin和cos)

输入正整数nn<360),输出n度的正弦、余弦函数值。提示:使用数学函数。

//错误的
#include <bits/stdc++.h>
using namespace std;

int main()
{
	int n;
	cin >> n;
	cout << sin(n) << endl;
	cout << cos(n) << endl;
	return 0;
}

习题1-5 打折 (discount)

一件衣服95元,若消费满300元,可打八五折。输入购买衣服件数,输出需要支付的金额(单位:元),保留两位小数。

#include <bits/stdc++.h>
using namespace std;

double discount(int cloth, int num)
{
	double amount = cloth * num;
	if(amount >= 300)
	{
		return 0.85 * amount;
	}
	else
	{
		return amount;
	}
}

int main()
{
	int cloth = 95;
	int num;
	cin >> num;
	cout << fixed << setprecision(2) << discount(cloth, num) << endl;
    return 0;

}

习题1-6 三角形(triangle)

输入三角形3条边的长度值(均为正整数),判断是否能为直角三角形的3个边长。如果可以,则输出yes,如果不能,则输出no。如果根本无法构成三角形,则输出not a triangle。

#include <bits/stdc++.h>
using namespace std;

void triangle(int a, int b, int c)
{
	if(a*a + b*b == c*c || a*a + c*c == b*b || b*b + c*c == a*a)
		cout << "yes" << endl;
	else if(a+b>c && a+c>b && b+c>a)
		cout << "no" << endl;
	else
    	cout << "not a triangle" << endl;
}

int main()
{
	int a, b, c;
	cin >> a >> b >> c;
	triangle(a,b,c);
	return 0;
}

习题1-7 年份(year)

输入年份,判断是否为闰年。如果是,则输出yes,否则输出no。

提示:简单地判断除以4的余数是不够的。

#include <bits/stdc++.h>
using namespace std;

void leapYear(int year)
{
	if(year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
		cout << "yes" << endl;
	else
		cout << "no" << endl;
}

int main()
{
	int year;
	cin >> year;
	leapYear(year);
	return 0;
}

拓展题目

接下来的题目需要更多的思考:如何用实验方法确定以下问题的答案?注意,不要查书,也不要在网上搜索答案,必须亲手尝试——实践精神是极其重要的。

问题1: int型整数的最小值和最大值是多少(需要精确值)?

问题2: double型浮点数能精确到多少位小数?或者,这个问题本身值得商榷?

问题3: double型浮点数最大正数值和最小正数值分别是多少(不必特别精确)?

问题4: 逻辑运算符号“&&”、“||”和“!”(表示逻辑非)的相对优先级是怎样的?也就是说,a&&b||c应理解成(a&&b)||c还是a&&(b||c),或者随便怎么理解都可以?

问题5: if(a)if(b)x++;else y++的确切含义是什么?这个else应和哪个if配套?有没有办法明确表达出配套方法?


要通过实验方法确定这些问题的答案,你需要编写一些简单的代码来测试和观察结果。以下是如何针对每个问题进行实验的步骤:

问题1:int型整数的最小值和最大值是多少? C and C++ Integer Limits

  1. 编写一个简单的程序,使用语言提供的常量来打印int类型的最小值和最大值。在C/C++中,这些常量通常是INT_MININT_MAX,在Java中是Integer.MIN_VALUEInteger.MAX_VALUE
#include <stdio.h>
#include <limits.h>

int main() {
    printf("The minimum value of int = %d\n", INT_MIN);
    printf("The maximum value of int = %d\n", INT_MAX);
    return 0;
}
  1. 运行程序并观察输出。

问题2:double型浮点数能精确到多少位小数?

  1. 创建一个double变量,并给它赋一个具有很多小数位的值。
  2. 打印这个变量,逐渐增加小数点后的位数,观察何时开始出现精度丢失。
#include <stdio.h>

int main() {
    double num = 1.12345678901234567890;
    printf("%.20f\n", num); // 尝试不同的精度,比如 %.5f, %.10f, %.15f, %.20f
    return 0;
}
  1. 通过观察输出,确定double类型能保持的最大精度。

问题3:double型浮点数最大正数值和最小正数值分别是多少?

  1. 使用编程语言提供的常量如DBL_MAXDBL_MIN来打印double类型的最大正数值和最小正数值。
  2. 运行程序并观察输出。
#include <stdio.h>
#include <float.h>

int main() {
    // %e: a floating point number in scientific notation
    printf("The largest positive double = %e\n", DBL_MAX);
    printf("The smallest positive double = %e\n", DBL_MIN);
    return 0;
}

Format Specifiers in C

问题4:逻辑运算符号“&&”、“||”和“!”的相对优先级是怎样的?

  1. 编写一个程序,包含复杂的逻辑表达式,使用括号明确指定优先级。
  2. 改变括号的位置,或者去掉括号,看看结果是否有变化。
  3. 根据观察到的结果确定优先级。
#include <stdio.h>

int main() {
    int a = 0, b = 1, c = 1;
    int result = a && b || c; // 尝试加括号改变优先级 (a && b) || c 和 a && (b || c)
    printf("Result: %d\n", result);
    return 0;
}

优先级: ! > && > ||

问题5:if(a)if(b)x++;else y++的确切含义是什么?

  1. 编写一个程序,设置不同的a和b值,观察x和y的变化。
  2. 通过添加花括号明确else子句与哪个if语句配对。
#include <stdio.h>

int main() {
    int a = 1, b = 0;
    int x = 0, y = 0;

    if (a)
        if (b)
            x++;
        else
            y++;
    
    printf("x: %d, y: %d\n", x, y);
    return 0;
}
  1. 运行程序,观察不同a和b值对x和y的影响。通过实验确定else与最近的未配对if语句配对。

通过上述实验,你可以确定这些问题的答案,同时加深对编程语言特性的理解。

1.5.3 习题的评估和优化(ChatGPT-4)

Q: 评价一下我写的这些代码,如果是在算法竞赛上,我编写的代码有什么可以改进的?请给出改进后的代码。

A: 首先,你的代码很好地实现了题目的基本要求,但是在算法竞赛中,评判标准通常不仅仅是正确性,还包括代码的效率、可读性和健壮性。

习题1-1 平均数(average)

输入3个整数,输出它们的平均值,保留3位小数。

  • 你的 average 函数使用了 int 类型的参数,然后除以 3.0 来得到 float 类型的结果,这是正确的。但是,为了更精确地表示浮点数,建议使用 double 类型,因为 double 提供了更高的精度。

  • 函数的返回类型是 float,但在 main 函数中你使用了 printf 并指定了 %.3f 格式化字符串,这适用于 double 类型。虽然 float 会被自动提升为 double,但为了一致性和避免混淆,建议将 average 函数的返回类型改为 double

  • 代码可读性方面,可以考虑添加一些注释来解释函数的作用。

    #include <iostream>
    #include <iomanip> // 用于设置输出格式
    using namespace std;
    
    double average(int a, int b, int c) {
        return (a + b + c) / 3.0;
    }
    
    int main() {
        int a, b, c;
        cin >> a >> b >> c;
        cout << fixed << setprecision(3) << average(a, b, c) << endl;
        return 0;
    }
    

习题1-2 温度(temperature)

输入华氏温度f,输出对应的摄氏温度c,保留3位小数。提示:c=5(f-32)/9。

  • 这段代码很简洁,并且正确地使用了 double 类型来存储温度值,这很好。

  • 你可以考虑将温度转换的逻辑封装到一个函数中,这样可以提高代码的可重用性和可读性。

    #include <iostream>
    #include <iomanip> // 用于设置输出格式
    using namespace std;
    
    double fahrenheitToCelsius(double f) {
        return 5.0 * (f - 32) / 9;
    }
    
    int main() {
        double f;
        cin >> f;
        cout << fixed << setprecision(3) << fahrenheitToCelsius(f) << endl;
        return 0;
    }
    

习题1-3 连续和(sum)

输入正整数n,输出1+2+…+n的值。提示:目标是解决问题,而不是练习编程。

  • 对于连续求和问题,你使用了一个循环来累加每个数,这在 n 较小时是可行的。但是,如果 n 非常大,这种方法的效率就会变得很低。

  • 可以使用等差数列的求和公式 n * (n + 1) / 2 来直接计算出结果,这样不仅效率更高,而且可以避免潜在的整数溢出问题。

  • 你的代码使用了 int 类型来存储 sum,如果 n 非常大,可能会导致整数溢出。可以考虑使用更大范围的整数类型,如 long long。同时,使用 static_cast 来确保正确的类型转换。 Difference between long int and long long int in C/C++ static_cast in C++

    #include <iostream>
    using namespace std;
    
    long long sum(int n) {
        return static_cast<long long>(n) * (n + 1) / 2;
    }
    
    int main() {
        int n;
        cin >> n;
        cout << sum(n) << endl;
        return 0;
    }
    

习题1-4 正弦和余弦(sin和cos)

输入正整数nn<360),输出n度的正弦、余弦函数值。提示:使用数学函数。

A: 在你提供的代码中,你试图计算一个角度的正弦和余弦值。然而,这里有一个关键的问题需要注意:C++ 中的 sincos 函数默认接受的参数是弧度而不是度。因此,你需要将角度转换为弧度,然后再传递给这些函数。另外,包含 <bits/stdc++.h> 是 C++ 竞赛编程中的一个常见做法,但在工业实践中通常不推荐,因为它不是标准库的一部分,且可能导致编译时间变长。在算法竞赛中,为了方便,这样做是可以接受的。

下面是改进后的代码:

#include <iostream>
#include <cmath> // 使用数学库

using namespace std;

int main()
{
	int n;
	cin >> n;
	// 将度转换为弧度
    //double radians = n * M_PI / 180.0;
	double radians = n * 3.14159265358979323846 / 180.0;
	// 使用弧度计算正弦和余弦
	cout << sin(radians) << endl;
	cout << cos(radians) << endl;
	return 0;
}

这里的几点改进:

  1. 使用 <cmath> 替换 <bits/stdc++.h>,因为我们只需要数学相关的功能。
  2. 引入了一个新的变量 radians,它将输入的角度 n 转换为弧度。
  3. 使用 M_PI 来获取 π 的值,这是 <cmath> 提供的一个常量。

在这个代码片段中,我用了 π 的值来代替 M_PI。现在,这段代码应该可以在不支持 M_PI 的编译器上运行了。如果你想保持 M_PI 的使用,可以在代码顶部定义它:

#define _USE_MATH_DEFINES // 启用数学定义
#include <cmath>

#ifndef M_PI
#define M_PI 3.14159265358979323846 // 手动定义M_PI
#endif

// ... 其余代码不变

这段代码首先尝试通过定义 _USE_MATH_DEFINES 来启用 M_PI 的定义(在某些编译器/平台中有效),如果这个宏仍然未定义,那么它会手动定义 M_PI

习题1-5 打折 (discount)

一件衣服95元,若消费满300元,可打八五折。输入购买衣服件数,输出需要支付的金额(单位:元),保留两位小数。

A: 您的代码整体上是正确的,功能上能够实现题目要求的打折计算。但是如果是在算法竞赛中,评委通常会关注代码的简洁性、效率和可读性。以下是一些可以改进的点:

  1. 函数discount的参数名clothnum可以更具描述性,比如pricePerClothnumClothes

  2. 如果discount函数只在main函数中使用,可以考虑将其定义为main函数内的局部函数,这样可以减少全局命名空间的污染

  3. 在算法竞赛中,通常不需要使用using namespace std;,因为这可能会导致命名冲突。最好是直接使用std::前缀。

  4. 代码中的double amount = cloth * num;可以直接在if语句中计算,减少变量的使用

以下是改进后的代码:

#include <iostream>
#include <iomanip>

double calculateTotalPrice(int pricePerCloth, int numClothes) {
    double amount = pricePerCloth * numClothes;
    if (amount >= 300) {
        amount *= 0.85;
    }
    return amount;
}

int main() {
    const int pricePerCloth = 95; // 衣服的单价为常量,建议用const修饰
    int numClothes;
    std::cin >> numClothes;
    std::cout << std::fixed << std::setprecision(2) << calculateTotalPrice(pricePerCloth, numClothes) << std::endl;
    return 0;
}

这样的代码更加清晰和易于维护,并且遵循了更多的C++最佳实践。

习题1-6 三角形(triangle)

输入三角形3条边的长度值(均为正整数),判断是否能为直角三角形的3个边长。如果可以,则输出yes,如果不能,则输出no。如果根本无法构成三角形,则输出not a triangle。

#include <iostream>
#include <algorithm> // 用于 std::sort
using namespace std;

void checkRightTriangle(int a, int b, int c) {
    // 首先,检查是否可以构成三角形
    if (a + b > c && a + c > b && b + c > a) {
        // 对边长进行排序,以方便判断直角三角形
        int sides[3] = {a, b, c};
        sort(sides, sides + 3);
        // 判断最短的的两边的平方和是否等于最长边的平方
        if (sides[0] * sides[0] + sides[1] * sides[1] == sides[2] * sides[2]) {
            cout << "yes" << endl;
        } else {
            cout << "no" << endl;
        }
    } else {
        cout << "not a triangle" << endl;
    }
}

int main() {
    int a, b, c;
    cin >> a >> b >> c;
    checkRightTriangle(a, b, c);
    return 0;
}

在这个改进版本中,我首先对输入的边长进行了排序,这样就可以很容易地应用勾股定理来检查是否是直角三角形。同时,我增加了无法构成三角形时的输出。

解释这段代码的的算法实现

这段 C++ 代码的目的是判断给定的三个正整数是否可以构成直角三角形。算法实现的步骤如下:

  1. 检查是否能构成三角形

    • 首先,代码通过比较三个数的和来判断它们是否可以构成一个三角形。根据三角形的形成条件,任意两边之和必须大于第三边。因此,如果 a + b > ca + c > bb + c > a 这三个条件都满足,那么这三个数可以构成一个三角形。如果任何一个条件不满足,则输出 "not a triangle"。
  2. 对边长进行排序

    • 如果能构成三角形,代码接下来会将三个边长存入一个数组 sides,然后使用标准库算法 std::sort 对数组进行升序排序。

      • 在 C++ 中,std::sort 是一个非常有用的标准库算法,用于对序列进行排序。在这段代码中,std::sort 被用来对包含三个边长的数组 sides 进行排序。

        这里的 sort(sides, sides + 3); 调用的是 <algorithm> 头文件中定义的 std::sort 函数。这个函数接收两个参数,分别是指向要排序序列开始的迭代器和指向序列尾端的迭代器。在这个例子中,sides 是数组的名称,它也可以被用作指向数组第一个元素的指针。因此,sides 相当于指向序列开始的迭代器。

        第二个参数 sides + 3 则是指向数组尾端的迭代器。由于数组有三个元素,所以 sides + 3 指向的是数组最后一个元素之后的位置。在 C++ 中,范围是左闭右开的,这意味着排序的范围包括第一个元素 sides[0] 但不包括 sides + 3 指向的位置。

        std::sort 默认使用 < 运算符来比较元素,因此它会将数组 sides 中的元素按升序排列。 排序完成后,数组 sides 中的元素将按从小到大的顺序排列。这对于之后判断是否可以构成直角三角形是非常有用的,因为这样可以确保 sides[2] 是最长的边,这样我们就可以直接使用勾股定理的公式来进行判断。

    • 排序后,sides[0] 将是最短的边,sides[1] 是中间的边,而 sides[2] 是最长的边。

  3. 判断是否为直角三角形

    • 根据勾股定理,如果三个数能构成直角三角形,那么最短的两边的平方和应该等于最长边的平方。代码通过计算 sides[0] * sides[0] + sides[1] * sides[1] 并检查它是否等于 sides[2] * sides[2] 来判断是否为直角三角形。如果等式成立,输出 "yes" 表示它们可以构成直角三角形;如果等式不成立,输出 "no" 表示它们不能构成直角三角形。
  4. 主函数

    • main 函数是程序的入口点。它从标准输入读取三个整数 abc,然后调用 checkRightTriangle 函数来进行检查,并最终返回 0 表示程序正常结束。

总结:代码首先验证输入的三边能否构成三角形,然后对边长进行排序,最后应用勾股定理判断是否为直角三角形。根据判断结果输出相应的信息。

习题1-7 年份(year)

输入年份,判断是否为闰年。如果是,则输出yes,否则输出no。

提示:简单地判断除以4的余数是不够的。

A: 你编写的代码逻辑是正确的,可以判断给定年份是否是闰年。但是,在算法竞赛中,为了提高代码的效率和可读性,可以进行以下改进:

  1. 可以将判断闰年的逻辑封装成一个函数,而不是直接在 main 函数中实现。
  2. 使用更加直观的变量名,增加代码的可读性。
  3. 可以考虑使用布尔类型的变量来存储判断结果,而不是直接输出字符串。

改进后的代码如下:

#include <iostream>
using namespace std;

bool isLeapYear(int year)
{
    if((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
        return true;
    else
        return false;
}

int main()
{
    int year;
    cin >> year;
    if(isLeapYear(year))
        cout << "yes" << endl;
    else
        cout << "no" << endl;
    return 0;
}

拓展题目

在C++中,使用std::numeric_limits可以获取各种数据类型的属性,包括最大值和最小值。运行这些程序将帮助你实验性地确定问题的答案。

问题1: int型整数的最小值和最大值是多少(需要精确值)?

#include <iostream>
#include <limits>

int main() {
    std::cout << "The minimum value of int = " << std::numeric_limits<int>::min() << std::endl;
    std::cout << "The maximum value of int = " << std::numeric_limits<int>::max() << std::endl;
    return 0;
}
The minimum value of int = -2147483648
The maximum value of int = 2147483647

解释:在大多数现代编程环境中,int 类型通常是32位的,采用二进制补码形式存储,这意味着它的范围是从 -2^312^31 - 1

问题2: double型浮点数能精确到多少位小数?或者,这个问题本身值得商榷?

预期结果:会发现当打印到15或16位小数时,数值开始显示不精确。

#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
	double num = 1.12345678901234567890;
	cout << fixed << setprecision(20) << num << endl;
    return 0;
}
1.12345678901234569125

--------------------------------
Process exited after 0.01193 seconds with return value 0
请按任意键继续. . .

解释:double 类型的浮点数通常遵循IEEE 754标准,使用64位来表示。这其中有52位用于表示尾数,大约可以提供15到16位的十进制精度。

问题3: double型浮点数最大正数值和最小正数值分别是多少(不必特别精确)?

#include <iostream>
#include <limits>

int main() {
    std::cout << "The largest positive double = " << std::numeric_limits<double>::max() << std::endl;
    std::cout << "The smallest positive double = " << std::numeric_limits<double>::min() << std::endl;
    return 0;
}
The largest positive double = 1.79769e+308
The smallest positive double = 2.22507e-308

解释:根据IEEE 754标准,double 类型的最大正数大约是 1.7976931348623157 x 10^308,最小正数大约是 2.2250738585072014 x 10^-308(不考虑次正规数)。

问题4: 逻辑运算符号“&&”、“||”和“!”(表示逻辑非)的相对优先级是怎样的?也就是说,a&&b||c应理解成(a&&b)||c还是a&&(b||c),或者随便怎么理解都可以?

#include <stdio.h>

int main() {
    int a = 0, b = 1, c = 1;
    int result = a && b || c; // 尝试加括号改变优先级 (a && b) || c 和 a && (b || c)
	printf("Result: %d\n", result);
	
	int result_1 = (a && b) || c;
	int result_2 = a && (b || c);
	printf("Result (a && b) || c: %d\n", result_1);
	printf("Result a && (b || c): %d\n", result_2);

	return 0;
}
Result: 1
Result (a && b) || c: 1
Result a && (b || c): 0

解释:在大多数编程语言中,逻辑与运算符 && 的优先级高于逻辑或运算符 ||,所以 a && b 会先被计算。逻辑非运算符 ! 的优先级通常高于 &&||

问题5: if(a)if(b)x++;else y++的确切含义是什么?这个else应和哪个if配套?有没有办法明确表达出配套方法?

预期结果:根据a和b的值,xy 会增加。如果 a 为真,b 为假,则 y 增加;如果 ab 都为真,则 x 增加。如果 a 为假,else 语句不会执行,无论 b 的值如何。

#include <iostream>

int main() {
    int a = 1, b = 0;
    int x = 0, y = 0;

    if (a)
        if (b)
            x++;
        else
            y++;

    std::cout << "x: " << x << ", y: " << y << std::endl;

    // 通过添加花括号明确else与第一个if配对
	a = 0, b = 1;
    x = 0, y = 0;
    
    if (a) {
        if (b) {
            x++;
        }
    } else {
        y++;
    }

    std::cout << "With braces - x: " << x << ", y: " << y << std::endl;

    return 0;
}

x: 0, y: 1
With braces - x: 0, y: 1

解释:如果没有花括号明确指示,else 语句总是与最近的 if 语句配对。 因此,如果 a 为真,程序会检查 b。如果 b 也为真,就执行 x++;如果 b 为假,就执行 y++。如果 a 为假,if(b) 及其后的代码都不会执行。

其他QA

Q: 为什么你把我的头文件换了

A: 抱歉给您带来了困惑。在您的代码中,您使用了 #include <bits/stdc++.h>,这个头文件是GCC编译器特有的,它包含了几乎所有的标准C++库。虽然这个头文件在算法竞赛中常常使用,因为它可以简化代码并减少编写多个 #include 指令的时间,但它并不是C++标准库的一部分,而且在非GCC编译器中可能不可用。

在实际的软件开发和一些对编译器兼容性有要求的场合,推荐直接包含所需的标准库头文件,比如 <iostream> 用于输入输出流,<iomanip> 用于控制输入输出格式等。这种做法可以提高代码的可移植性和明确表达代码的依赖,使得代码更容易被他人理解和维护。

因此,在改进后的代码中,我使用了 <iostream><iomanip> 来替换了非标准的 <bits/stdc++.h> 头文件。这样做的目的是为了让代码遵循更通用的C++标准,确保它在不同的编译器和环境中都能够正常工作。

Q: '在算法竞赛中,通常不需要使用using namespace std;,因为这可能会导致命名冲突。'何以见得?

A: 在算法竞赛中,使用using namespace std;可能不是一个好习惯,原因主要是出于代码的清晰性和避免潜在的命名冲突。

std是C++标准库的命名空间,它包含了所有标准库的类和函数。当你使用using namespace std;后,你可以不用std::前缀就能直接访问这些类和函数。这在一些情况下可以减少代码量,但是也有一些缺点:

  1. 命名冲突:如果你的代码(或者你所包含的其他库的代码)定义了与标准库中同名的函数或者类,那么使用using namespace std;会导致不清楚你所引用的是哪一个。这种情况下,编译器可能会报错或者产生意料之外的行为。

  2. 代码清晰性:在阅读代码的时候,如果看到一个没有命名空间的函数或者类,可能不清楚它是来自标准库还是其他地方。使用std::前缀可以立即明确这一点,提高代码的可读性。

  3. 习惯养成:在算法竞赛中,代码通常较短,可能不会引起太大问题。但在大型项目中,使用using namespace std;可能会导致很多难以发现的问题。因此,养成不使用using namespace std;的习惯,对于日后的编程实践是有益的。

虽然在算法竞赛中,代码长度和编写速度很重要,有时候使用using namespace std;看起来会更方便,但是从长远来看,避免使用它可以帮助你写出更清晰、更可维护的代码。因此,在算法竞赛中,也推荐明确使用std::前缀,以养成良好的编程习惯。

1.5.4 小结

一是重视实验。 哪怕不理解背后的道理,至少要清楚现象。

二是学会模仿。 初学者自学和理解能力不够,自信心也不够,不适合在动手之前被灌输大量的理论。

正确的处理方法是“抓住主要矛盾”——始终把学习、实验的焦点集中在最有趣的部分。 如果直观地解决方案行得通,就不必追究其背后的原理。如果对一个东西不理解,就不要对其进行修改;如果非改不可,则应根据自己的直觉和猜测尝试各种改法,而不必过多地思考“为什么要这样”。

当然,这样的策略并不一定持续很久。当学生有一定的自学、研究能力之后,本书会在适当的时候解释一些重要的概念和原理,并引导学生寻找更多的资料进一步学习。要想把事情做好,必须学得透彻,但没有必要操之过急。