本讲定位:CSP-J 初赛系列的数论专题。为什么单开一讲?因为七年真题统计(见
cspj-exam-analysis-2019-2025.md)里它是全卷第一大题材:2020 完善程序考质因数分解、2021 阅读程序整段考约数、2022 考枚举因数和求平方根、2023 考因子平方和、2024 考质数统计和完全平方数、2025 考 gcd 与互质枚举——每年至少一段程序题,折算 12~15 分,另有选择题不时加码。初赛数论有两种考法,本讲每个知识点都按两种考法各练一遍:手算(选择题:97 是不是质数、gcd(319,377) 是多少)和读写代码(程序题:试除循环的边界、成对枚举防重复)。不涉及复赛的同余理论和逆元,全部内容都在入门级考纲内。
前置知识:整除与余数(
%运算符)、第 01 讲的 STL 基础。适合对象:CSP-J 初赛。标注 ⭐ 的小节为进阶内容,第一遍学习可以跳过。
质数:大于 1、且只有 1 和自身两个约数的整数。判定 $n$ 是否为质数,不需要试除到 $n-1$,试除到 $\sqrt{n}$ 就够了——如果 $n = a \times b$ 且 $a \le b$,那么必有 $a \le \sqrt{n}$;既然找不到 $\le \sqrt{n}$ 的因子,也就不存在任何因子。
bool isPrime(int n) {
if (n < 2) return false; // 0、1 都不是质数——最常被忘掉的边界
for (int i = 2; i * i <= n; i++) // i*i <= n 等价于 i <= sqrt(n),且避开浮点
if (n % i == 0) return false;
return true;
}
三个必须讲透的细节,全都是真题考过的:
i * i <= n 而不是 i <= sqrt(n):避免浮点误差,也是程序题里的标准写法(2024 阅读程序原型)。n < 2 的边界:判断题问”输入 1 时返回什么”,漏了这行就是送分给出题人。选择题手算技巧:判定一个两位数/三位数是不是质数,只需用 $\le \sqrt{n}$ 的质数去试除。比如 97:$\sqrt{97} < 10$,试除 2、3、5、7 都不行——97 是质数。这正是 2019 年选择题”100 以内最大的素数”(答案 97)的做法。
顺手记住两个常考数字:100 以内质数有 25 个(2021 阅读程序把它藏在”$f[1..100]$ 中约数个数等于 2 的数有几个”里考);1000 以内有 168 个(了解即可)。
2024 年阅读程序第 1 段就是”isPrime + 主循环统计 $2..n$ 的质数个数与总和”,考法是:给定输入验证输出、修改循环条件问结果变化、归纳函数功能。完整程序:
#include<iostream>
using namespace std;
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;
}
int main() {
int n, cnt = 0, sum = 0;
cin >> n;
for (int i = 2; i <= n; i++)
if (isPrime(i)) { cnt++; sum += i; }
cout << cnt << " " << sum << endl;
return 0;
}
输入 100,输出 25 1060。读这类程序时在草稿纸上记下”cnt 数个数、sum 累加值”即可,不要逐个质数硬算——判断题考的是结构(边界、条件、复杂度),不是算术。
要判定的数很多时,逐个试除太慢。埃拉托斯特尼筛反过来做:从 2 开始,每找到一个质数,就把它的所有倍数划掉:
const int N = 1000005;
bool composite[N]; // composite[i] 为 true 表示 i 被划掉(是合数)
void sieve(int n) {
for (int i = 2; i <= n; i++)
if (!composite[i]) // i 没被划掉 → i 是质数
for (int j = 2 * i; j <= n; j += i) // 划掉 i 的所有倍数
composite[j] = true;
}