函数与结构体

函数

程序中有时会多次使用相同的语句,而且无法通过循环来减少重复编程。对于这样的代码块,可以将其封装成一个函数。每个程序都用到了主函数 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 小时结果为负,相等时结果为 0b 小时结果为正
  • strcat(a, b) 拼接字符串 ab
  • strcpy(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) 时:

image

  1. 进入 main() 函数(主程序),主程序中三个 dist() 函数调用形式是一致的,以 dist(x2,y2,x3,y3) 举例,程序运行到此处时收集了 \(4\)参数 [3,0,0,4],然后传递给 dist() 函数,也就是调用 dist(3,0,0,4)
  2. 进入 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)
  3. 进入 sq() 函数,接收到了传递来的 \(1\) 个参数,按照顺序分配给参数列表中唯一的变量 x,因此 x=3。经过计算后,返回结果 \(9.0\)
  4. 回到 dist() 函数,得到了 sq(x1-x2) 的值是 \(9\)。同理,sq(y1-y2) 的值是 \(16\)。加起来取平方根,得到结果 \(5.0\),返回这个结果。
  5. 回到 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 类型的变量。在主程序中读入 xy 后,依次考虑 xy 之间的每一个年份,用 isleap() 函数调用对应的年份。进入 isleap() 函数后,调用的数字就成为了函数内的 year 变量的值。根据闰年的判断条件,返回 true 或者 false 代表是闰年或者不是闰年。回到主程序,得到了函数返回的结果,如果返回的是 true,就计数一次,并将这个年份存入数组。最后将总数和存下来的闰年年份输出。

习题:P8651 [蓝桥杯 2017 省 B] 日期问题

解题思路

首先计算出 AACC 分别代表的四位数年份,程序应该先处理年份更早的所有可能性。例如,对于 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 中调用 bb 中调用 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

image

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

image

选择题:C++ 是一种面向对象的程序设计语言。在 C++ 中,下面哪个关键字用于声明一个类,其缺省继承方式为 private 继承?

A. union / B. struct / C. class / D. enum

答案

C

STL pair

pair 可以用来将两个数据成员打包成一个对象,两个数据成员的类型可以一样也可以不一样。pair 在头文件 <utility> 中定义。

  • pair 中的第一个成员称为 first,第二个成员称为 second,顺序是固定的,取两个成员的方式是 xxx.firstxxx.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;
}
posted @ 2023-07-25 14:29  RonChen  阅读(122)  评论(0)    收藏  举报