一、单点拆解的物理边界:试除法与质因数分解
如果给你一个数
这在数学上对应着极其崇高的基石——算术基本定理(唯一分解定理):
任何一个大于
的正整数 ,都可以被唯一分解为有限个质数的乘积:
在竞赛中,面对单点大整数(例如
1. 为什么枚举界限只需要到 ?
很多初学者容易死记“循环写到根号
一个整数
因此,我们从最小的质数 while 循环把它彻底除干净(榨干该质因子的贡献,并统计其指数
核心细节拆解:
- 循环上界动态收缩:条件写成
p <= x / p。这一方面避免了p * p <= x在接近 long long上限时的乘法溢出,另一方面,随着被不断除小,上界也在实时变小,大幅削减无效枚举。 - 为什么不必判断
是否为质数? 比如枚举到 时,难道不会把 当成质因子放进去吗?绝对不会。因为 是 的倍数,在 时,我们已经用 while(x % 2 == 0) x /= 2;把内部所有的因子 彻底抽干了!轮到 时, 根本不可能再被 整除。任何合数在轮到它之前,它的所有质因子都已被清空。 - 孤胆英雄:剩下的
是什么? 当 p超过当前x / p跳出循环后,如果剩下的,它一定是一个质因子!因为若它还是合数,至少还有一个不超过它自身平方根的因子没被除掉,循环就不该结束。如果漏掉这一行判断,分解 就会丢失最后的质因子 。
vector<pair<long long, int>> factor(long long x) {
vector<pair<long long, int>> res;
for (long long p = 2; p <= x / p; p++) {
if (x % p != 0) continue;
int c = 0;
while (x % p == 0) {
x /= p;
c++;
}
res.push_back({p, c});
}
if (x > 1) res.push_back({x, 1}); // 收下最后剩余的质因子
return res;
}
该算法的单次时间复杂度为最坏
二、约数个数的本质:指数维度的“独立选餐”
拿到
它的任意一个正约数
此时,决定一个约数的本质,变成了两个独立的单选题:
- 质因子
的指数 可以选: (共 种选法); - 质因子
的指数 可以选: (共 种选法)。
根据乘法原理,两个维度的选择互不干扰,组合出来的约数个数恰好是:
我们把这 12 个约数在网格中整整齐齐地铺开:

推广到一般情况,若
💡 考场绝杀性质:约数个数的奇偶性 任何一个整数的约数通常都是成对出现的:
与 。 唯独什么时候 会重合?当且仅当 是一个完全平方数(此时所有的质因子指数 均为偶数,导致所有的 均为奇数,乘积也必然是奇数)。 结论: 为奇数 是完全平方数。这在许多博弈论或开关灯问题中是瞬间秒题的题眼。
三、从逐个试除到批量剔除:线性筛(欧拉筛)
如果现在的任务变了:不是查单个数字,而是要找出
如果依然对每个数跑一次试除法,总时间是
1. 埃氏筛的缺陷与线性筛的诞生
埃氏筛 (Sieve of Eratosthenes):从小到大扫,只要扫到一个没被标记的数
线性筛(欧拉筛)的核心铁律:
每一个合数,必须且仅被它的“最小质因子”标记一次!
2. 核心代码的一行灵魂:if (i % p == 0) break;
在线性筛中,我们用 primes。内层循环枚举当前已知的所有质数
请紧盯这一行代码:
bad[i * p] = 1;
if (i % p == 0) break; // 灵魂刹车
为什么一旦 i % p == 0 就必须强制跳出?
- 这里的质数
是从小到大枚举的。当第一次出现 i % p == 0时,说明是 的最小质因子。 - 如果我们不刹车,继续枚举下一个更大的质数
( ),接下来被标记的数将是 。 - 来看这个新数
的质因数结构:因为 包含质因子 ,所以 也必然包含质因子 。 - 关键就在这里:
的质因子中既有 又有 ,而 !这意味着 的最小质因子根本不是 ,而是 ! - 如果此时让
这一对组合去标记 ,就彻底违背了“由最小质因子标记”的原则!这个合数 将来必然会在 、枚举到素数 时被再次标记,从而引发重复计算。
手动追踪一趟,彻底搞懂执行轨迹:
- 当
时:质数表里有 2, 3。- 乘
:标记 。 ,继续。 - 乘
:标记 。 ,触发 break!停下。
- 乘
- 当
时:质数表里有 2, 3。- 乘
:标记 。 ,触发 break!停下。(绝不去算 )。
- 乘
- 当
时:质数表里有 2, 3, 5。- 乘
:标记 。 ,触发 break!停下。(不会去标记 )。
- 乘
- 那么刚才没被处理的
到底谁来管? - 当
时:乘质数 ,标记 ! 的最小质因子 恰好被放在了质数侧!
- 当
没有一个合数会被漏掉,也没有一个合数会被重复标记。每一个数都被精准命中一次,时间复杂度是极其干净利落的 严格
3. 完整程序一:筛出所有素数
输入格式:输入一个整数
输出格式:第一行输出不超过
数据范围:
样例输入:
20
样例输出:
8
2 3 5 7 11 13 17 19
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1000005;
vector<int> primes;
bool bad[N]; // bad[x]=1 表示合数,bad[x]=0 表示素数
int n;
void solve(){
cin>>n;
for(int i=2;i<=n;i++){
if(!bad[i]) primes.push_back(i);
for(int j=0;j<primes.size();j++){
int p=primes[j];
if(p>n/i) break; // 防止 i*p 越过数组上限
bad[i*p]=1;
if(i%p==0) break; // 核心:保证每个合数只被最小质因子筛除
}
}
cout<<primes.size()<<'\n';
for(int i=0;i<primes.size();i++){
cout<<primes[i]<<(i+1==primes.size()?"":" ");
}
cout<<'\n';
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0);
solve();
return 0;
}
四、欧拉函数 :容斥计数与在线性筛上的顺手牵羊
1. 什么是欧拉函数?
定义:
特别约定:
直观例子:
2. 容斥原理的降维推导
想知道
假设
- 一共有
个数; - 减去
的倍数个数: ; - 减去
的倍数个数: ; - 加上多减了的公倍数(即
的倍数)个数: 。
全部合起来:
推广到任意一般情况,欧拉函数的计算公式就是如此纯粹与优雅:
⚠️ 避坑提醒:公式里的每一项只取决于“不同的质因子”。
,虽然有两个 ,但也只能乘一次 。
如果单独求单个数的 ans = ans / p * (p - 1)(先除后乘防止溢出),复杂度同样是
3. 欧拉函数与线性筛的完美合体
如果题目需要我们频繁查询很多数的
能不能在跑线性筛标质数的同时,顺手把所有数的
答案是绝对的。回顾线性筛生成新数
基础基座: 是质数
显然
分支一:i % p != 0
说明
分支二:i % p == 0
说明
绝妙的契合:这两个分支,分毫不差地对应了线性筛的 if(i % p == 0) 的判断逻辑!我们一行多余的代码都不用加,欧拉函数就已经借着线性筛的东风算好了。
4. 完整程序二:多次查询欧拉函数
输入格式:第一行输入两个整数
输出格式:对每个查询
数据范围:
样例输入:
12 5
1 2 5 8 12
样例输出:
1
1
4
4
4
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1000005;
vector<int> primes;
int phi[N];
bool bad[N];
int n,q;
void solve(){
cin>>n>>q;
phi[1]=1; // 物理特判:gcd(1,1)=1
for(int i=2;i<=n;i++){
if(!bad[i]){
primes.push_back(i);
phi[i]=i-1; // 质数的欧拉函数为 p-1
}
for(int j=0;j<primes.size();j++){
int p=primes[j];
if(p>n/i) break;
int v=i*p;
bad[v]=1;
if(i%p==0){
// 分支二:p 已经是 i 的质因子,集合无新增,直接乘 p
phi[v]=phi[i]*p;
break;
}
// 分支一:p 是全新质因子,根据互质积性转移
phi[v]=phi[i]*(p-1);
}
}
while(q--){
int x;
cin>>x;
cout<<phi[x]<<'\n';
}
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0);
solve();
return 0;
}
5. 手推追踪:前 12 个数的填表过程
把抽象公式还原到直观表格,看看程序到底怎么流水线式递推:
- 初始化
。 :质数, 。通过 命中分支二,直接填好 。 :质数, 。 - 乘质数
(分支一):填好 。 - 乘质数
(分支二):填好 。
- 乘质数
:合数,但 早就被算好了! - 乘质数
(分支二):填好 。
- 乘质数
:质数, 。 - 乘质数
(分支一):填好 。
- 乘质数
:合数, 已经就绪! - 乘质数
(分支二):填好 。
- 乘质数
不需要在处理
五、欧拉定理:同余世界里的周期律与“互质红线”
在快速幂求逆元时,大家都熟背费马小定理:若
但如果模数不是质数,而是一个普通的合数
若
且 ,则:
1. 指数降维打击
欧拉定理在竞赛中最大的战术价值,就是把天文数字级别的巨大指数拍扁。
只要满足
经典实战小例:求
2. 考场高压触电警告:底数与模数不互质!
千万不要把欧拉定理当成“万能指数取模公式”!
看这个反例:求
- 如果有人盲目套公式:
,他把指数 ,得出 。 - 但事实上,
!算出来的结果截然相反!
为什么崩溃了?因为
六、约数进阶:约数和的物理拼图与 提取
1. 约数和的本质:乘法分配律的展开
刚才我们看到,72 的所有约数,都是由不同指数的 2 和 3 组合出来的。 如果我们想把这 12 个约数全部加起来,暴力相加当然可以,但数学上有一种直接打包计算的方法。
请看这个式子:
根据乘法分配律,左边括号里的每一项,都会和右边括号里的每一项相乘一次。这就完美穷举了刚才表格里的所有 12 个组合。 因此,这个乘积的结果,丝毫不差地等于 72 的所有约数之和。
推广到一般情况:对于 while 循环直接累加。
2. 提取单个数的所有约数
知道约数个数不够,很多题目要求我们把
物理推导:约数永远是成对出现的。如果你发现
细节处理:
- 当
是完全平方数(如 36),枚举到 时, 也是 6,小心别把它重复添加。 - 应对
的大整数时,配对出来的约数完全可能超过 32 位整型上限,必须使用 vector<long long>来接收!
vector<long long> get_divisors(long long x) {
vector<long long> res;
for (long long i = 1; i <= x / i; i++) {
if (x % i == 0) {
res.push_back(i);
if (i != x / i) {
res.push_back(x / i); // 配对的另一个约数
}
}
}
return res;
}
七、线性筛的隐藏彩蛋:维护最小质因子实现极速分解
场景痛点:给你
思路拆解:
还记得线性筛的铁律吗?——每一个合数,都是被它的“最小质因子”标记的。
那我们能不能在线性筛的过程中,顺手把每个数的最小质因子(可以简写为 min_p)记录下来?
有了 min_p 数组,质因数分解就像顺藤摸瓜一样简单,比如要分解 12:
- 查表
min_p[12]是 2,记录下 2,让 12 变成 6。 - 查表
min_p[6]是 2,记录下 2,让 6 变成 3。 - 查表
min_p[3]是 3,记录下 3,让 3 变成 1。分解结束!
整个过程只有查表和除法,单次分解的时间复杂度降到了
1. 完整程序三:极速多次质因数分解
输入格式:第一行输入两个整数
样例输入:
100 3
12
30
98
样例输出:
2 2 3
2 3 5
2 7 7
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=10000005;
vector<int> primes;
int32_t min_p[N]; // 存最小质因子,最大 10^7。使用 int32_t 避免全局宏导致内存翻倍。
void init_sieve(int limit) {
for (int i = 2; i <= limit; i++) {
if (!min_p[i]) {
min_p[i] = i; // 质数的最小质因子就是自己
primes.push_back(i);
}
for (int j = 0; j < primes.size(); j++) {
int p = primes[j];
if (p > limit / i) break;
min_p[i * p] = p; // 顺手牵羊:i*p 的最小质因子显然是 p
if (i % p == 0) break;
}
}
}
void solve() {
int m, q;
cin >> m >> q;
init_sieve(m);
while (q--) {
int x;
cin >> x;
// O(log x) 顺藤摸瓜式分解
while (x > 1) {
cout << min_p[x] << (x == min_p[x] ? "" : " ");
x /= min_p[x];
}
cout << '\n';
}
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0);
solve();
return 0;
}
八、区间筛法入门与起筛位置(选学)
先修要求:熟练掌握埃氏筛原理与基本的时间复杂度概念。
极限场景:求区间
破局思路:
根据第一节的结论,一个合数
我们可以准备一个长度为 bool is_prime_range[1000005],用下标 x - L 来代表真实的数字
核心难点:对于每个质因子 (L + p - 1) / p * p。
但请注意两个关键边界:
- 如果求出来的倍数就是
自己(比如 ,算出来起筛点是 3),绝对不能把 自己当成合数筛掉。所以起步倍数至少要是 。 - 如果区间包含真实数字 0 或 1,必须把它们对应的偏移位置手动划掉,绝不能留下当质数!例如
时,下标 0 对应数字 1,要划掉;下标 1 对应数字 2,可别误伤。
综合起来,起筛点公式为:
long long start = max(2LL * p, (L + p - 1) / p * p);
找到起点后,每次让 start += p,在偏移数组中把对应的 start - L 划掉即可。
九、破除互质红线:扩展欧拉定理(降幂公式)(选学)
在前文我们留下了一个警告:当底数
降幂公式:
在不要求
指数长到读不进整数怎么办? 把 rem 只存模 lim 只存原数与 d,更新 rem=(rem*10+d)%phi、lim=min(phi,lim*10+d);中间乘法用能装下 10*phi 的类型。读完后,lim==phi 就说明原指数够大,该给 rem 加上一个 phi;否则 lim 就是原指数。不要只看余数判断指数大小。
实战对拍验证:
求
- 指数
,满足降幂条件。 - 降维后的新指数为:
。 - 根据公式,应该有
。
我们用真实数值验证:
十、渐进式实战练习与思维对拍
1. 练习一:大整数约数档案
- 题目要求:输入一个整数
( ),输出其正约数的个数。 - 输入示例:
72输出: 12;输入36输出: 9;输入1输出: 1。 - 教练破题指引:单点
,绝不可用筛法开数组。调用第一节的 factor(n),从ans = 1开始,遍历返回表,每个指数贡献 ans *= (c + 1)。函数已经收下了最后剩余的质因子,不要再乘一次!只有把试除循环直接写进 main、没有调用factor时,才需要自己处理末尾的x > 1。
2. 练习二:欧拉函数的暴力对拍器
- 题目要求:输入一个整数
( )。不使用任何质因数分解或筛法,直接写一层循环枚举 ,统计有多少个 满足 __gcd(i, n) == 1,输出该计数。 - 教练破题指引:线性筛欧拉函数的转移分支容易在考场紧张时写反。把这个
暴力和筛法对照,先穷举 ,就能抓出不少小错误。对拍怎么组织,见《综合建模与赛场调试》·对拍。
3. 深度反思提问
- 为什么
,但我们绝不能推论出“所有小于 的正整数都是质数”? - 同样是对
跑一遍质因数分解,为什么一份分解结果,既能被拿去算约数个数,又能被拿去算欧拉函数?二者对指数的处理方式有何本质不同? - (提示:约数关注的是指数的每一次独立选取;而欧拉函数关注的是质因子对互质比例的剔除,指数多大不影响被排除的比例)。