函数与结构体
函数
程序中有时会多次使用相同的语句,而且无法通过循环来减少重复编程。对于这样的代码块,可以将其封装成一个函数。每个程序都用到了主函数 main(),除此之外,C++ 中有一些常用函数,有时也需要自定义函数,并将参数传给函数,使其能够根据这些参数完成要求的任务。
常见函数
头文件 <cmath> 里:
sqrt(x)根号pow(a, b)\(a^b\),在 \(a, b\) 均为整数时不建议使用,建议用循环实现乘方ceil(x)上取整,对整除式子取整时不建议使用,可以用if判断或用(a+b-1)/b代表 \(\lceil \dfrac{a}{b} \rceil\)floor(x)下取整,对整除式子取整时不建议使用,可以用整数除法截断,负数时使用if判断round(x)四舍五入取整- 三角函数(注意用的是弧度制,\(\pi\) 为 \(180^{\circ}\) )与反三角函数(根据三角函数值计算对应弧度)
sin(x)正弦函数cos(x)余弦函数tan(x)正切函数asin(x)反正弦函数acos(x)反余弦函数,特殊地,acos(-1)表示 \(\pi\)atan(x)反正切函数
头文件中 <algorithm> 里:
abs(x)绝对值max(a, b)返回较大值min(a, b)返回较小值swap(a, b)交换a, b两个变量
头文件 <cstring> 里:
strlen(s)字符串长度strcmp(a, b)字符串字典序比较,a小时结果为负,相等时结果为0,b小时结果为正strcat(a, b)拼接字符串a和bstrcpy(a, b)把b字符串复制到a上memset(a, 0, sizeof(a))将数组清零,按字节赋值(把0改成-1可以将数组清成-1),另一种用法是memeset(a, 0, sizeof(int) * n)表示把数组的前n个元素清零
例题:P5735 [深基7.例1] 距离函数
给出平面坐标上不在一条直线上 \(3\) 个点坐标 \((x_1,y_1),(x_2,y_2),(x_3,y_3)\),坐标值是实数,且绝对值不超过 \(100.00\),求其围成的三角形周长。
分析:\(3\) 个点,两两组成一条线段。平面上两点的距离是 \(\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}\)。分别计算这三条线段的长度,累加到一起,就可以得到三角形的周长。可以写出下面的程序:
#include <cstdio>
#include <cmath>
int main()
{
double x1, y1, x2, y2, x3, y3, ans;
scanf("%lf%lf%lf%lf%lf%lf", &x1, &y1, &x2, &y2, &x3, &y3);
ans = sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
ans += sqrt((x2-x3)*(x2-x3)+(y2-y3)*(y2-y3));
ans += sqrt((x1-x3)*(x1-x3)+(y1-y3)*(y1-y3));
printf("%.2f\n", ans);
return 0;
}
这个程序本身是正确的,但是有一些语句看起来比较啰嗦,手动写了 \(3\) 条算两点距离的语句,看起来复杂,而且容易因为键入有误而出错。可以利用函数来减少重复的代码逻辑,简化程序的主干。改进后的程序如下:
#include <cstdio>
#include <cmath>
double sq(double x) { // 计算平方
return x * x;
}
double dist(double x1, double y1, double x2, double y2) { // 计算两点距离
return sqrt(sq(x1-x2) + sq(y1-y2));
}
int main()
{
double x1, y1, x2, y2, x3, y3;
scanf("%lf%lf%lf%lf%lf%lf", &x1, &y1, &x2, &y2, &x3, &y3);
printf("%.2f\n", dist(x1, y1, x2, y2) + dist(x2, y2, x3, y3) + dist(x1, y1, x3, y3));
return 0;
}
函数定义的一般形式如下:
返回类型 函数名(参数类型1 参数名1, ..., 参数类型n 参数名n) {
函数体
return 结果;
}
如果函数不需要返回值,则返回类型应写成
void。如果在执行函数的过程中碰到了return语句,将直接退出这个函数,不去执行后面的语句。相反,如果在执行过程中始终没有return语句,则会返回一个不确定的值。除非本来就不需要返回值的情况(即返回类型为void),函数内如果没有编写return语句往往说明程序设计有问题。
main函数是有返回值的。通常我们让它返回 \(0\),因为main函数是整个程序的入口,换句话说,有一个“其他的程序”来调用这个main函数——如操作系统、IDE、调试器、自动评测系统等。这个 \(0\) 代表“正常结束”,返回给调用者。在竞赛中,除了有特殊规定之外,请让其返回 \(0\),以免评测系统错误地认为程序结束时是异常情况。
函数又称为子程序,上面的例子中定义了两个函数:一个是 sq() 函数,需要调用一个 double 类型的变量 x,经过计算后“返回”一个 double 类型的结果;另一个是 dist() 函数,调用 \(4\) 个 double 类型的变量 x1,y1,x2,y2,经过计算后返回一个 double 类型的结果。由于在 main() 函数中使用了 dist() 函数,dist() 函数又使用了 sq() 函数,所以 sq() 函数应当在 dist() 函数前定义,dist() 函数应在 main() 函数前定义。
当输入数据为“0 0 3 0 0 4”,计算 dist(x2,y2,x3,y3) 时:

- 进入
main()函数(主程序),主程序中三个dist()函数调用形式是一致的,以dist(x2,y2,x3,y3)举例,程序运行到此处时收集了 \(4\) 个参数[3,0,0,4],然后传递给dist()函数,也就是调用dist(3,0,0,4)。 - 进入
dist()函数,需要接收 \(4\) 个double类型的变量x1,y1,x2,y2,这些是形式参数(因为传递参数之前,并不知道具体的值是什么)。传递过来的 \(4\) 个参数的值[3,0,0,4]称为实际参数,然后按照顺序分配给参数列表中的 \(4\) 个变量,因此x1=3,y1=0,x2=0,y2=4。需要注意的是,这里的x1和主程序的x1没有直接关系,是两个不同的变量。然后求sq(x1-x2)的值,程序接收到参数[3],传递给sq()函数调用sq(3)。 - 进入
sq()函数,接收到了传递来的 \(1\) 个参数,按照顺序分配给参数列表中唯一的变量x,因此x=3。经过计算后,返回结果 \(9.0\)。 - 回到
dist()函数,得到了sq(x1-x2)的值是 \(9\)。同理,sq(y1-y2)的值是 \(16\)。加起来取平方根,得到结果 \(5.0\),返回这个结果。 - 回到
main()函数,得到了dist(x2,y2,x3,y3)的结果是 \(5.0\)。其他两个dist()函数的计算过程也是类似的,得到这些值之后加起来,就得到了最终的答案并输出。
需要注意的是,在一个函数里定义的变量,在其他函数里是不能直接使用的。
变量的作用域
阅读下面代码,猜测输出结果,与实际运行结果对比。
#include <cstdio>
void change(int a, int b) {
int t = a; a = b; b = t;
}
int main()
{
int a, b; scanf("%d%d", &a, &b);
change(a, b);
printf("%d %d\n", a, b);
return 0;
}
这个 change 函数看起来是实现两个整型变量的交换,但实际并不能达到交换的效果,原因是对形参的操作不改变实参的值(如果参数为数组则会改变)。
可以使用 <algorithm> 头文件中的 swap 函数实现变量交换。
另外,函数内变量(包括参数)只在函数内有效,与之类似地,for 循环内定义的变量只在该 for 循环内有效,if 大括号里定义的变量只在该大括号内有效。
更一般地说,在一个代码块内定义的变量只在该代码块内有效(当然,必须在声明的那个语句之后才有效),这些都叫局部变量。
与之对应的是全局变量,即在所有函数外声明的变量,在声明之后(声明的那一个语句之后)的任何位置,包括自定义的函数内均有效。
如果在某代码块内局部变量和全局变量名有冲突,则该代码块内该变量名对应的是局部变量。
如果在某代码块内没有定义某局部变量,则如果该代码块嵌套在某大代码块中,程序会往外层寻找,直到找到有效的局部变量或最终找到有效的全局变量为止。
注意:在找变量的时候只会在有嵌套关系的部分里找,两个独立的代码块之前的局部变量永远是独立的。
一般除非十分明确(如两个独立的循环或独立的函数)不建议使用相同的变量名。
下面是一段辅助理解变量作用域的代码,其中三个输出的 x 对应的是不同的变量。
#include <cstdio>
int x;
int main()
{
int a, b; scanf("%d%d", &a, &b);
for (int i = 1; i <= a; i++) {
int x = 1;
for (int j = 1; j <= b; j++) {
int x = 1;
printf("%d %d %d\n", i, j, x); // 这里输出的x是第9行定义的x
x++; // 第9行定义的x
}
for (int j = 1; j <= b; j++) {
printf("%d %d %d\n", i, j, x); // 这里输出的x是第7行定义的x
x++; // 第7行定义的x
}
}
printf("%d\n", x); // 这里输出的x是第2行定义的x
return 0;
}
指针与引用
想要在函数内改变实参(调用时的参数)的值,可以使用指针或引用。
指针存储的数据是某变量的地址。
声明时写:int *p = &a;,表示声明一个指向 int 类型变量(地址存的是 int 类型变量的地址)的指针 p,它里边存 a 变量的地址。注意:int *p, p2;,这样声明时只有 p 是指针。
利用指针实现的 add 函数:
#include <cstdio>
void add(int *p) {
(*p)++;
}
int main()
{
int x = 2;
int *p = &x;
add(p);
printf("%d\n", x);
return 0;
}
目前在竞赛中使用指针的人不多,因为指针的熟练掌握相对比较复杂,多数情况可以用其他方式代替。
如果想要在函数内改变实参(调用时的参数),可以使用引用。
引用相当于别名,无论是对引用的操作,还是对原变量本身的操作,都会作用在该变量上。
#include <cstdio>
int main()
{
int a; scanf("%d", &a);
int &b = a; // 此时&表示引用类型,即b是a的别名,之后无论对a操作还是对b操作都是对同一变量操作
a++;
printf("%d %d\n", a, b);
b++;
printf("%d %d\n", a, b);
return 0;
}
使用引用传参时,只需要在函数实现时的参数类型上写上 & 即可。
#include <cstdio>
void add(int &x) {
x++;
}
int main()
{
int x; scanf("%d", &x);
add(x);
printf("%d\n", x);
return 0;
}
例题:P5736 [深基7.例2] 质数筛
输入 \(n (n \le 100)\) 个不大于 \(100000\) 的整数,要求去除不是质数的数字,依次输出剩余的质数。
解题思路
回想判断质数的方法,把判断质数写成一个函数 bool isprime(int x)。
参考代码
#include <cstdio>
bool isprime(int x) {
if (x < 2) return false;
for (int i = 2; i * i <= x; ++i)
if (x % i == 0) return false;
return true;
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
int x;
scanf("%d", &x);
if (isprime(x)) printf("%d ", x);
}
return 0;
}
例题:B2136 素数回文数的个数
解题思路
最直接的想法就是分别判一个数是不是素数和回文数,判断素数在之前已经写过了,关键就在于判回文了。
回文就是正反都一样,或者说是从两边往中间看对应相等。
需要拆出每一位,这个只需要把数不断对 \(10\) 取余和整除就可以了,把每次拆出来的数串起来(相当于原来的数倒过来)看是否和原数相等即可。
参考代码
#include <cstdio>
bool ispalindrome(int x) {
int tmp = x, rev = 0;
while (x > 0) {
rev = rev * 10 + x % 10;
x /= 10;
}
return rev == tmp;
}
bool isprime(int x) {
for (int i = 2; i * i <= x; i++) {
if (x % i == 0) return false;
}
return true;
}
int main()
{
int n; scanf("%d", &n);
int ans = 0;
for (int i = 11; i <= n; i++) {
if (ispalindrome(i) && isprime(i)) {
ans++;
}
}
printf("%d\n", ans);
return 0;
}
时间复杂度 \(O(n \sqrt{n})\),因为判断素数是 \(O(\sqrt{n})\) 的。
对每个数先判回文再判素数会比先判素数再判会问快一点,因为对于用 && 连接的多个条件,如果前边结果已经是 false 了,后边就不执行了。同理对于用 || 连接的多个条件,如果前边结果已经是 true 了,后边就不执行了。而这题显然回文数比较少,也就意味着先判回文就会有很多数不执行判素数的函数了,会更快一些。
习题:P1304 哥德巴赫猜想
输入一个偶数 \(N(N \le 10000)\),验证 \(4 \sim N\) 的所有偶数是否符合哥德巴赫猜想:任一大于 \(2\) 的偶数都可写成两个质数之和。如果一个数不止一种分法,则输出第一个加数相比其他分法最小的方案。例如输入 \(10\),因为 \(10=3+7=5+5\),因此 \(10=5+5\) 是错误答案。
参考代码
#include <cstdio>
bool isprime(int x) { // 判断x是否为质数
if (x < 2) return false;
for (int i = 2; i * i <= x; i++) // 枚举因子到根号x
if (x % i == 0) return false;
return true;
}
void goldbach(int n) {
for (int i = 2; i <= n / 2; i++)
if (isprime(i) && isprime(n - i)) {
printf("%d=%d+%d\n", n, i, n - i);
return; // 找到第一个数最小的拆分方式即可结束对n的分解
}
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 4; i <= n; i += 2) goldbach(i);
return 0;
}
例题:P5737 [深基7.例3] 闰年展示
输入 \(x\) 和 \(y \ (1582 \le x < y \le 3000)\),输入 \([x,y]\) 区间中闰年个数,并在下一行输出所有闰年年份数字,使用空格隔开。
分析:可以把判断闰年这一部分独立出来成为一个函数,这样主程序就会更加清晰明了。代码如下:
#include <cstdio>
const int N = 3005;
int ans[N];
bool isleap(int year) {
// 四年一闰,百年不闰,四百年又闰
// 如果year是闰年,返回true,不是闰年返回false
return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
}
int main()
{
int x, y; scanf("%d%d", &x, &y);
int cnt = 0;
for (int i = x; i <= y; i++) {
if (isleap(i)) {
cnt++;
ans[cnt] = i;
}
}
printf("%d\n", cnt);
for (int i = 1; i <= cnt; i++) printf("%d ", ans[i]);
return 0;
}
程序中定义了一个 isleap() 的函数,接收一个 int 类型的变量。在主程序中读入 x 和 y 后,依次考虑 x 到 y 之间的每一个年份,用 isleap() 函数调用对应的年份。进入 isleap() 函数后,调用的数字就成为了函数内的 year 变量的值。根据闰年的判断条件,返回 true 或者 false 代表是闰年或者不是闰年。回到主程序,得到了函数返回的结果,如果返回的是 true,就计数一次,并将这个年份存入数组。最后将总数和存下来的闰年年份输出。
习题:P8651 [蓝桥杯 2017 省 B] 日期问题
解题思路
首先计算出 AA 和 CC 分别代表的四位数年份,程序应该先处理年份更早的所有可能性。例如,对于 02/03/60,1960 比 2002 早,程序应该先检查所有 1960 年的可能日期,再检查 2002 年的,这样能确保最终输出在年份上是严格递增的。
当年份确定后,剩下的两个数字可能是 月/日 或 日/月。程序应该保证先检查月份较小的那种组合,这样确保在同一年份下,输出结果在月份和日期上也是有序的。
对于一种候选的年月日情况,应该检查这个日期是否在 1960-2059 的范围内,以及月、日是否符合历法规则(特别是处理闰年的情况)。只有完全合法的日期,才能按格式输出出来。
注意避免在月和日的数字相同时,出现重复的检查和打印。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
// 存储每个月的天数。days[0]不用。
const int DAYS_IN_MONTH[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
/**
* @brief 将两位数年份转换为四位数。
* @param y 两位数年份 (0-99)。
* @return 规则:[60, 99] -> 19xx, [00, 59] -> 20xx。
*/
int to_full_year(int y) {
return y >= 60 ? 1900 + y : 2000 + y;
}
/**
* @brief 判断是否为闰年。
* @param y 四位数年份。
* @return 如果是闰年返回true,否则返回false。
*/
bool is_leap(int y) {
return (y % 400 == 0) || (y % 4 == 0 && y % 100 != 0);
}
/**
* @brief 验证并打印一个给定的日期 (如果合法)。
* @param y 四位数年份。
* @param m 月份。
* @param d 日期。
*/
void check_and_print(int y, int m, int d) {
// 检查年份范围
if (y < 1960 || y > 2059) return;
// 检查月份和日期基本有效性
if (m < 1 || m > 12 || d < 1) return;
// 获取当前月份的正确天数
int days_in_month = DAYS_IN_MONTH[m];
if (m == 2 && is_leap(y)) {
days_in_month = 29;
}
// 如果日期在有效范围内,则打印
if (d <= days_in_month) {
printf("%d-%02d-%02d\n", y, m, d);
}
}
/**
* @brief 处理年份确定后,剩下两个数字a和b作为月/日的两种可能性。
* 通过min/max保证先检查月份较小的日期,从而实现输出排序。
* @param year 确定的四位数年份。
* @param p1 第一个可能的月/日数字。
* @param p2 第二个可能的月/日数字。
*/
void process_month_day(int year, int p1, int p2) {
// 可能性 1: 月=min(p1,p2), 日=max(p1,p2)
check_and_print(year, min(p1, p2), max(p1, p2));
// 如果p1和p2不相等,才需要检查另一种排列
if (p1 != p2) {
// 可能性 2: 月=max(p1,p2), 日=min(p1,p2)
check_and_print(year, max(p1, p2), min(p1, p2));
}
}
int main() {
int a, b, c;
scanf("%d/%d/%d", &a, &b, &c);
int year_a = to_full_year(a);
int year_c = to_full_year(c);
if (year_a < year_c) {
// 1. 年份 a 更早,先检查所有和年份 a 相关的情况。
// 格式: 年/月/日 (a/b/c)
check_and_print(year_a, b, c);
// 2. 再检查所有和年份 c 相关的情况。
// 格式: 月/日/年 (a/b/c) 和 日/月/年 (b/a/c) -> 题目中是 a/b/c 和 b/a/c,但年份都是c
// 这里的p1, p2 应该是 a, b
process_month_day(year_c, a, b);
} else if (year_c < year_a) {
// 1. 年份 c 更早,先检查所有和年份 c 相关的情况。
process_month_day(year_c, a, b);
// 2. 再检查所有和年份 a 相关的情况。
check_and_print(year_a, b, c);
} else { // year_a == year_c (意味着 a == c)
// 此时,年/月/日 (a/b/c) 和 日/月/年 (c/b/a) 是同一个日期。
// 我们只需要检查 (年=a, 月=b, 日=a) 和 (年=a, 月=a, 日=b) 即可。
// process_month_day 恰好能处理这两种情况并按月/日排序。
process_month_day(year_a, a, b);
}
return 0;
}
习题:P9978 [USACO23DEC] Cycle Correspondence S
解题思路
首先,有些编号只出现在一套系统中,而没有出现在另一套中,这些谷仓不可能被赋予相同的编号。还有一些编号在两套系统中都没有出现,这些编号可以自由地分配给那些没有出现在 \(a\) 或 \(b\) 环中的谷仓。每个这样的编号都可以贡献一个匹配,因为可以让 a 和 b 都把这个编号分配给同一个“自由”的谷仓。最复杂的部分是处理那些同时出现在 \(a\) 环和 \(b\) 环中的编号,需要找到一种对齐方式,使得 \(a\) 环和 \(b\) 环中重合的编号最多。
\(a\) 环和 \(b\) 环都是环形结构,可以固定 \(b\) 环的位置,然后尝试旋转 \(a\) 环来与 \(b\) 对齐。“旋转 \(a\) 环”意味着可以将 \(a\) 环的 \(a_1\) 与 \(b\) 环的 \(b_j\) 对齐,其中 \(j\) 可以是 \(1\) 到 \(k\) 的任意值。对于一个固定的对齐方式(例如,\(a_1\) 对齐 \(b_j\)),\(a_2\) 就必须对齐 \(b_{j+1}\)(如果 \(j+1 \lt k\) 则为 \(b_1\)),以此类推。在这种对齐下,如果 \(a_i\) 恰好等于 \(b_{j+i-1}\),那么就获得了一个匹配。需要找到一个最佳的“偏移量”或“旋转量”,使得这种匹配的数量最多。
可以用一个数组 \(pos\) 来快速查找 \(b\) 环中每个编号的位置,\(pos_v = i\) 表示编号 \(v\) 在 \(b\) 环的第 \(i\) 个位置。现在,遍历 \(a\) 环。对于 \(a\) 环中的每个元素 \(a_i\),检查它是否存在于 \(b\) 环中(即 \(pos_{a_i} \ne 0\))。如果存在,\(a_i\) 在 \(a\) 环的位置是 \(i\),在 \(b\) 环的位置是 \(pos_{a_i}\)。这两个环要对齐,它们的位置之间需要一个固定的偏移量,这个偏移量 \(d\) 可以计算出来。遍历 \(a\) 环中所有也存在于 \(b\) 环中的元素,计算出它们各自对应的偏移量 \(d\)。用一个计数器数组 \(cnt\) 来统计每个偏移量 \(d\) 出现了多少次,\(cnt_{dis}\) 的值就表示,如果采用偏移量 \(d\) 来对齐两个环,能够产生多少个匹配。只需要找到 \(cnt\) 数组中的最大值,这个最大值就是 \(a\) 环和 \(b\) 环之间能产生的最大匹配数。
环可以是顺时针的,也可以是逆时针的,\(a_1, a_2, \dots, a_k\) 和 \(b_1,b_2, \dots, b_k\) 只是描述了相邻关系,\(a\) 的环可以是 \(a_1 \rightarrow a_2 \rightarrow \cdots\),也可以是 \(a_1 \rightarrow a_k \rightarrow a_{k-1} \rightarrow \cdots\)。所以,需要计算两种情况:\(a\) 环 \((a_1, \dots, a_k)\) 与 \(b\) 环对齐,\(a\) 环的反向 \((a_k, \dots, a_1)\) 与 \(b\) 环对齐。对这两种情况分别计算最大匹配数,然后取其中较大的一个。
最终答案等于 \(a\) 环和 \(b\) 环的最大匹配数 加上 未出现在 \(a\) 环或 \(b\) 环中的自由编号数量。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 500005;
bool vis[N]; // vis[i] = true 表示编号 i 出现在了 a 或 b 环中
int a[N], b[N]; // 存储 A 和 B 的编号序列
int pos[N]; // pos[v] = i 表示编号 v 在 b 环的第 i 个位置
int cnt[N]; // cnt[d] 统计偏移量为 d 的匹配出现了多少次
// 函数 solve:计算在给定 a 环和 b 环的情况下,通过旋转能得到的最大匹配数
int solve(int n, int k) {
for (int i = 1; i <= n; i++) cnt[i] = 0; // 初始化偏移量计数器
for (int i = 1; i <= k; i++) { // 遍历 a 环
if (pos[a[i]] == 0) continue; // 如果 a[i] 这个编号不在 b 环中,则跳过
// 计算偏移量 d
// a[i] 在 a 环的位置是 i,在 b 环的位置是 pos[a[i]]
// d 是 b 环相对于 a 环的顺时针偏移
int d = pos[a[i]] >= i ? pos[a[i]] - i : pos[a[i]] + k - i;
cnt[d]++; // 对应偏移量的计数器加一
}
int ret = 0;
for (int i = 0; i < k; i++) ret = max(ret, cnt[i]); // 找到所有偏移量中出现次数最多的那个
return ret;
}
int main()
{
int n, k;
scanf("%d%d", &n, &k);
// 读取 a 环,并标记出现过的编号
for (int i = 1; i <= k; i++) {
scanf("%d", &a[i]);
vis[a[i]] = true;
}
// 读取 b 环,建立编号对位置的映射,并标记出现过的编号
for (int i = 1; i <= k; i++) {
scanf("%d", &b[i]);
pos[b[i]] = i;
vis[b[i]] = true;
}
// 1. 计算 a 环正向与 b 环对齐的最大匹配数
int ans = solve(n, k);
// 2. 将 a 环反转
for (int i = 1; i * 2 <= k; i++) swap(a[i], a[k - i + 1]);
// 3. 计算 a 环反向与 b 环对齐的最大匹配数,并更新 ans
ans = max(ans, solve(n, k));
// 4. 加上那些从未在 a 环或 b 环中出现过的“自由”编号的数量
for (int i = 1; i <= n; i++)
if (!vis[i]) ans++;
printf("%d\n", ans);
return 0;
}
数组传参
参数除了可以传普通变量外,也可以传数组。
- 传一维数组
- 函数实现:
void f(int *a)或者void f(int a[]) - 函数调用:
f(a)
- 函数实现:
- 传二维数组
- 函数实现:
void f(int **a)或者void f(int a[][25]),注意第二维长度必须写要传入的数组在声明时第二维的长度,更高维的话需要从第二维开始把声明的长度全写上 - 函数调用:
f(a)
- 函数实现:
例题:P5738 【深基7.例4】歌唱比赛
解题思路
这道题要实现的功能稍微复杂一些,首先可以用二维数组读入数据。
接下来应该对每一行(可以看做一个又一个一维数组)求和、最大值、最小值。
可以通过自定义函数来简化代码。
参考代码
#include <cstdio>
#include <algorithm>
int score[105][25];
double calc(int *a, int n) {
int sum = 0, mx = -1, mn = 11;
for (int i = 1; i <= n; i++) {
sum += a[i];
if (a[i] > mx) mx = a[i];
if (a[i] < mn) mn = a[i];
}
return 1.0 * (sum - mx - mn) / (n - 2);
}
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &score[i][j]);
double ans = 0;
for (int i = 1; i <= n; i++) ans = std::max(ans, calc(score[i], m));
printf("%.2f\n", ans);
return 0;
}
也可以把求和、最大值、最小值再分别写个函数,在 calc 里调用那些函数,自己写的函数中是可以调用在该函数之前已经定义了的函数的。
参考代码
#include <cstdio>
#include <algorithm>
int score[105][25];
int getsum(int *a, int n) {
int sum = 0;
for (int i = 1; i <= n; i++) sum += a[i];
return sum;
}
int getmin(int *a, int n) {
int mn = 11;
for (int i = 1; i <= n; i++) mn = std::min(mn, a[i]);
return mn;
}
int getmax(int *a, int n) {
int mx = -1;
for (int i = 1; i <= n; i++) mx = std::max(mx, a[i]);
return mx;
}
double calc(int *a, int n) {
return 1.0 * (getsum(a, n) - getmin(a, n) - getmax(a, n)) / (n - 2);
}
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &score[i][j]);
double ans = 0;
for (int i = 1; i <= n; i++) ans = std::max(ans, calc(score[i], m));
printf("%.2f\n", ans);
return 0;
}
另外,如果想传二维数组的某一行当参数,也可以把二维数组开成全局变量,然后用一个 int 表示行号,代替数组作为参数,这样更适用于传一列等更多情况。
参考代码
#include <cstdio>
#include <algorithm>
int score[105][25];
int getsum(int id, int n) {
int sum = 0;
for (int i = 1; i <= n; i++) sum += score[id][i];
return sum;
}
int getmin(int id, int n) {
int mn = 11;
for (int i = 1; i <= n; i++) mn = std::min(mn, score[id][i]);
return mn;
}
int getmax(int id, int n) {
int mx = -1;
for (int i = 1; i <= n; i++) mx = std::max(mx, score[id][i]);
return mx;
}
double calc(int id, int n) {
return 1.0 * (getsum(id, n) - getmin(id, n) - getmax(id, n)) / (n - 2);
}
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &score[i][j]);
double ans = 0;
for (int i = 1; i <= n; i++) ans = std::max(ans, calc(i, m));
printf("%.2f\n", ans);
return 0;
}
习题:P8091 [USACO22JAN] Non-Transitive Dice B
解题思路
题目给定了骰子 A 和 B 的点数,而骰子 C 是未知的,需要“寻找”一个符合条件的 C。关键限制条件是:骰子 C 的四个面,每个面上的数字都必须是 1 到 10 之间的整数。这个限制使得所有可能的骰子 C 的组合是有限的,具体来说,C 的 4 个面,每个面有 10 种选择,总可能性是 \(10^4 = 10000\) 种。因此直接尝试每一种可能的 C,看它是否满足条件。
参考代码
#include <cstdio>
// a, b, c 分别代表三个骰子的点数数组
int a[5], b[5], c[5];
/**
* @brief 判断骰子x是否“击败”骰子y。
*
* @param x 骰子x的点数数组。
* @param y 骰子y的点数数组。
* @return 如果x击败y,返回true;否则返回false。
*/
bool win(int x[], int y[]) {
int cnt1 = 0; // 记录 x > y 的次数
int cnt2 = 0; // 记录 x < y 的次数
// 遍历两个骰子所有可能的 4*4=16 种结果组合
for (int i = 1; i <= 4; i++) {
for (int j = 1; j <= 4; j++) {
if (x[i] > y[j]) {
cnt1++;
} else if (x[i] < y[j]) {
cnt2++;
}
// x[i] == y[j] 的情况是平局,不计数
}
}
// 根据题意,如果x获胜的组合数严格大于y获胜的组合数,则x击败y
return cnt1 > cnt2;
}
bool solve() {
// 使用四层嵌套循环,暴力枚举骰子C的四个面的所有可能点数 (1-10)
for (c[1] = 1; c[1] <= 10; c[1]++) {
for (c[2] = 1; c[2] <= 10; c[2]++) {
for (c[3] = 1; c[3] <= 10; c[3]++) {
for (c[4] = 1; c[4] <= 10; c[4]++) {
// 对于每一个枚举出的骰子C,检查是否存在非传递关系
// 检查第一种循环: A -> B -> C -> A
if (win(a, b) && win(b, c) && win(c, a)) return true;
// 检查第二种循环: B -> A -> C -> B
if (win(b, a) && win(a, c) && win(c, b)) return true;
}
}
}
}
// 如果所有可能的C都试过了,仍未找到,则说明不存在
return false;
}
int main()
{
int t; // 测试用例的数量
scanf("%d", &t);
while (t--) {
// 读取骰子A和B的点数
for (int j = 1; j <= 4; j++) scanf("%d", &a[j]);
for (int j = 1; j <= 4; j++) scanf("%d", &b[j]);
// 调用solve函数求解
if (solve()) {
printf("yes\n");
} else {
printf("no\n");
}
}
return 0;
}
函数互相调用
自定义的函数可以调用在该函数之前定义的函数。
不过,如果两个函数想要互相调用,即 a 中调用 b,b 中调用 a 也是可以做到的。
#include <cstdio>
void a(int x); // 定义函数a
void b(int x); // 定义函数b
void a(int x) { // 实现函数a
// return只返回一层,这里是因为一路都只有一个分支,所以返回到main里
// 如果一层有多个分支,是返回到上一层继续走接下来的分支
if (x <= 0) return;
printf("a: %d\n", x);
b(x - 1);
}
void b(int x) { // 实现函数b
// return只返回一层,这里是因为一路都只有一个分支,所以返回到main里
// 如果一层有多个分支,是返回到上一层继续走接下来的分支
if (x <= 0) return;
printf("b: %d\n", x);
a(x - 1);
}
int main()
{
int n; scanf("%d", &n);
a(n);
return 0;
}
这个写法其实是把函数的定义和实现拆开了。
结构体
有时候要大量存储批量数据,比如学生的信息,可以考虑数组。但是数组只能存储一组同样数据类型的信息,如果同时记录考生的姓名、成绩等不同的信息,就不能使用一个数组来存储了。结构体可以将一些不同类型的信息聚合成整体,以便处理这些信息。
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。比如,一名学生有姓名信息(字符串),有成绩信息(整数)。结构体定义的一般形式如下:
struct 类型名 {
数据类型1 成员变量1;
数据类型2 成员变量2;
...
};
已经定义过的类型名 结构体变量名;
例题:P5740 [深基7.例9] 最厉害的学生
现有 \(N (N \le 1000)\) 名同学参加了期末考试,并且获得了每名同学的信息:姓名(不超过 \(8\) 个字符的字符串,没有空格),语文、数学、英语成绩(均为不超过 \(150\) 的自然数)。总分最高的学生就是最厉害的,请输出最厉害学生的各项信息(姓名、各科成绩)。如果有多个总分相同的学生,输出靠前的那位。
分析:每个学生的信息使用结构体存储,每次比较当前总分最大的答案和枚举到的学生的总分,如果后者更大,就把当前学生的信息(一个结构体类型的变量)赋值给答案变量,代码如下:
#include <cstdio>
const int N = 1005;
const int LEN = 15;
struct Student {
char name[LEN];
int chi, math, eng;
// 定义一个结构体记录每个学生的信息
}; // 定义了一种Student类型,里面包括name chi math eng四个成员
// 其中name是char数组,成绩是整数类型
int main()
{
int n; scanf("%d", &n);
Student ans; // 定义了Student类型的变量ans,用来记录答案
int maxsum = -1; // 考虑有可能最厉害的学生总分是0分
for (int i = 1; i <= n; i++) {
Student s;
scanf("%s%d%d%d", s.name, &s.chi, &s.math, &s.eng);
int sum = s.chi + s.math + s.eng;
if (sum > maxsum) {
maxsum = sum; // 如果找到更厉害的学生,则更新答案
ans = s; // 用一个结构体对另一个结构体赋值
}
}
printf("%s %d %d %d\n", ans.name, ans.chi, ans.math, ans.eng);
return 0;
}
例题:P5741 [深基7.例10] 旗鼓相当的对手 - 加强版
现有 \(N \ (N \le 1000)\) 名同学参加了期末考试,并且获得了每名同学的信息:姓名(不超过 \(8\) 个字符的字符串,没有空格),语文、数学、英语成绩(均为不超过 \(150\) 的自然数)。如果某对学生 \(\left \langle i,j \right \rangle\) 的每一科成绩的分差都不大于 \(5\),且总分分差不大于 \(10\),那么这对学生就是“旗鼓相当的对手”。现在想知道这些同学中,哪些是“旗鼓相当的对手”,请输出他们的姓名。
#include <cstdio>
#include <cmath>
const int N = 1005;
struct Info {
char name[10];
int s[4]; // s[0]: chinese, s[1]: math, s[2]: english, s[3]: sum
};
Info a[N];
bool check(int delta, int threshold) { // 检查某个分差是否不超过一个数
return abs(delta) <= threshold;
}
bool near(int i, int j) { // 判断两者是否“旗鼓相当”
for (int k = 0; k < 3; k++)
if (!check(a[i].s[k] - a[j].s[k], 5)) return false;
return check(a[i].s[3] - a[j].s[3], 10);
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%s", a[i].name);
a[i].s[3] = 0;
for (int j = 0; j < 3; j++) {
scanf("%d", &a[i].s[j]); a[i].s[3] += a[i].s[j];
}
}
for (int i = 1; i <= n; ++i) // 枚举第一个学生i
for (int j = i + 1; j <= n; ++j) // 枚举第二个学生j
if (near(i, j)) printf("%s %s\n", a[i].name, a[j].name);
return 0;
}
例题:P5742 [深基7.例11] 评等级
现有 \(N \ (N \le 1000)\) 名同学,每名同学需要设计一个结构体记录以下信息:学号(不超过 \(100000\) 的正整数)、学业成绩和素质拓展成绩(分别是 \(0 \sim 100\) 的整数)、综合分数(实数)。每行读入同学的学号、学业成绩和素质拓展成绩,并且计算综合分数(分别按照 \(70\%\) 和 \(30\%\) 的权重累加),存入结构体中。还需要再结构体中定义一个成员函数,返回该结构体对象的学业成绩和素质拓展成绩的总分。然后需要设计一个函数,其参数是一个学生结构体对象,判断该学生是否“优秀”。优秀的定义是学业和素质拓展成绩总分大于 \(140\) 分,且综合分数不小于 \(80\) 分。
#include <cstdio>
const int N = 1005;
struct Student {
int id, a, b;
int overall() { // 成员函数,返回学业分和素质分的总分
return a + b;
}
};
int is_excellent(Student s) {
// 访问成员变量,调用成员函数
return s.a * 7 + s.b * 3 >= 800 && s.overall() > 140;
}
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) {
Student stu;
scanf("%d%d%d", &stu.id, &stu.a, &stu.b);
if (is_excellent(stu)) { // 将结构体变量作为参数传递
printf("Excellent\n");
} else {
printf("Not excellent\n");
}
}
return 0;
}
习题:P5744 [深基7.习9] 培训
某培训机构的学员有如下信息:
(1)姓名(字符串);
(2)年龄(周岁,整数);
(3)去年 NOIP 成绩(整数,且保证是 \(5\) 的倍数)。
经过为期一年的培训,所有同学的成绩都有所提高,提升了 \(20\%\)(NOIP 满分是 \(600\) 分,不能超过这个得分)。
输入学员信息,请设计一个结构体存储这些学生信息,并设计一个函数模拟培训过程,其参数是这样的结构体类型,返回同样的结构体类型,并输出学员信息。
参考代码
#include <iostream>
#include <string>
#include <algorithm>
using std::cin;
using std::cout;
using std::string;
using std::min;
struct Student {
string name;
int age, score;
};
Student train(Student s) {
s.age++;
s.score = min(s.score / 5 * 6, 600);
return s;
}
int main()
{
int n; cin >> n;
for (int i = 1; i <= n; i++) {
Student stu;
cin >> stu.name >> stu.age >> stu.score;
stu = train(stu);
cout << stu.name << " " << stu.age << " " << stu.score << "\n";
}
return 0;
}
class 与 struct
C++ 除了支持结构体 struct 以外,还支持类 class。在工程中,一般用 struct 定义“纯数据”的类型,只包含较少的辅助成员函数,而用 class 定义“拥有复杂行为”的类型。通常出于简单起见,竞赛中的代码主要使用 struct。“成员变量”、“成员函数”、“构造函数”这些概念对于两者都是适用的,所以在 struct 中学习到的一些东西不影响对 class 的学习。
从语言机制上讲,在 C++ 中 struct 和 class 最主要的区别是默认访问权限和继承方式不同,而其他方面的差异很小。
区别 1:struct 的默认访问权限是 public,class 默认是 private

区别 2:struct 默认的继承方式是 public,class 默认的继承方式是 private

选择题:C++ 是一种面向对象的程序设计语言。在 C++ 中,下面哪个关键字用于声明一个类,其缺省继承方式为 private 继承?
A. union / B. struct / C. class / D. enum
答案
C
STL pair
pair 可以用来将两个数据成员打包成一个对象,两个数据成员的类型可以一样也可以不一样。pair 在头文件 <utility> 中定义。
pair中的第一个成员称为first,第二个成员称为second,顺序是固定的,取两个成员的方式是xxx.first和xxx.second。pair可以用来赋值、拷贝、比较。pair之间的比较方式是先比较第一个成员,再比较第二个成员。
pair 的使用语法如 pair<type1, type2> p,其中 type1 对应第一个成员的数据类型,type2 对应第二个成员的数据类型。
#include <cstdio>
#include <utility>
using std::pair;
int main()
{
pair<int, char> p;
p.first = 100; p.second = 'G';
printf("%d %c\n", p.first, p.second); // 100 G
return 0;
}
pair 有几种不同的初始化方式:
#include <cstdio>
#include <utility>
using std::pair;
using std::make_pair;
using pic = pair<int, char>; // 因为pair完整的类型通常较长,可以起个别名
int main()
{
pic p1; // 初始化为两种类型的默认值
pic p2(1, 'a');
pic p3(1, 97); // 97会转换到char,这个pair的值和上面的p2相等
pic p4(p3); // 初始化的值从p3拷贝过来
pic p5 = make_pair(1, 'a'); // make_pair函数用来构造一个pair对象
pic p6 = {1, 'a'};
return 0;
}

浙公网安备 33010602011771号