把 n! 的质因数分解转成逐质数统计:埃氏筛求出所有 ≤n 的质数,对每个质数 p 用勒让德公式 c_p = Σ⌊n/p^k⌋ 一次算出指数。
OJ: roj
题目 ID: 3086
难度:普及+/提高-
标签:数学数论素数筛法质因数分解python
创建: 2026-10-01 16:12
更新: 2026-10-07 13:50
形式化题目
给定整数
要求按
例如
2 3
3 1
5 1注意
正解
思路
先想最直接的做法。 按题意算出
退一步,还可以对每个
仍然远远超出 1s。
瓶颈在哪里? 两个朴素做法都默认「必须逐个处理
关键观察:勒让德公式。 在
- 能被
整除的数有 个,每个至少贡献 1 个因子 ; - 能被
整除的数有 个,每个还要再贡献 1 个; - 能被
整除的数有 个,每个再贡献 1 个; - …… 直到
为止。
把这些层加起来,就是
这个公式好记的直觉是「每
| 2 | 1 | |||
| 4 | 2 | |||
| 6 | 1 | |||
| 8 | 3 | |||
| 10 | 1 | |||
| 列合计 | 8 |
读表方式:横着看某个
剩下的问题:质数从哪来。 如果对每个
于是完整流程只有三步:
- 埃氏筛求出
的全部质数; - 对每个质数
用勒让德公式求 ; - 按
升序输出 p c。
输出形式的两个细节。 第一,p c 两列的输出题面里最容易被忽略,本题也要照顾到。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 16:13
# update_at: 2026-10-01 16:19
import sys
from math import isqrt
def sieve(n: int) -> bytearray:
"""埃氏筛:返回长度 n+1 的表,is_prime[i] 非 0 表示 i 是质数。"""
is_prime = bytearray([1]) * (n + 1) # 先全部当质数,再划掉合数
is_prime[:2] = bytes(min(2, n + 1)) # 0 和 1 不是质数
for i in range(2, isqrt(n) + 1):
if is_prime[i]:
# 从 i*i 起把 i 的倍数清成 0:切片赋值一次清空整段,长度用 range 数出来
is_prime[i * i :: i] = bytes(len(range(i * i, n + 1, i)))
return is_prime
def legendre(p: int, n: int) -> int:
"""勒让德公式:n! 中质数 p 的指数 = n//p + n//p^2 + n//p^3 + ..."""
power, count = n, 0
while power:
power //= p
count += power
return count
def solve() -> None:
it = iter(sys.stdin.buffer.read().split())
n = int(next(it, 0))
# n! 的质因数只可能是 <= n 的质数,筛出它们再逐个套勒让德公式
is_prime = sieve(n)
out = [f"{p} {legendre(p, n)}" for p in range(2, n + 1) if is_prime[p]]
# 每个质数至少贡献一个因子,必有 c >= 1,所以非空时直接 join 就是题面要求的分行输出
sys.stdout.write("\n".join(out) + "\n" if out else "")
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 18:10
* update_at: 2026-10-06 18:10
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
typedef long long ll;
int n;
bool is_prime[MAXN]; // is_prime[i] = true 表示 i 是质数
// 埃氏筛:筛出 2 ~ n 的全部质数
void sieve(int limit) {
for (int i = 2; i <= limit; i++) {
is_prime[i] = true;
}
for (int i = 2; (ll)i * i <= limit; i++) {
if (is_prime[i]) {
// 从 i*i 开始把 i 的倍数划掉
for (int j = i * i; j <= limit; j += i) {
is_prime[j] = false;
}
}
}
}
// 勒让德公式:计算 n! 中质数 p 的指数
// c_p = n/p + n/p^2 + n/p^3 + ...
ll legendre(int p, int nn) {
ll power = nn;
ll cnt = 0;
while (power) {
power /= p;
cnt += power;
}
return cnt;
}
void solve() {
cin >> n;
// n = 1 时 1! = 1,没有质因子,输出为空
if (n < 2) {
return;
}
sieve(n);
for (int p = 2; p <= n; p++) {
if (is_prime[p]) {
cout << p << " " << legendre(p, n) << "\n";
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}复杂度
设筛上界为
- 时间:埃氏筛
;扫一遍 挑质数 ;勒让德公式对质数 只做 次整除,把次数按「第 层有哪些质数」重新分组可得 , 即 次, 比 只多一个 级别的尾巴。总计 。 - 空间:筛表是
字节的 bytearray(时 1MB);输出共 行、 约 706KB 文本。总计 ,实测峰值 22.7MB(含解释器),远低于 128MB。
实测:
对比:大整数做法要先算
实现上唯一需要留意的是埃氏筛的划除方式。纯 Python 里写 for j in range(i*i, n+1, i): is_prime[j] = 0
会执行约 is_prime[i*i::i] = bytes(len(range(i*i, n+1, i))),把整段连续内存一次清空,
划除工作落到 C 层完成,这才是 0.05s 的来源。另外 range(i*i, n+1, i) 在 bytes(0) 赋值不会报错,所以不必额外加 if。
总结
本题的骨架是「不分解乘积,而是逐个质数统计」:
的质因数集合就是所有 的质数,用埃氏筛一次求出; - 每个质数的指数彼此独立,可以单独用勒让德公式
算出; - 公式的直觉是分层数:每
个连续整数贡献 1 个 ,每 个再补 1 个; - 整体复杂度
,把朴素的 和大整数做法都甩开。
这类「积的因子统计」问题,通用的思路都是交换求和顺序:不要把因子按「哪个数」分组, 而要按「哪个质数」分组,分组后往往一个闭式公式就能解决。
图示解析
整条思路可以压成下面这张路线图:
求解目标:n! = ∏ p^c 的每个 (p, c)
│
├─ 想法 A:算出 n! 再试除 ────► 死因:10^6! 有 ~5.5×10^6 位十进制,乘法都超时
├─ 想法 B:对 i = 2..n 逐个试除 ──► 死因:Σ√i = O(n√n) ≈ 10^9
│
▼
关键观察:指数可分离 —— c_p 只依赖 p 和 n,与其他质数无关
│
├─ 有哪些 p ──► 埃氏筛,O(n log log n) 一次求出全部 ≤ n 的质数
│
└─ c_p 怎么算 ──► 勒让德公式分层数:
k=1 : ⌊n/p⌋ 个「至少含一个 p」
k=2 : ⌊n/p²⌋ 个「至少含两个 p」,再补 1
k=3 : ⌊n/p³⌋ … 直到 p^k > n
│
▼
对每个质数 p 输出一行 "p c_p"(c_p ≥ 1 恒成立)图中最关键的一步是中间那个分岔:朴素做法都在「枚举