把「判断闰年」「算两点距离」「判断素数」这些逻辑各装一个盒子,主程序就只剩下一句句人话
| 0 - 25 min | 函数三要素、怎么写、怎么调用(第 1-2 节) |
| 25 - 50 min | 值传递 / 引用传递、全局数组 / 局部数组(第 3 节) |
| 50 - 70 min | 两个必背模板:判素数、判闰年(第 4 节) |
| 70 - 160 min | 做 4 道题,每题限时 30 分钟 |
| 160 - 180 min | 对照本文题解补错题 + 做自测 |
昨天你其实已经在写函数了,只是没意识到:P1553 里的 stripLeft、revInt,
P1200 里的 nameToNumber —— 那些就是函数。
它们带来了什么好处?main 里只剩四行 if-else,一眼就能看懂「先分类、再各自处理」的骨架, 细节全都藏进了一个个有名字的小盒子里。今天就正式把这件事学明白。
题目要求:判断 5 个数是不是素数。
// 没有函数的写法
int main(){
int a[5];
for(int i = 0; i < 5; i++) cin >> a[i];
for(int i = 0; i < 5; i++){
bool ok = true;
if(a[i] < 2) ok = false;
else for(int j = 2; j * j <= a[i]; j++)
if(a[i] % j == 0){ ok = false; break; }
cout << a[i] << (ok ? " 是素数" : " 不是素数") << endl;
}
}
再看有函数的写法:
// 有函数的写法 bool isPrime(int n){ /* ……判断逻辑…… */ } int main(){ int a[5]; for(int i = 0; i < 5; i++) cin >> a[i]; for(int i = 0; i < 5; i++) cout << a[i] << (isPrime(a[i]) ? " 是素数" : " 不是素数") << endl; }
第二段代码的 main 里,「怎么判断素数」这件事消失了,只剩下一句 isPrime(a[i]) ——
读代码的人一眼就懂,不用去抠那一堆循环和 break。
① 好读:isPrime(x)、isLeap(y)、dist(x1,y1,x2,y2) 这些名字本身就是注释,
主程序读起来像人话。
② 好改:判断逻辑写错了,只需要改函数里那一处,不用满屏找。
③ 好复用:P1304 要判断上万次素数,P5736 要判断上百次 —— 同一个函数到处调用,代码只写一遍。
你可以在函数里临时加一句 cout << "check " << n << endl; 看它被调用了哪些参数,
查完删掉就行。如果所有逻辑挤在 main 里,你根本不知道该在哪打这条语句。
返回类型 函数名 (参数列表) {
函数体
return 返回值; // 返回类型是 void 时可以不写
}
拿昨天的代码当例子:
double dist(double x1, double y1, double x2, double y2){
return sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
}
| 要素 | 这里是什么 | 怎么想 |
|---|---|---|
| 返回类型 | double | 算出来的距离是小数 → 用 double |
| 函数名 | dist | 起一个「一看就知道干什么」的名字 |
| 参数列表 | 四个 double | 要算距离,必须先知道两个点的坐标 |
| 函数体 | return ... | 干活的地方,最后 return 交作业 |
| 函数要给出什么 | 返回类型 | 例子 |
|---|---|---|
| 是 / 否 的结论 | bool | bool isPrime(int n) |
| 一个整数结果 | int / long long | int countLeap(int x, int y) |
| 一个小数结果 | double | double dist(...) |
| 什么都不用返回(只打印 / 只修改参数) | void | void printAns(int x) |
isPrime(是素数吗)、isLeap(是闰年吗)、countLeap(数闰年)、
dist(求距离)、nameToNumber(名字转数字)。
返回 bool 的函数用 is / has 开头,这几乎是全世界的默契。
① 交作业:把你的结果送回调用处。
② 立刻结束函数:return 一执行,函数马上停,后面的代码都不再走。
正因为作用 ②,素数判断可以写得这么短:
bool isPrime(int n){
if(n < 2) return false; // 提前下班
for(int i = 2; i * i <= n; i++){
if(n % i == 0) return false; // 找到一个因数就收工,不用再往下找
}
return true; // 一个因数都没找到 → 是素数
}
C++ 是从上往下读的。你把函数写在 main 后面,main 里调用的时候编译器还不认识这个名字,
会报 'isPrime' was not declared in this scope。
两种解法:
① 直接把函数整个写在 main 前面(推荐,最省事);
② 或者先写一行「函数声明」(也叫函数原型),把函数体留在后面:
bool isPrime(int n); // 声明:告诉编译器「有这个函数」 int main(){ // 这里就可以调用 isPrime 了 } bool isPrime(int n){ // 定义:真正的实现 // …… }
默认情况下,参数是值传递 —— 调用时会把实参的值复制一份给形参。函数里改的是这份复印件,外面的变量毫发无伤。
写个交换两个数的函数试试(真实运行结果):
void swapBad(int a, int b){ // 值传递
int t = a; a = b; b = t;
}
void swapGood(int &a, int &b){ // 引用传递:多了一个 &
int t = a; a = b; b = t;
}
int main(){
int x = 1, y = 2;
swapBad(x, y);
cout << "值传递之后: x=" << x << " y=" << y << endl;
swapGood(x, y);
cout << "引用传递之后: x=" << x << " y=" << y << endl;
}
值传递之后: x=1 y=2 引用传递之后: x=2 y=1
值传递:白忙一场,x、y 一点没变。引用传递:交换成功。
| 写法 | 名字 | 函数里改 a,外面的 x 会变吗 |
|---|---|---|
| void f(int a) | 值传递 | 不会(改的是复印件) |
| void f(int &a) | 引用传递 | 会(a 就是 x 的另一个名字) |
「引用」可以理解成给同一个变量起了个别名。void f(int &a) 里的 a,和外面的 x 就是同一个东西,
改哪个都是改它。
① 需要修改实参时(比如交换、把答案写回参数里)。
② 传递「大东西」时:传数组、传 string、传结构体。值传递要把整个东西复制一遍,
又慢又费内存;用引用 const string &s 就只是「贴个名字」,零复制。
③ 其他情况(int、double 这种小东西):值传递更安全,函数绝对改不坏你的变量。
数组有个特殊之处:把数组传进函数时,传的不是整份拷贝,而是首地址。
所以函数里改了数组元素,外面会跟着变 —— 哪怕你没写 &。
void addOne(int a[], int n){ // 中括号里可以不写长度
for(int i = 0; i < n; i++) a[i]++; // 外面的数组真的会被改
}
因为数组传进去之后只知道自己开头在哪,不知道自己有多长。所以习惯上总是这么写:
void f(int a[], int n) —— 数组 + 长度,两个参数一起传。
另外函数里用 sizeof(a) 得到的不是数组的总字节数(而是指针的大小,通常是 8),别用它算长度。
「全局」就是写在所有函数外面(包括 main 外面)的变量;「局部」是写在函数体里面的变量。两者最大的区别是:
| 放哪 | 存在哪 | 多大 | 会自动清零吗 |
|---|---|---|---|
| 全局(函数外) | 数据段 | 受内存限制,能开很大 | ✅ 会 |
| 局部(函数内) | 栈 | 只有 1~2 MB 左右 | ❌ 不会,是随机值 |
我用同一段填数组求和的代码,只改「数组放里放外」跑了一遍(真实结果):
| 数组 | 大小 | 位置 | 实测结果 |
|---|---|---|---|
| int a[250000] | 1.0 MB | 局部 | ✅ 正常输出 31249875000 |
| int a[500000] | 1.9 MB | 局部 | ✅ 正常输出 124999750000 |
| int a[1000000] | 3.8 MB | 局部 | ❌ 爆栈崩掉,退出码 0xC00000FD |
| int a[3000000] | 11.4 MB | 局部 | ❌ 爆栈崩掉,退出码 0xC00000FD |
| int a[3000000] | 11.4 MB | 全局 | ✅ 正常输出 4499998500000 |
0xC00000FD 就是 Windows 的「栈溢出」(Stack Overflow)。
数组一开大,程序连第一行都跑不到就直接死掉 —— 而且它不一定每次都崩,
有时候只是「莫名其妙 WA」,这是竞赛里最难查的一类 bug。
本机实测的临界点就在 2MB 附近:1.9MB 还行,3.8MB 就崩。所以规矩很简单:
凡是几百 KB 以上的数组(大概 int a[100000] 往上),统统写在 main 外面。
写在全局还有两个附带好处:自动初始化为 0(不用写 = {0}),
而且写函数的时候可以直接用,不用一个个当参数传来传去。
它的素数筛要开一个 bool isPrime[10005] 的数组,而且要在循环里反复访问。
把这个数组开成全局,函数和 main 都能直接用,代码立刻清爽一大截。
今天的 4 道题,全都是在考这两个模板 + 一点包装。背下来,以后遇到直接用。
bool isPrime(int n){
if(n < 2) return false; // 0 和 1 都不是素数
for(int i = 2; (long long)i * i <= n; i++){ // 只需试到 sqrt(n)
if(n % i == 0) return false; // 找到因数 → 不是素数
}
return true; // 一直没有因数 → 是素数
}
两个必须记住的点:
① 为什么要 n < 2 特判?因为 0 和 1 都不是素数(素数定义:大于 1 且只有 1 和自身两个因数)。
如果不判,n = 1 会跳过循环直接 return true,把 1 当成素数 —— 今天 P5736 的输入里就有 1 这种数。
② 为什么只试到 √n?因为因数总是成对出现的:如果 n = a × b 且 a ≤ b,那么 a ≤ √n。
也就是说,n 如果有因数,一定有一个不超过 √n。所以试到 √n 就够了 ——
判断 10⁵ 只需要试 316 次,而不是 10⁵ 次。
(long long)i * i <= n
如果 n 接近 int 上限(约 2.1×10⁹),那么 i 也会接近 46341,
而 i * i 会超过 int 上限 → 溢出成负数 → 循环条件永远成立 → 死循环或乱答。
把 i 临时转成 long long 再乘,就不会溢出。今天 ai ≤ 10⁵ 用不到,但这个习惯必须从现在养起。
另一种同样安全的写法是 i <= n / i(用除法代替乘法)。
能被 4 整除,且不能被 100 整除;或者能被 400 整除。
bool isLeap(int y){
if(y % 400 == 0) return true; // 2000 是闰年
if(y % 100 == 0) return false; // 1900 不是闰年
return y % 4 == 0; // 其余的看能不能被 4 整除
}
为什么判断顺序是先 400 再 100?因为 2000 既能被 400 整除、也能被 100 整除, 两条规则冲突。所以必须先捡出「能被 400 整除」这一种,剩下的才轮得到「能被 100 整除就否掉」。
也可以写成一行:
return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
两种写法都行,但多行版更好调试 —— 万一判错,你能一眼看出是哪条规则的问题。
| 年份 | 能被4整除 | 能被100整除 | 能被400整除 | 是闰年吗 |
|---|---|---|---|---|
| 1992 | ✅ | ❌ | ❌ | 是 |
| 1900 | ✅ | ✅ | ❌ | 不是(世纪年陷阱) |
| 2000 | ✅ | ✅ | ✅ | 是 |
| 2001 | ❌ | ❌ | ❌ | 不是 |
下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译、实际运行过,输出是你看到的真实结果。
题意:给出平面上不共线的三个点的坐标(实数),求这三点围成的三角形的周长,保留两位小数。
坐标都是实数,绝对值不超过 100,小数点后最多 3 位。
· 坐标有小数 → 必须用 double,不能用 int;
· 距离公式是 √((x2−x1)² + (y2−y1)²),要用 sqrt()(需要 <cmath>);
· 保留两位小数 → printf("%.2f")(需要 <cstdio>)。
这题是「把公式封装成函数」的教科书例子。三点围成的三角形,三条边正好是 AB、BC、CA:
周长 = dist(A,B) + dist(B,C) + dist(C,A)
写一个 dist(x1, y1, x2, y2) 求两点距离,然后调用三次 ——
这就是函数最大的价值:公式只写一遍,用几次调几次。
输入是三行、每行两个数,但 cin 会自动跳过所有空白(空格、换行都算空白),
所以直接 cin >> x1 >> y1 >> x2 >> y2 >> x3 >> y3; 一次读完就行,完全不用管它在哪一行换行。
0 0 0 3 4 0
12.00
手算核对:(0,0)-(0,3) 长度 3;(0,3)-(4,0) 长度 √(16+9) = 5;(4,0)-(0,0) 长度 4。3+5+4 = 12 → 12.00 ✅
#include <iostream> #include <cstdio> #include <cmath> using namespace std; // 求两点之间的距离(今天的主角:把公式封装成函数) double dist(double x1, double y1, double x2, double y2){ return sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); } int main(){ double x1, y1, x2, y2, x3, y3; cin >> x1 >> y1 >> x2 >> y2 >> x3 >> y3; double ans = dist(x1, y1, x2, y2) + dist(x2, y2, x3, y3) + dist(x3, y3, x1, y1); printf("%.2f\n", ans); // 保留两位小数 return 0; }
官方样例 0 0 / 0 3 / 4 0 → 12.00 完全一致
额外测试(真实输出):
· 0 0 / 1 0 / 0 1 → 3.41(1 + 1 + √2 ≈ 3.414213… 保留两位 3.41)✅
· 100.00 100.00 / -100.00 -100.00 / 100.00 -100.00 → 682.84
(200 + 200 + 282.8427… = 682.8427… → 682.84)✅
-O2 -Wall 编译无任何警告。
int 读坐标:输入 0.5 这种小数会被读成 0,直接错。坐标是实数就必须 double。float:精度只有 7 位左右,本题数据小,勉强能过,但没必要冒这个险 —— 竞赛里小数一律 double。(x2-x1)^2:C++ 里 ^ 是异或,不是乘方!必须老老实实写成 (x2-x1)*(x2-x1),或者用 pow(x2-x1, 2)(不推荐,慢且返回 double)。#include <cmath>:sqrt 找不到。用 printf 则要 <cstdio>。cout << ans:会打出 12 或 12.0000000 这样的一串,不是两位小数。要么 printf("%.2f"),要么 cout << fixed << setprecision(2)(后者要 <iomanip>)。题意:读入 n 个正整数,把其中的合数和 1 全部筛掉,按原顺序输出剩下的素数,用空格隔开。
1 ≤ n ≤ 100,1 ≤ ai ≤ 10⁵。
· ai 最小是 1,而 1 不是素数 —— 这是本题最大的坑(很多人的 isPrime 会把 1 判成素数);
· 10⁵ 的平方根约 316,所以试除法最多试 316 次,100 个数总共 3 万多次运算,快到可以忽略;
· 题目要求「全部储存在数组中」—— 所以用一个数组存读入,再遍历判断。
① 先把 n 个数读进数组(题目明确要求用数组存)。
② 写 isPrime 函数,即第 4 节的模板一:小于 2 直接否掉,然后从 2 试到 √n。
③ 遍历数组,是素数就输出,注意用 first 标记控制空格,避免行尾多一个空格。
埃氏筛适合「筛一次、查很多次」。本题 n 只有 100、值也只有 10⁵,对每个数单独试除反而更简单 —— 没有额外数组,也没有「哪些数需要筛」的边界问题。
真正需要筛的是今天的 P1304(要判断几千个数),那里我们就会换上筛法。什么规模用什么工具,别为了炫技把简单题写复杂。
5 3 4 5 6 7
3 5 7
#include <iostream> using namespace std; // 判断一个数是不是质数 bool isPrime(int n){ if(n < 2) return false; // 0 和 1 都不是质数 for(int i = 2; (long long)i * i <= n; i++){ // 只需试到 sqrt(n) if(n % i == 0) return false; } return true; } int main(){ int n; cin >> n; int a[105]; for(int i = 0; i < n; i++) cin >> a[i]; // 题目要求先全部存进数组 bool first = true; for(int i = 0; i < n; i++){ if(isPrime(a[i])){ if(!first) cout << " "; // 不是第一个才补空格 first = false; cout << a[i]; } } cout << endl; return 0; }
官方样例 3 4 5 6 7 → 3 5 7 完全一致
额外测试(真实输出):
· 1 个数,值为 1 → 输出一个空行(1 不是素数,被正确筛掉)✅
· 1 2 99991 100000 97 → 2 99991 97(99991、97 都是素数,100000 是偶数被筛掉)✅
· n = 100、100 个数全是 100000 → 空行,耗时 15 ms✅
isPrime 忘了 n < 2 的特判:那么 1 会被判成素数输出 —— 而本题的 ai 正好可以等于 1。i <= n:会慢到 10⁵ 次,而且逻辑上也多余(试到 √n 就够)。i < n / 2:虽然结果对,但没必要试那么多。用 i * i <= n 最省事。first 标记,或写 if(i != 0) cout << " ";。a[105] 够了,但别开 a[100](下标只到 99)。题意:输入 x 和 y,第一行输出 [x, y] 区间里闰年的个数,第二行按从小到大输出所有闰年年份,空格隔开。
1582 ≤ x < y ≤ 3000(注意是严格小于,区间至少跨两年,不会出现 x = y)。
· 区间长度最多约 1418 年,直接一年一年枚举完全没问题;
· 1582 年是格里高利历开始启用的年份 —— 这题的数据正好避开「历法改革前怎么算闰年」的麻烦,你只管用标准规则判;
· 因为 x < y 且每年都判,天然就是从小到大,不用排序。
① 先把 isLeap(y) 函数写好(模板二:400 → 100 → 4 的顺序判断)。
② 数个数:从 x 到 y 走一遍,是闰年就 cnt++,先输出 cnt。
③ 再输出年份:从 x 到 y 再走一遍(或者把两步合并),是闰年就输出 + 空格处理。
两次循环其实可以合成一次,但这里分两步写更清楚:题目要求先给出总数,再列出明细, 分两步正好对应题目的输出顺序,不容易乱。
1989 2001
3 1992 1996 2000
手算核对:1989~2001 之间能被 4 整除的有 1992、1996、2000。 其中 2000 是世纪年,但能被 400 整除,所以是闰年 ✅(如果是 1900,就要被剔掉)
#include <iostream>
using namespace std;
// 判断闰年:能被 4 整除但不能被 100 整除,或者能被 400 整除
bool isLeap(int y){
if(y % 400 == 0) return true;
if(y % 100 == 0) return false;
return y % 4 == 0;
}
int main(){
int x, y;
cin >> x >> y;
int cnt = 0;
for(int i = x; i <= y; i++)
if(isLeap(i)) cnt++;
cout << cnt << endl;
bool first = true;
for(int i = x; i <= y; i++){
if(isLeap(i)){
if(!first) cout << " ";
first = false;
cout << i;
}
}
cout << endl;
return 0;
}
官方样例 1989 2001 → 第一行 3,第二行 1992 1996 2000 完全一致
额外测试(真实输出):
· 1896 1904 → 2 / 1896 1904
(1900 被正确跳过,这正是世纪年陷阱的验证点)✅
· 1582 1583 → 0 / 空行(区间内一个闰年都没有,第二行就是空的,符合题目要求)✅
· 最大范围 1582 3000 → 344 / 从 1584 到 2996 的完整列表,耗时 13 ms
(列表里同样看不到 1700、1800、1900,能看到 1600、2000、2400)✅
y % 4 == 0 && y % 100 != 0 就完了,2000 会被漏掉,样例直接挂。i < y:区间是闭区间 [x, y],要用 i <= y,否则会漏掉 y 这一年。first 标记处理。题意:输入一个偶数 N,把 4 到 N 之间的每一个偶数都写成「两个质数之和」,
每行一个,形如 10=3+7。如果一个偶数有多种拆法,输出第一个加数最小的那一种。
题面举的例子很关键:10 = 3+7 = 5+5,必须输出 10=3+7(3 比 5 小),写 10=5+5 判错。
4 ≤ N ≤ 10000,N 是正偶数。
· 输出行数是 (N−2)/2 行(N = 10 时是 4 行:4、6、8、10);
· 要处理的偶数最多 N/2 ≈ 5000 个,每个都要找一对质数 —— 如果每次都从头试除判断质数,会非常慢,
所以要先「打表」;
· 第一个加数要最小:只要从小到大试 a,第一个能拆成功的 a 就是最小的。
① 先用埃氏筛把 2~N 的质数全部标记出来。
埃氏筛的思路特别朴素:从 2 开始,把每个质数的倍数全部划掉。 2 是质数 → 划掉 4、6、8……;3 是质数 → 划掉 9、12、15……;下一个没被划掉的就是质数,继续划……
for(int i = 2; i * i <= N; i++)
if(isPrime[i]) // i 本身没被划掉 → i 是质数
for(int j = i * i; j <= N; j += i)
isPrime[j] = false; // 把 i 的倍数全部划掉
· 为什么 j 从 i * i 开始,而不是 2 * i?
因为 2i、3i、…、(i−1)i 这些数,在更小的质数(2、3……)那一轮就已经被划掉了,
重复划是白费力气。从 i² 开始才都是没被划过的。
(注意:i * i 可能超出 int 范围,本题 N ≤ 10000 无所谓,但习惯上可以写成 i <= N / i。)
· 为什么只筛到 √N?
因为大于 √N 的质数,它的平方早就超过 N 了,没有更小的倍数可划 —— 换句话说,
如果 N 以内的数还有合数没被划掉,它一定有个不超过 √N 的质因数,早就被处理过了。
② 对每个偶数 n,从小到大试 a:
for(int a = 2; a <= n / 2; a++){
if(isPrime[a] && isPrime[n - a]){
cout << n << "=" << a << "+" << n - a << endl;
break; // ★ 找到第一个就停,这才保证 a 最小
}
}
a 只需要试到 n / 2
因为如果 a > n/2,那么 n − a < n/2 ≤ a ——
也就是说这一对加数「反过来了」,早就被 a 较小的时候试过了。
试到 n/2 就覆盖了所有本质不同的拆分。而且这样还顺便保证 a ≤ n−a, 虽然本题没要求按大小排,但看着更整齐。
③ 外层循环从 4 开始,每次 +2,对应题目要求的那 (N−2)/2 个偶数。
10
4=2+2 6=3+3 8=3+5 10=3+7
4 = 2+2;6 = 3+3;8 = 3+5(8 还能拆成 5+3,但 3 更小);10 = 3+7(不是 5+5)—— 正是题目要求的「第一个加数最小」。
#include <iostream> using namespace std; // 埃氏筛:把 2~N 里所有质数标记出来,放在 main 外面(全局数组自动清零) bool isPrime[10005]; int main(){ int N; cin >> N; for(int i = 2; i <= N; i++) isPrime[i] = true; // 先假设都是质数 for(int i = 2; i * i <= N; i++){ if(isPrime[i]){ // i 是质数 for(int j = i * i; j <= N; j += i) isPrime[j] = false; // i 的倍数全部划掉 } } for(int n = 4; n <= N; n += 2){ // 从 4 开始,每个偶数都要验证 for(int a = 2; a <= n / 2; a++){ // a 从小到大试,第一个成功的 a 最小 if(isPrime[a] && isPrime[n - a]){ cout << n << "=" << a << "+" << n - a << endl; break; // 找到一个就够,换下一个偶数 } } } return 0; }
官方样例 10 → 四行 4=2+2 / 6=3+3 / 8=3+5 / 10=3+7 逐行完全一致
边界测试 4 → 4=2+2(只有一行,正好是 (4−2)/2 = 1 行)✅
最大数据 N = 10000 的完整验证(真实运行 + 逐行程序校验):
4=2+2,末行 10000=59+9941(59 和 9941 都是质数)✅break,会输出多行、或者把最后的 a 打印出来(那就成了「第一个加数最大」)。必须 break。bool isPrime[10005] 刚好够;开 isPrime[10000] 会越界。n <= N; n++:会输出奇数行(3=…、5=… 这些偶数才需要验证)。要写 n += 2。a = 1 开始:1 不是质数,虽然 isPrime[1] 是 false 不会出错,但从 2 开始更清楚。for(i = 2; i <= N; i++) isPrime[i] = true; 把 2~N 假设成质数,再筛。| 错误写法 | 后果 | 正确写法 |
|---|---|---|
| 把函数写在 main 后面 | 编译报 'xxx' was not declared in this scope | 函数写在 main 前面,或先写一行函数声明 |
| isPrime 里漏了 if(n < 2) return false; | 1 和 0 被当成质数输出,P5736 直接错 | 函数第一行就判 n < 2 |
| for(i = 2; i <= n; i++) 判质数 | 多试了 √n 倍的工作量,大数会超时 | for(i = 2; (long long)i*i <= n; i++) |
| 闰年只写 y%4==0 && y%100!=0 | 2000 年被漏掉,样例就挂 | 先判 y % 400 == 0 返回 true |
| 在函数里用 sizeof(a) 算数组长度 | 得到的是指针大小(8 或 4),不是数组长度 | 长度当参数传进去:f(a, n) |
| int a[1000000]; 写在 main 里面 | 实测直接爆栈,退出码 0xC00000FD,程序根本跑不到输出 | 大数组一律写在 main 外面(全局) |
| P1304 找到一对质数后不 break | 输出重复行,或第一个加数不是最小的 | 找到就 break,保证 a 最小 |
| P1304 里对每个偶数重新试除判质数 | 5000 个偶数 × 每个都试到 √n,慢几十倍 | 先用埃氏筛打表,再枚举 a |
| 输出最后多一个空格 / 少一个换行 | 格式错,部分评测点 WA | 用 bool first 控制分隔符,结尾照常 endl |
| P5735 里用 (x2-x1)^2 求平方 | ^ 是异或运算符,结果完全不对 | 写成 (x2-x1)*(x2-x1) |
isPrime 只需要试到 √n?答:因为因数成对出现。如果 n = a × b 且 a ≤ b,那么 a ≤ √n。
换句话说,n 只要是个合数,它一定有一个不超过 √n 的因数。试到 √n 都没找到因数,另一半就更不可能有 —— 那就是质数。
效果:判断 10⁵ 只需试 316 次;判断 10⁹ 也只需试 31623 次。
bool isLeap(int y){ return y % 4 == 0 && y % 100 != 0; }答:漏了「能被 400 整除」这一条。它会把 2000 年判成平年。
正确写法:return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
注意括号不能省:&& 的优先级比 || 高,不写括号虽然结果也对,但读起来容易误解,加上更清楚。
int a[500000]; 写在 main 里面,会发生什么?答:500000 × 4 字节 = 1.9 MB,搭在栈上。我实测这个大小还能跑(刚好卡在 2MB 栈限制的边缘)。
但只要再大一点,比如 int a[1000000](3.8 MB),就立刻爆栈,退出码 0xC00000FD,程序连输出都没有。
结论:别去试探那个临界点。凡是 int a[100000](约 400KB)以上的数组,一律开成全局。
答:
· 要修改实参 → 用引用:void swap(int &a, int &b);
· 要传大对象(string、结构体、数组)→ 用引用(最好是 const &,表示「只读,别改我」),避免整份拷贝;
· 只是算个结果返回(int、double 这种小东西)→ 值传递最省心,函数绝对改不坏外面的变量。
★ 补充:数组比较特殊,传参时天生就是「传地址」,函数里改了外面就变,不需要也不该写 &。
bool isPrime(int n){
if(n < 2) return false;
for(int i = 2; (long long)i * i <= n; i++)
if(n % i == 0) return false;
return true;
}
int countPrime(int n){ // 数 1~n 里有多少个质数
int cnt = 0;
for(int i = 1; i <= n; i++) // 从 1 开始也没关系,isPrime(1) 是 false
if(isPrime(i)) cnt++;
return cnt;
}
★ countPrime(10) = 4(2、3、5、7)。用今天学的话说:这就是「函数调用函数」 ——
countPrime 里可以直接用写好的 isPrime,不用重复写判断逻辑。
(如果想再快,就换成埃氏筛,像 P1304 那样。)
【函数骨架】
返回类型 函数名(参数列表){
函数体
return 返回值; // void 可不写
}
// ★ 函数写在 main 前面,或先写一行声明
【参数传递】
void f(int a) 值传递 → 改的是复印件,外面不变
void f(int &a) 引用 → 改的就是外面那个变量
void f(int a[], int n) 数组 → 天生传地址,必须另传长度
void f(const string &s) 大对象只读引用,零拷贝
【数组放哪】
全局(main 外):数据段,可以开很大,自动清零 ← 大数组放这里
局部(函数内):栈,只有 1~2MB,不自动清零 ← 实测 3.8MB 就爆栈
【素数判断(背)】
bool isPrime(int n){
if(n < 2) return false; // 0 和 1 不是
for(int i = 2; (long long)i*i <= n; i++) // 试到 sqrt(n)
if(n % i == 0) return false;
return true;
}
【埃氏筛(背)】
bool isPrime[10005]; // 全局!
for(int i = 2; i <= N; i++) isPrime[i] = true;
for(int i = 2; i*i <= N; i++)
if(isPrime[i])
for(int j = i*i; j <= N; j += i)
isPrime[j] = false;
// 一次预处理,之后 O(1) 查
【闰年判断(背)】
bool isLeap(int y){
if(y % 400 == 0) return true; // 2000 是
if(y % 100 == 0) return false; // 1900 不是
return y % 4 == 0;
}
// 顺序不能反!先 400,再 100,最后 4
【两点距离】
double dist(double x1,double y1,double x2,double y2){
return sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
}
// 需要 <cmath>;小数一律 double,保留两位用 printf("%.2f")
【输出分隔符】
bool first = true;
for(...){
if(!first) cout << " ";
first = false;
cout << 内容;
}
结构体:把相关的数据打包
今天你已经体会到「一个函数管一件事」的清爽。明天会遇到一个新问题:
数据本身是一整组、分不开的 —— 比如一个学生的姓名 + 语文 + 数学 + 英语,
或者一个点的 x + y + z。用四个平行数组存(name[i]、chinese[i]……)会很别扭,
一排序就全乱套。
结构体 struct 就是解决这个的:把描述同一个东西的好几个变量捆成一包,
然后像用一个普通变量那样用这一整包。配合今天学的函数,你就能写出「传一个学生进去、返回他的总分」这种自然又好读的代码。
明天题目:洛谷 P5740 最厉害的学生 / P1093 奖学金 / P1104 生日 / P5143 攀爬者 —— 全是「把数据打包再排序」的活儿。