
在算法竞赛的数论领域,算术基本定理是当之无愧的 “基石定理”。它看似简单 —— 每个大于 1 的自然数都能唯一分解为质数乘积,但由此延伸出的质因数分解、阶乘分解等问题,却贯穿了从入门到进阶的各类竞赛题目。本文将从定理本质出发,层层拆解质因数分解的核心思路,详解试除法的优化技巧,手把手教你掌握阶乘分解的高效解法,让你在数论题目中轻松举一反三。下面就让我们正式开始吧!
算术基本定理(又称唯一分解定理)指出:任何一个大于 1 的自然数 n,都可以唯一分解成有限个质数的乘积,形式如下:n=p1α1×p2α2×p3α3×⋯×pkαk其中 p1<p2<p3<⋯<pk 均为质数,α1,α2,…,αk 是正整数(称为质因子的指数),这样的分解称为 n 的标准分解式。
这个定理的关键在于 “唯一” 二字 —— 无论你从哪个角度分解,最终得到的质因子种类和对应的指数都是固定的。比如 12 的分解式只能是 22×31,不可能出现 21×32 或其他形式。
算术基本定理之所以重要,是因为它将任意一个复杂的自然数,拆解成了最基本的 “质数积木”。就像乐高积木可以拼出各种造型,质数通过不同的组合(乘积)和重复次数(指数),构成了所有大于 1 的自然数。
在算法竞赛中,这个定理是解决以下问题的核心工具:
我们以 360 为例,看看它的标准分解过程:
最终得到 360 的标准分解式:23×32×51,这正是算术基本定理的直观体现。
质因数分解是算术基本定理的直接应用,核心任务是找到一个数的所有质因子及其指数。最常用的方法是试除法,看似暴力,却能通过优化达到极高的效率。
试除法的本质的是 “枚举可能的质因子,逐一验证并分解”。根据算术基本定理,我们可以得到两个关键优化点:
举个例子,分解 n=100:
在代码实现中,枚举条件如果写成i <= sqrt(n),可能会因为 sqrt 函数的精度问题出错;如果写成i * i <= n,当 i 接近 1e9 时,i*i 会超出 int 范围导致溢出。因此,推荐使用 i <= n / i 的写法,既避免了精度问题,又防止了溢出。
#include <iostream>
#include <unordered_map>
using namespace std;
// 分解质因数,返回质因子及其指数(键为质因子,值为指数)
unordered_map<int, int> prime_factor(int x) {
unordered_map<int, int> factors;
// 枚举到sqrt(x)即可
for (int i = 2; i <= x / i; ++i) {
if (x % i == 0) { // i是x的质因子
int cnt = 0;
while (x % i == 0) { // 除尽所有i
cnt++;
x /= i;
}
factors[i] = cnt;
}
}
// 剩余的x如果大于1,说明是最后一个质因子(大于sqrt(原x))
if (x > 1) {
factors[x] = 1;
}
return factors;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
auto factors = prime_factor(n);
// 按质因子从小到大输出
cout << n << " = ";
bool first = true;
for (auto [p, cnt] : factors) {
if (!first) cout << " × ";
cout << p << "^" << cnt;
first = false;
}
cout << endl;
return 0;
}输入:360
输出:360 = 2^3 × 3^2 × 5^1
时间复杂度:O (√n)。最坏情况下需要枚举到√n(如 n 是质数时),但实际情况中,由于每次分解都会将 n 大幅减小,实际运行时间远低于理论上限。对于 n≤1e12 的数,√n=1e6,枚举 1e6 次完全可以在毫秒级完成。
空间复杂度:O (k),k 为 n 的质因子种类数(如 360 有 3 种质因子,k=3),空间开销可以忽略。
当需要对多个数进行质因数分解时,逐一使用试除法的效率会偏低。此时可以先通过线性筛(欧拉筛) 预处理出范围内的所有质数,再用这些质数进行分解,进一步降低时间复杂度。
线性筛可以在 O (n) 时间内筛选出 [2, n] 范围内的所有质数,并记录每个数的最小质因子(MPF,Minimum Prime Factor)。利用最小质因子,我们可以快速分解任意数 x:
这种方法的优势在于,无需枚举所有可能的因子,而是直接使用预处理好的最小质因子,分解效率可以提升至 O (log x)。
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
const int MAXN = 1e6 + 10;
int min_prime[MAXN]; // 存储每个数的最小质因子
vector<int> primes; // 存储所有质数
// 线性筛预处理最小质因子
void sieve(int n) {
memset(min_prime, 0, sizeof min_prime);
primes.clear();
for (int i = 2; i <= n; ++i) {
if (min_prime[i] == 0) { // i是质数
min_prime[i] = i;
primes.push_back(i);
}
// 枚举所有质数,更新i*primes[j]的最小质因子
for (int j = 0; j < primes.size() && primes[j] <= min_prime[i] && i * primes[j] <= n; ++j) {
min_prime[i * primes[j]] = primes[j];
}
}
}
// 利用最小质因子分解x
unordered_map<int, int> fast_prime_factor(int x) {
unordered_map<int, int> factors;
while (x > 1) {
int p = min_prime[x]; // x的最小质因子
int cnt = 0;
while (x % p == 0) {
cnt++;
x /= p;
}
factors[p] = cnt;
}
return factors;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int max_range = 1e6;
sieve(max_range); // 预处理1e6以内的最小质因子
int n;
while (cin >> n) {
auto factors = fast_prime_factor(n);
cout << n << " = ";
bool first = true;
for (auto [p, cnt] : factors) {
if (!first) cout << " × ";
cout << p << "^" << cnt;
first = false;
}
cout << endl;
}
return 0;
}这种方法适用于多组数据分解的场景,例如:
预处理线性筛的时间是 O (max_range),之后每组分解的时间是 O (log x),整体效率远高于多次单独使用试除法。
题目链接:https://www.luogu.com.cn/problem/P2043

题目描述:对 N! 进行质因子分解,输出每个质因子及其出现的次数(按质因子从小到大输出)。
输入:一个正整数 N(N≤1e4)。
输出:若干行,每行两个正整数 p 和 a,表示 N! 包含 a 个质因子 p。
示例:输入:10输出:2 83 45 27 1
核心难点:N! = 1×2×3×…×N,直接计算 N! 再分解会导致数值溢出(1e4! 是一个极其庞大的数,远超出 long long 范围),因此必须找到更巧妙的方法。
根据算术基本定理,N! 的质因子必然是 [2, N] 范围内的质数。对于每个质数 p,其在 N! 中的出现次数(指数)为:

直到

为止。
#include <iostream>
#include <cstring>
using namespace std;
const int MAXN = 1e4 + 10;
int cnt[MAXN]; // cnt[p]表示质因子p在N!中的指数
// 试除法分解单个x的质因数,并累加指数到cnt数组
void decompose(int x) {
for (int i = 2; i <= x / i; ++i) {
while (x % i == 0) {
cnt[i]++;
x /= i;
}
}
if (x > 1) {
cnt[x]++;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
memset(cnt, 0, sizeof cnt);
// 对1~N的每个数分解质因数,累加指数
for (int i = 2; i <= N; ++i) {
decompose(i);
}
// 输出所有有贡献的质因子(cnt[p]>0)
for (int p = 2; p <= N; ++p) {
if (cnt[p] > 0) {
cout << p << " " << cnt[p] << endl;
}
}
return 0;
}对于 N≤1e6 的场景,上述解法的效率会偏低(时间复杂度 O (N√N))。此时可以结合线性筛先筛选出 [2, N] 的所有质数,再用公式计算每个质数的指数,时间复杂度优化至 O (N log N)。
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int MAXN = 1e6 + 10;
bool st[MAXN]; // 标记是否为合数
int primes[MAXN], cnt_primes; // 存储质数,cnt_primes为质数个数
// 线性筛筛选[2, n]的所有质数
void sieve(int n) {
memset(st, false, sizeof st);
cnt_primes = 0;
for (int i = 2; i <= n; ++i) {
if (!st[i]) {
primes[++cnt_primes] = i;
}
for (int j = 1; 1LL * i * primes[j] <= n; ++j) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) {
break;
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
sieve(N); // 筛选[2, N]的所有质数
// 对每个质数计算其在N!中的指数
for (int i = 1; i <= cnt_primes; ++i) {
int p = primes[i];
int a = 0;
long long current = p;
while (current <= N) {
a += N / current;
current *= p; // 计算p^2, p^3, ...
}
cout << p << " " << a << endl;
}
return 0;
}解法 | 时间复杂度 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
试除法累加 | O(N√N) | N≤1e4 | 代码简单,无需预处理 | 效率较低,不适合大数据 |
线性筛 + 公式 | O(N log N) | N≤1e6 | 效率极高,适合大数据 | 需要预处理,代码稍复杂 |
对于题目中 N≤1e4 的限制,两种解法都能通过,但线性筛 + 公式的优势在 N 较大时会更加明显。
题目链接:https://www.luogu.com.cn/problem/P10495

题目描述:给定整数 N(3≤N≤1e6),将 N! 分解质因数,按质因数从小到大输出 pi 和 ci(ci 为 pi 的指数)。
输入:一个整数 N。
输出:若干行,每行一对 pi 和 ci。
示例:输入:5输出:2 33 15 1
核心考点:高效处理大规模阶乘分解(N=1e6),考验线性筛和公式的结合应用。
#include <iostream>
#include <cstring>
using namespace std;
typedef long long LL;
const int MAXN = 1e6 + 10;
bool st[MAXN];
int primes[MAXN], cnt;
// 线性筛优化版(筛选质数并存储)
void get_primes(int n) {
memset(st, 0, sizeof st);
cnt = 0;
for (int i = 2; i <= n; ++i) {
if (!st[i]) {
primes[++cnt] = i;
}
// 优化:用long long避免i*primes[j]溢出
for (int j = 1; 1LL * i * primes[j] <= n; ++j) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
get_primes(N);
for (int i = 1; i <= cnt; ++i) {
int p = primes[i];
int ci = 0;
LL current = p;
while (current <= N) {
ci += N / current;
// 防止current溢出(当p=2,N=1e6时,current最大为2^19=524288,不会溢出)
if (current > N / p) break; // 提前判断,避免current*p溢出
current *= p;
}
cout << p << " " << ci << endl;
}
return 0;
}current > N / p,如果成立,则 currentp 会超出 N,直接 break,避免溢出;ios::sync_with_stdio(false); cin.tie(nullptr);关闭同步,提升大数据量下的输入输出速度;由算术基本定理,若 n 的标准分解式为

,则 n 的约数个数为:

原理:每个质因子 p_i 可以选择 0 到 α_i 个,共 (α_i + 1) 种选择,所有质因子的选择组合即为约数总数。
示例:n=12=2²×3¹,约数个数为 (2+1)×(1+1)=6,分别是 1、2、3、4、6、12。
n 的所有约数之和为:

原理:将每个质因子的所有可能次幂相加,再将结果相乘,得到所有约数的和。
示例:n=12=2²×3¹,约数和为 (1+2+4)×(1+3)=7×4=28,验证:1+2+3+4+6+12=28。
对于两个数 a 和 b,设其标准分解式为:

(缺失的质因子指数视为 0)则:


i <= x / i替代i*i <= x;current > N / p。在实际竞赛中,这些知识点往往会结合其他数论内容(如欧拉函数、同余方程)考查,因此建议多做综合性题目,加深对定理的理解和应用能力。 如果你在学习过程中遇到具体题目无法解决,或者想进一步了解算术基本定理的其他应用(如大质数分解、密码学中的 RSA 算法),可以随时留言交流。后续将持续更新数论进阶内容,敬请关注!