ACM 一个月学习计划 · DAY 10

Day 10 · 函数:把代码写成模块

把「判断闰年」「算两点距离」「判断素数」这些逻辑各装一个盒子,主程序就只剩下一句句人话

今日目标:会自己写函数,理解值传递 / 引用传递、全局数组 / 局部数组的区别,背下素数判断和闰年判断两个模板
预计用时:约 3 小时(含做题)  难度:入门 ~ 普及-
题目来源:洛谷 P5735 / P5736 / P5737 / P1304
← Day 09 · 字符串进阶:数字、进制与格式处理

⏱今日时间安排

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,一眼就能看懂「先分类、再各自处理」的骨架, 细节全都藏进了一个个有名字的小盒子里。今天就正式把这件事学明白。

1为什么需要函数:先看一段「没有函数」的代码

题目要求:判断 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 里,你根本不知道该在哪打这条语句。

2函数三要素:返回类型、函数名、参数列表

函数长什么样

返回类型  函数名 (参数列表) {
    函数体
    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 交作业

返回类型怎么选

函数要给出什么返回类型例子
是 / 否 的结论boolbool isPrime(int n)
一个整数结果int / long longint countLeap(int x, int y)
一个小数结果doubledouble dist(...)
什么都不用返回(只打印 / 只修改参数)voidvoid printAns(int x)
💡 起名建议:动宾结构 + 小驼峰

isPrime(是素数吗)、isLeap(是闰年吗)、countLeap(数闰年)、 dist(求距离)、nameToNumber(名字转数字)。

返回 bool 的函数用 is / has 开头,这几乎是全世界的默契。

return 的两个作用

① 交作业:把你的结果送回调用处。
② 立刻结束函数: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;                      // 一个因数都没找到 → 是素数
}

函数要写在 main 前面

🚫 最常见的编译错误:函数写在 main 后面

C++ 是从上往下读的。你把函数写在 main 后面,main 里调用的时候编译器还不认识这个名字, 会报 'isPrime' was not declared in this scope。

两种解法:
① 直接把函数整个写在 main 前面(推荐,最省事);
② 或者先写一行「函数声明」(也叫函数原型),把函数体留在后面:

bool isPrime(int n);      // 声明:告诉编译器「有这个函数」

int main(){
    // 这里就可以调用 isPrime 了
}

bool isPrime(int n){      // 定义:真正的实现
    // ……
}

3值传递 vs 引用传递 · 局部数组 vs 全局数组

值传递:函数拿到的是「复印件」

默认情况下,参数是值传递 —— 调用时会把实参的值复制一份给形参。函数里改的是这份复印件,外面的变量毫发无伤。

写个交换两个数的函数试试(真实运行结果):

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),别用它算长度。

局部数组 vs 全局数组:一个会爆栈的真实事故

「全局」就是写在所有函数外面(包括 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}), 而且写函数的时候可以直接用,不用一个个当参数传来传去。

💡 今天的 P1304 就会用到这一条

它的素数筛要开一个 bool isPrime[10005] 的数组,而且要在循环里反复访问。 把这个数组开成全局,函数和 main 都能直接用,代码立刻清爽一大截。

4两个必背模板:判断素数、判断闰年

今天的 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❌❌❌不是

5四道例题详解

下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译、实际运行过,输出是你看到的真实结果。

P5735 【深基7.例1】距离函数 入门 · 函数封装

题意:给出平面上不共线的三个点的坐标(实数),求这三点围成的三角形的周长,保留两位小数。

⚠️ 数据范围 / 关键约束

坐标都是实数,绝对值不超过 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; 一次读完就行,完全不用管它在哪一行换行。

样例 1 输入
0 0
0 3
4 0
样例 1 输出
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>)。
P5736 【深基7.例2】质数筛 入门 · 素数判断

题意:读入 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(要判断几千个数),那里我们就会换上筛法。什么规模用什么工具,别为了炫技把简单题写复杂。

样例 1 输入
5
3 4 5 6 7
样例 1 输出
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 << " ";。
  • 数组开太小:n ≤ 100,开 a[105] 够了,但别开 a[100](下标只到 99)。
P5737 【深基7.例3】闰年展示 入门 · 闰年判断函数

题意:输入 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 再走一遍(或者把两步合并),是闰年就输出 + 空格处理。

两次循环其实可以合成一次,但这里分两步写更清楚:题目要求先给出总数,再列出明细, 分两步正好对应题目的输出顺序,不容易乱。

样例 1 输入
1989 2001
样例 1 输出
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)✅

易错点

  • 漏掉 400 的规则:写成 y % 4 == 0 && y % 100 != 0 就完了,2000 会被漏掉,样例直接挂。
  • 判断顺序反了:先判 100 再判 400,2000 会被 100 那条规则提前否掉。必须先 400。
  • 循环边界写成 i < y:区间是闭区间 [x, y],要用 i <= y,否则会漏掉 y 这一年。
  • 行尾多空格:同样用 first 标记处理。
  • 忘了先输出个数:题目要两行输出,顺序不能反。
P1304 哥德巴赫猜想 普及- · 筛法 + 枚举

题意:输入一个偶数 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 个偶数。

样例 1 输入
10
样例 1 输出
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 的完整验证(真实运行 + 逐行程序校验):

  • 输出行数 4999 行,与公式 (10000−2)/2 = 4999 完全吻合 ✅
  • 我把每一行都解析出来,逐个核对:两个加数都是质数、两数之和等于该偶数、 且第一个加数确实是所有可行拆法中最小的 —— 4999 行全部通过,失败 0 行 ✅
  • 首行 4=2+2,末行 10000=59+9941(59 和 9941 都是质数)✅
  • 总耗时 67 ms(含从 5000 个偶数中找最小质数对),非常宽裕

易错点

  • 找到一对就继续试别的 a:忘了 break,会输出多行、或者把最后的 a 打印出来(那就成了「第一个加数最大」)。必须 break。
  • 素组数组开小了:N 最大 10000,开 bool isPrime[10005] 刚好够;开 isPrime[10000] 会越界。
  • 外层循环写成 n <= N; n++:会输出奇数行(3=…、5=… 这些偶数才需要验证)。要写 n += 2。
  • 内层从 a = 1 开始:1 不是质数,虽然 isPrime[1] 是 false 不会出错,但从 2 开始更清楚。
  • 全局数组忘了初始化为 true:这是我代码里唯一「要手动做」的初始化 —— 全局数组默认全是 false(=「还不是质数」), 所以要先 for(i = 2; i <= N; i++) isPrime[i] = true; 把 2~N 假设成质数,再筛。
  • 把筛法写成对每个偶数都重新判断质数:那样虽然 N ≤ 10000 也能过,但复杂度差一个量级。 「一次预处理、多次查询」是今天最值得带走的思维方式。

✗今日常见错误汇总

错误写法后果正确写法
把函数写在 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)

✓自测:你真的会了吗

第 1 题:为什么 isPrime 只需要试到 √n?

答:因为因数成对出现。如果 n = a × b 且 a ≤ b,那么 a ≤ √n。

换句话说,n 只要是个合数,它一定有一个不超过 √n 的因数。试到 √n 都没找到因数,另一半就更不可能有 —— 那就是质数。

效果:判断 10⁵ 只需试 316 次;判断 10⁹ 也只需试 31623 次。

第 2 题:下面这个函数有什么问题?
bool isLeap(int y){ return y % 4 == 0 && y % 100 != 0; }

答:漏了「能被 400 整除」这一条。它会把 2000 年判成平年。

正确写法:return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);

注意括号不能省:&& 的优先级比 || 高,不写括号虽然结果也对,但读起来容易误解,加上更清楚。

第 3 题:int a[500000]; 写在 main 里面,会发生什么?

答:500000 × 4 字节 = 1.9 MB,搭在栈上。我实测这个大小还能跑(刚好卡在 2MB 栈限制的边缘)。

但只要再大一点,比如 int a[1000000](3.8 MB),就立刻爆栈,退出码 0xC00000FD,程序连输出都没有。

结论:别去试探那个临界点。凡是 int a[100000](约 400KB)以上的数组,一律开成全局。

第 4 题:引用传递和值传递,分别什么时候用?

答:

· 要修改实参 → 用引用:void swap(int &a, int &b);
· 要传大对象(string、结构体、数组)→ 用引用(最好是 const &,表示「只读,别改我」),避免整份拷贝;
· 只是算个结果返回(int、double 这种小东西)→ 值传递最省心,函数绝对改不坏外面的变量。

★ 补充:数组比较特殊,传参时天生就是「传地址」,函数里改了外面就变,不需要也不该写 &。

第 5 题(手写代码):写一个函数,求 n 以内有多少个质数。
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 << 内容;
  }

→明天预告 · Day 11

结构体:把相关的数据打包

今天你已经体会到「一个函数管一件事」的清爽。明天会遇到一个新问题: 数据本身是一整组、分不开的 —— 比如一个学生的姓名 + 语文 + 数学 + 英语, 或者一个点的 x + y + z。用四个平行数组存(name[i]、chinese[i]……)会很别扭, 一排序就全乱套。

结构体 struct 就是解决这个的:把描述同一个东西的好几个变量捆成一包, 然后像用一个普通变量那样用这一整包。配合今天学的函数,你就能写出「传一个学生进去、返回他的总分」这种自然又好读的代码。

明天题目:洛谷 P5740 最厉害的学生 / P1093 奖学金 / P1104 生日 / P5143 攀爬者 —— 全是「把数据打包再排序」的活儿。