阶乘分解

GitHub跳转原题关系图返回列表

把 n! 的质因数分解转成逐质数统计:埃氏筛求出所有 ≤n 的质数,对每个质数 p 用勒让德公式 c_p = Σ⌊n/p^k⌋ 一次算出指数。

OJ: roj

题目 ID: 3086

难度:普及+/提高-

标签:数学数论素数筛法质因数分解python

创建: 2026-10-01 16:12

更新: 2026-10-07 13:50

形式化题目

给定整数 nn,把 n!=1⋅2⋯nn! = 1 \cdot 2 \cdots n 写成算术基本定理的形式

n!=p1c1p2c2⋯pkck,p1<p2<⋯<pk, n! = p_1^{c_1} p_2^{c_2} \cdots p_k^{c_k}, \qquad p_1 < p_2 < \cdots < p_k,

要求按 pip_i 从小到大的顺序,输出每一对 (pi,ci)(p_i, c_i)。

例如 n=5n = 5 时 5!=120=23⋅31⋅515! = 120 = 2^3 \cdot 3^1 \cdot 5^1,所以输出三行:

text
2 3
3 1
5 1

注意 n!n! 的质因数集合恰好是「所有不超过 nn 的质数」,记 π(n)\pi(n) 为不超过 nn 的质数个数, 输出的行数就是它——n=106n = 10^6 时 π(106)=78498\pi(10^6) = 78498 行。

正解

思路

先想最直接的做法。 按题意算出 n!n! 的数值,再从小到大拿质数去试除。题面给的上界是 n⩽106n \leqslant 10^6,这个做法在本题规模下根本跑不动:106!10^6! 的十进制位数约 5.5×1065.5 \times 10^6 位, 光是做 nn 次大整数乘法就已经超时,之后还要用 π(n)≈7.8×104\pi(n) \approx 7.8 \times 10^4 个质数逐个去试除这个巨数。

退一步,还可以对每个 i=2,3,…,ni = 2, 3, \dots, n 单独试除分解,把得到的指数累加起来。 这个做法不用大整数,但每个 ii 试除一遍要 O(i)O(\sqrt i),总计

∑i=2nO(i)=O(nn)≈109, \sum_{i=2}^{n} O(\sqrt i) = O(n\sqrt n) \approx 10^9 ,

仍然远远超出 1s。

瓶颈在哪里? 两个朴素做法都默认「必须逐个处理 ii」。但我们要的只是每个质数在整个 乘积里出现了几次,这件事本身可以按质数分开统计:质数 pp 的指数只和 pp、nn 有关, 和其他质数互不影响。于是问题从「分解 nn 个数」变成「对每个质数单独数一遍」,而单独的 一遍有闭式公式。

关键观察:勒让德公式。 在 1,2,…,n1, 2, \dots, n 里数 pp 的个数可以分层:

  • 能被 pp 整除的数有 ⌊n/p⌋\lfloor n/p \rfloor 个,每个至少贡献 1 个因子 pp;
  • 能被 p2p^2 整除的数有 ⌊n/p2⌋\lfloor n/p^2 \rfloor 个,每个还要再贡献 1 个;
  • 能被 p3p^3 整除的数有 ⌊n/p3⌋\lfloor n/p^3 \rfloor 个,每个再贡献 1 个;
  • …… 直到 pk>np^k > n 为止。

把这些层加起来,就是 n!n! 中 pp 的总指数:

cp=⌊np⌋+⌊np2⌋+⌊np3⌋+⋯ c_p = \left\lfloor \frac{n}{p} \right\rfloor + \left\lfloor \frac{n}{p^2} \right\rfloor + \left\lfloor \frac{n}{p^3} \right\rfloor + \cdots

这个公式好记的直觉是「每 pp 个连续整数贡献 1 个 pp,每 p2p^2 个再补 1 个,每 p3p^3 个再补 1 个」。 下面用 n=10n = 10、p=2p = 2 验证一遍(10!=3628800=28⋅34⋅52⋅710! = 3628800 = 2^8 \cdot 3^4 \cdot 5^2 \cdot 7,所以 c2c_2 应该是 8)。 表格的行为「哪个数」,列为「哪一层」,✓\checkmark 表示这个数在第 kk 层被数到一次:

ii v2(i)v_2(i) k=1k=1(2∣i2 \mid i) k=2k=2(4∣i4 \mid i) k=3k=3(8∣i8 \mid i)
2 1 ✓\checkmark
4 2 ✓\checkmark ✓\checkmark
6 1 ✓\checkmark
8 3 ✓\checkmark ✓\checkmark ✓\checkmark
10 1 ✓\checkmark
列合计 8 ⌊10/2⌋=5\lfloor 10/2 \rfloor = 5 ⌊10/4⌋=2\lfloor 10/4 \rfloor = 2 ⌊10/8⌋=1\lfloor 10/8 \rfloor = 1

读表方式:横着看某个 ii,它被打勾的次数正好是 v2(i)v_2(i);竖着看某一层,打勾的个数正好是 ⌊n/pk⌋\lfloor n/p^k \rfloor。所以「按行求和」和「按列求和」得到同一个数,而按列求和只需要 O(log⁡pn)O(\log_p n) 次除法——不用去看任何单个 ii。k=4k = 4 时 ⌊10/16⌋=0\lfloor 10/16 \rfloor = 0,循环到此停止。

剩下的问题:质数从哪来。 如果对每个 p=2,3,…,np = 2, 3, \dots, n 都试除判断一次质数,就退化回 O(nn)O(n\sqrt n) 了。改用埃氏筛:先假设 2∼n2 \sim n 全是质数,然后从小到大遇到质数 ii 就把 i2,i2+i,i2+2i,…i^2, i^2+i, i^2+2i, \dots 全部划掉。每个合数只会被划掉若干次(总次数 O(nlog⁡log⁡n)O(n \log\log n)), 一次筛出全部质数。

于是完整流程只有三步:

  1. 埃氏筛求出 2∼n2 \sim n 的全部质数;
  2. 对每个质数 pp 用勒让德公式求 cpc_p;
  3. 按 pp 升序输出 p c。

输出形式的两个细节。 第一,cpc_p 一定 ⩾⌊n/p⌋⩾1\geqslant \lfloor n/p \rfloor \geqslant 1(因为 p⩽np \leqslant n), 所以每个筛出来的质数都恰好占一行,不需要额外判断。第二,n=1n = 1 时 1!=11! = 1 没有质因子, 输出为空——这一点在只看 p c 两列的输出题面里最容易被忽略,本题也要照顾到。

代码

Python 版:

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++ 版(同一算法):

cpp
/**
 * 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;
}

复杂度

设筛上界为 NN(本题 N=nN = n)。

  • 时间:埃氏筛 O(Nlog⁡log⁡N)O(N \log\log N);扫一遍 2∼N2 \sim N 挑质数 O(N)O(N);勒让德公式对质数 pp 只做 ⌊log⁡pN⌋\lfloor \log_p N \rfloor 次整除,把次数按「第 kk 层有哪些质数」重新分组可得 ∑p⩽N⌊log⁡pN⌋=∑k⩾1π ⁣(⌊N1/k⌋)\sum_{p \leqslant N} \lfloor \log_p N \rfloor = \sum_{k \geqslant 1} \pi\!\left(\lfloor N^{1/k} \rfloor\right), 即 π(N)+π(N)+⋯=78498+168+25+11+⋯=78734\pi(N) + \pi(\sqrt N) + \cdots = 78498 + 168 + 25 + 11 + \cdots = 78734 次, 比 π(N)\pi(N) 只多一个 O(N)O(\sqrt N) 级别的尾巴。总计 O(Nlog⁡log⁡N)O(N \log\log N)。
  • 空间:筛表是 N+1N + 1 字节的 bytearray(10610^6 时 1MB);输出共 π(N)⩽78498\pi(N) \leqslant 78498 行、 约 706KB 文本。总计 O(N)O(N),实测峰值 22.7MB(含解释器),远低于 128MB。

实测:n=106n = 10^6 用时 0.05s;真实数据 5 组(9,100,9753,99991,1069, 100, 9753, 99991, 10^6)全部通过, 单组最长 0.050s。

对比:大整数做法要先算 106!10^6!(约 550 万位十进制)再试除;逐个试除分解是 O(nn)≈109O(n\sqrt n) \approx 10^9。 把「逐个 ii 分解」换成「逐个质数按层计数」,正是本题的关键一步。

实现上唯一需要留意的是埃氏筛的划除方式。纯 Python 里写 for j in range(i*i, n+1, i): is_prime[j] = 0 会执行约 Nlog⁡log⁡N≈2.1×106N \log\log N \approx 2.1 \times 10^6 次 Python 级赋值,反而成为瓶颈; 代码改用切片批量赋值 is_prime[i*i::i] = bytes(len(range(i*i, n+1, i))),把整段连续内存一次清空, 划除工作落到 C 层完成,这才是 0.05s 的来源。另外 range(i*i, n+1, i) 在 i2>ni^2 > n 时长度为 0, bytes(0) 赋值不会报错,所以不必额外加 if。

总结

本题的骨架是「不分解乘积,而是逐个质数统计」:

  • n!n! 的质因数集合就是所有 ⩽n\leqslant n 的质数,用埃氏筛一次求出;
  • 每个质数的指数彼此独立,可以单独用勒让德公式 cp=⌊n/p⌋+⌊n/p2⌋+⋯c_p = \lfloor n/p \rfloor + \lfloor n/p^2 \rfloor + \cdots 算出;
  • 公式的直觉是分层数:每 pp 个连续整数贡献 1 个 pp,每 p2p^2 个再补 1 个;
  • 整体复杂度 O(nlog⁡log⁡n)O(n \log\log n),把朴素的 O(nn)O(n\sqrt n) 和大整数做法都甩开。

这类「积的因子统计」问题,通用的思路都是交换求和顺序:不要把因子按「哪个数」分组, 而要按「哪个质数」分组,分组后往往一个闭式公式就能解决。

图示解析

整条思路可以压成下面这张路线图:

text
求解目标: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 恒成立)

图中最关键的一步是中间那个分岔:朴素做法都在「枚举 ii」这一侧, 而正解把它拆成「枚举 pp」——左支负责回答有哪些 pp(交给筛法), 右支负责回答每个 pp 出现几次(交给公式)。两支的规模分别是 O(nlog⁡log⁡n)O(n\log\log n) 与 O(log⁡pn)O(\log_p n),都比原来的 O(i)O(\sqrt i) 试除便宜得多。