16 数论基础
GESP 5 级NOIP 进阶CSP-S
级别标注:GESP 5 级(考级要求)/ NOIP、CSP-J 进阶(竞赛进阶:质数筛、快速幂、取模、扩展欧几里得、组合数)。
一、质数与合数,sqrt 试除法
质数:大于 1 且只被 1 和自身整除。判断:
C++ 实例
#include <iostream>
#include <cmath>
using namespace std;
// 判断质数:只需试除到 sqrt(n)
bool isPrime(int n) {
if (n < 2) return false; // 0 和 1 不是质数
for (int i = 2; (long long)i * i <= n; i++) // i*i 防溢出用 long long
if (n % i == 0) return false; // 能被整除 -> 合数
return true; // 没有因子 -> 质数
}注意用
i*i <= n需防溢出,转(long long)i*i。
质因数分解(试除法)
C++ 实例
#include <iostream>
using namespace std;
int main() {
int n; cin >> n; // n 分解质因数
for (int i = 2; (long long)i * i <= n; i++) { // 从小到大试除
while (n % i == 0) { // 能整除就反复除干净
cout << i << " ";
n /= i;
}
}
if (n > 1) cout << n; // 剩余 n 是大于 sqrt 的质因子
cout << endl;
return 0;
}形象理解判断质数像查户口本找“共犯”:如果 n 有因子,必有一个因子 ≤ √n,所以只试到 √n 就够。比如 101 只要试 2~10,不用试到 100。
易错点循环条件
i * i <= n 在 n 接近 10^9 时 i*i 会溢出 int。两种安全写法:(long long)i * i <= n 或 i <= n / i。GESP 5 级笔试常考这个坑。二、质数筛:埃氏筛与欧拉筛
埃氏筛(O(n log log n))
C++ 实例
#include <iostream>
using namespace std;
const int N = 1000005;
bool notPrime[N]; // false 表示是质数(埃氏筛标记数组)
void sieve(int n) {
notPrime[0] = notPrime[1] = true; // 0、1 不是质数
for (int i = 2; (long long)i * i <= n; i++) { // 只需筛到 sqrt(n)
if (!notPrime[i]) { // i 是质数
for (int j = i * i; j <= n; j += i) // 从 i*i 开始(更小的已筛过)
notPrime[j] = true; // 把 i 的倍数标记为合数
}
}
}
int main() {
sieve(100);
for (int i = 1; i <= 30; i++)
if (!notPrime[i]) cout << i << " "; // 2 3 5 7 11 13 17 19 23 29
cout << endl;
return 0;
}线性筛(欧拉筛,O(n),每个合数只被最小质因子筛一次,竞赛提速)
C++ 实例
#include <iostream>
#include <vector>
using namespace std;
const int N = 10000005;
int primes[N / 10], pc; // 质数表(线性筛)
bool notPrime[N];
// 线性筛:每个合数只被它的最小质因数筛掉一次,O(n)
void linearSieve(int n) {
for (int i = 2; i <= n; i++) {
if (!notPrime[i]) primes[pc++] = i; // i 是质数,收入质数表
for (int j = 0; j < pc; j++) { // 用已有质数筛 i 的倍数
long long v = (long long)i * primes[j];
if (v > n) break; // 超出范围
notPrime[v] = true;
if (i % primes[j] == 0) break; // 保证每个合数只被最小质因数筛
}
}
}形象理解埃氏筛像逐张划掉乘法表:先划掉 2 的所有倍数,再划掉 3 的倍数、5 的倍数…剩下没被划掉的就是质数。欧拉筛更精细:每个合数只被它的最小质因子划一次,所以是 O(n)。
考级视角埃氏筛标记起点是
j = i * i 而不是 2 * i:比 i 小的质数已经把更小的倍数划掉了。欧拉筛的 if (i % primes[j] == 0) break; 是保证“最小质因子只筛一次”的关键,笔试读程序题常考。三、约数(因子)与最大公约数
求约数集合
C++ 实例
// 枚举 1..sqrt(n) 找成对约数(约数是成对出现的)
vector<int> getDivisors(int n) {
vector<int> res;
for (int i = 1; (long long)i * i <= n; i++) {
if (n % i == 0) {
res.push_back(i); // 小约数 i
if (i != n / i) res.push_back(n / i); // 配对的大约数 n/i(避免平方重复)
}
}
return res;
}最大公约数 GCD(辗转相除法)
C++ 实例
// 最大公约数:辗转相除法,b 为 0 时 a 即答案
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
// 最小公倍数:a / gcd(a,b) * b(先除再乘防溢出)
long long lcm(int a, int b) { return (long long)a / gcd(a, b) * b; }扩展欧几里得(NOIP 进阶:解 ax + by = gcd(a,b))
C++ 实例
// 扩展欧几里得:返回 gcd(a,b),同时求出 x,y 使 ax + by = gcd(a,b)
long long exgcd(long long a, long long b, long long &x, long long &y) {
if (b == 0) { x = 1; y = 0; return a; } // 递归出口:a*1 + 0*0 = a
long long g = exgcd(b, a % b, y, x); // 递归时交换 x、y
y -= a / b * x; // 回代修正 y
return g;
}形象理解GCD 辗转相除像用长条量地:用 b 量 a,剩 r;再用 r 量 b,再剩 r'…直到正好量完,最后的量尺就是最大公约数。
gcd(a,b) = gcd(b, a%b)。易错点算 lcm 一定要先除后乘:
a / gcd(a,b) * b,写成 a * b / gcd(a,b) 会先溢出 int。另外 exgcd 的递归参数 exgcd(b, a%b, y, x) 里 x、y 是交换传入的,容易写错。四、快速幂与取模
取模运算:(a + b) % m = (a%m + b%m) % m;乘:(a * b) % m = (a%m * b%m) % m。
注意:C++ 中负数取模结果是负数,需 ((x % m) + m) % m 修正。
C++ 实例
#include <iostream>
using namespace std;
// 快速幂:a^n % mod,O(log n)
long long fastPow(long long a, long long n, long long mod) {
long long res = 1;
a %= mod; // 先取模防溢出
while (n > 0) { // 按二进制位处理指数
if (n & 1) res = res * a % mod; // 当前位为 1 才乘
a = a * a % mod; // a 平方(对应下一位的底)
n >>= 1; // 指数右移
}
return res;
}
int main() {
cout << fastPow(2, 10, 1000000007) << endl; // 1024
cout << fastPow(3, 100, 1000000007) << endl;
return 0;
}逆元(进阶:求分数在模意义下的值)
当 mod 为质数时,a 的乘法逆元为 fastPow(a, mod-2, mod)(费马小定理)。
C++ 实例
// 费马小定理求逆元:mod 为质数时,a 的逆元 = a^(mod-2) % mod
long long inv(long long a, long long mod) {
return fastPow(a, mod - 2, mod);
}
// 组合数 C(n,k) = fact[n] * inv(fact[k]) % mod * inv(fact[n-k]) % mod形象理解快速幂像翻倍算账法:算 3^100 不是乘 100 次,而是把指数写成二进制(100=64+32+4),底数不断自乘翻倍,只做 log 次乘法。每步取模保证数字不爆炸。
易错点三个坑:①
a %= mod 开头要做,否则 a 可能超大;② res * a 可能溢出,全部用 long long 并每次 % mod;③ C++ 负数取模结果是负数,要写成 ((x % m) + m) % m。五、组合计数基础
- 阶乘与组合数(配合逆元/杨辉三角递推
C[i][j] = C[i-1][j] + C[i-1][j-1])。 - 排列数
P(n,k) = n!/(n-k)!。
六、易错点 / 常见坑点
- ⚠️ 负数取模在 C++ 是负的,算术题务必修正为结果非负。
- ⚠️ 判断质数时
i*i <= n溢出(用(long long)或i <= n / i)。 - ⚠️ 快速幂里
res * a可能溢出,用long long并% mod。 - ⚠️ 埃氏筛从
i * i开始标记(小因子已在前面处理),别从2*i重复标记太多。 - ⚠️
gcd递归层级最深约 log 级别,别怕;但取模运算中除数为 0 时报错(b==0 时终止)。 - ⚠️ 求组合数模大质数时直接阶乘会爆,需用逆元。
- ⚠️
lcm(a,b)先除后乘防溢出。
七、考级要求 vs 竞赛进阶
- 考级要求(GESP 5 级):质数判断、约数枚举、最大公约数/最小公倍数、简单取模运算、二进制/进制转换;部分考查埃氏筛概念。
- 竞赛进阶(NOIP/CSP-J 提高):线性筛、快速幂 + 取模、费马小定理逆元、扩展欧几里得、组合数取模、唯一分解定理在约数/和计算中的应用。
八、课内练习建议
- 手写
isPrime并测试 10^6 附近的大数。 - 埃氏筛打印 1~1000 内所有质数。
- 用快速幂求
7^123456 % 1000000007。 - 求若干组 a、b 的 gcd / lcm(注意 long long)。
- 扩展:用 exgcd 求解
47x + 30y = 1的一组整数解。