变换

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

把终值拆成 a_i·P/q_i,公共因子藏进各 q_i 免费复用,推出答案 = ΣΩ(a_i) − n·Ω(gcd),筛法预处理后单遍扫描求得。

OJ: roj

题目 ID: 10007

难度:普及+/提高-

标签:数论质因数分解gcdpython

创建: 2026-10-02 17:36

更新: 2026-10-07 11:01

形式化题目

给定长度为 nn 的正整数序列 a1,…,ana_1,\dots,a_n。一次操作:选定一个数字不变,其余每个数字都乘以同一个素数 xx(xx 任选,不同操作可以不同)。求最少经过多少次操作,可以让序列中所有数字全部相同。变换过程中数值可以任意大,只需要输出最少次数。

正解

思路

朴素想法与瓶颈:

  1. 把「序列 + 当前数值」当状态做 BFS/搜索:数值没有上界,状态空间无穷,不可行。
  2. 照样例说明的直觉「把数逐个除到公共值」:操作里根本没有除法,只有「不动一个、其余乘素数」,这种规约没有对应操作。
  3. 把所有数都乘成一个公共大值(比如全体乘积 PP):这是可行的——对每个 aia_i,把它拆成素因子,每个素因子安排一次「ii 当不动点」的操作即可,代价 ∑iΩ(ai)\sum_i \Omega(a_i)(Ω(m)\Omega(m) 表示 mm 的质因数个数,计重数)。这是个正确上界,但不最优:若所有数共享因子 gg,上界把 Ω(g)\Omega(g) 在每个元素上重复付费了 nn 次,而这部分本可以免费。

把操作写成代数式:设第 tt 次操作的素数为 ptp_t、不动位置为 iti_t,P=∏tptP=\prod_t p_t。位置 jj 只有在「不动点不是 jj」的操作里才被乘,所以

终值j=aj⋅∏t: it≠jpt=aj⋅Pqj,qj=∏t: it=jpt.\text{终值}_j = a_j \cdot \prod_{t:\,i_t \neq j} p_t = a_j \cdot \frac{P}{q_j},\qquad q_j = \prod_{t:\,i_t = j} p_t.

qjq_j 是「jj 当不动点的那些操作」的素数乘积(从未被选中时 qj=1q_j=1)。

关键观察(三步塌缩):

  1. 所有终值相等   ⟺  \iff aj/qja_j/q_j 是与 jj 无关的常数 CC   ⟺  \iff CC 整除每个 aja_j、且 qj=aj/Cq_j = a_j/C。于是「操作序列搜索」变成了「选一个公因数 CC」。
  2. qjq_j 必须是若干素数之积,而任何 ⩾1\geqslant 1 的整数都能写成素数之积(质因数分解),所以任意公因数 CC 都能构造出合法方案。
  3. 计数:qjq_j 里的每个素因子都必须来自某次「不动点是 jj」的操作,所以 jj 至少要当 Ω(qj)\Omega(q_j) 次不动点;而每次操作恰好指定一个不动点、恰好给一个 qjq_j 贡献一个素因子,两头一凑,最少操作数就是 ∑jΩ(qj)\sum_j \Omega(q_j)(每个素因子各安排一次操作即可达到)。代入 qj=aj/Cq_j=a_j/C:
T=∑j=1nΩ(aj/C)=∑j=1nΩ(aj)−n⋅Ω(C).T = \sum_{j=1}^{n}\Omega(a_j/C) = \sum_{j=1}^{n}\Omega(a_j) - n\cdot\Omega(C).

要最小化 TT,就要让 CC 的质因子最多。任何公因数 CC 都整除 g=gcd⁡(a1,…,an)g=\gcd(a_1,\dots,a_n),而 C∣g⇒Ω(C)⩽Ω(g)C\mid g\Rightarrow\Omega(C)\leqslant\Omega(g),所以取 C=gC=g 最优:

 T=∑i=1nΩ(ai)−n⋅Ω(gcd⁡(a1,…,an)) \boxed{\,T = \sum_{i=1}^{n}\Omega(a_i) - n\cdot\Omega(\gcd(a_1,\dots,a_n))\,}

构造也即正确性:对每个 ii,把 ai/ga_i/g 质因数分解,每个素因子执行一次「ii 不动、乘该素数」。每个元素额外拿到的乘积都是 P=∏i(ai/g)P=\prod_i (a_i/g),终值统一为 g⋅Pg\cdot P;操作数恰好 ∑iΩ(ai/g)\sum_i\Omega(a_i/g),达到下界。

用公式核对两组数据(下表:Ω\Omega 列是各数质因数个数之和,gg 列是全体 gcd⁡\gcd):

数据 序列 ∑Ω(ai)\sum\Omega(a_i) gg Ω(g)\Omega(g) n⋅Ω(g)n\cdot\Omega(g) 答案
样例 1 5, 75,\ 7 1+1=21+1=2 11 00 00 22
测点 1 500000, 106500000,\ 10^6 11+12=2311+12=23 500000500000 1111 2222 11

样例里两数互质,g=1g=1 一分也免不掉,必须 1+1=21+1=2 次操作(如两次都变成 3535);测点 1 里 500000=25⋅56500000=2^5\cdot5^6 是 106=26⋅5610^6=2^6\cdot5^6 的因数,公共的 1111 个质因子全部免费,两数只差 106/500000=210^6/500000=2 这一个质因子,q2=2q_2=2 排一次「位置 2 不动、乘 2」的操作即可,共 11 次。

实现:ai⩽106a_i\leqslant10^6,先把 Ω\Omega 表用筛法一次性预处理(每个质数幂 pkp^k 给它的所有倍数 +1+1),再单遍扫输入同时累加 ∑Ω(ai)\sum\Omega(a_i) 和滚动 gcd⁡\gcd,最后套公式。

代码

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-02 17:40
# update_at: 2026-10-02 17:40

import sys
from math import gcd

MAX_A = 10**6  # 题面数据范围里 a_i 的上限


def build_omega() -> list[int]:
    """omega[n] = n 的质因数个数(计重数):质数幂 p^k 给它的每个倍数贡献 1。"""
    omega = [0] * (MAX_A + 1)
    for p in range(2, MAX_A + 1):
        if omega[p]:  # 下标是合数时已被更小的质数标记过
            continue
        power = p
        while power <= MAX_A:
            for multiple in range(power, MAX_A + 1, power):
                omega[multiple] += 1  # 多整除一个 p,就多计一个质因数
            power *= p
    return omega


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data)  # 序列长度
    omega = build_omega()

    # 扫输入时同步维护 ΣΩ(a_i) 与全体 a_i 的 gcd,不存整个序列
    total = 0
    common = 0
    for _ in range(n):
        value = next(data)  # 第 i 个元素 a_i
        total += omega[value]
        common = gcd(common, value)

    # 每个 a_i / g 的一个质因子恰好要占一次操作
    print(total - n * omega[common])


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-04 21:49
 * update_at: 2026-10-04 21:49
 */
// main.cpp:最小操作次数 = ΣΩ(a_i) - n·Ω(gcd)
// 每次操作“留一个数不变、其余乘素数”,把第 i 个数额外乘上的素数记为 q_i 的因子,
// 所有数最终相等等价于每个 a_i / (q_i) 都等于同一个公因数 C。
// 取 C = g = gcd 最优,于是答案就是每个 a_i 去掉 g 的质因子后,剩下的质因子个数之和。
// 用筛法预处理 Ω 表:质数幂 p^k 给它的每个倍数贡献 1,再单遍扫描累加即可。

#include <cstdio>

typedef long long ll;

const int MAX_A = 1000000; // 题面数据范围里 a_i 的上限

int omega[MAX_A + 1]; // omega[v] = v 的质因数个数(计重数),只有 0~20 级别,用 int 即可

// 筛法预处理 Ω 表:对每个质数 p,其幂 p^k(p^k <= MAX_A)给所有 p^k 的倍数多贡献一个质因子。
void build_omega() {
    for (int p = 2; p <= MAX_A; p++) {
        if (omega[p] != 0) {
            continue; // 合数已被更小的质数标记过,p 不是质数
        }
        for (int power = p; power <= MAX_A; ) {
            for (int multiple = power; multiple <= MAX_A; multiple += power) {
                omega[multiple]++;
            }
            if (power > MAX_A / p) {
                break; // 再乘一次 p 就超过值域上限,直接结束,避免 int 溢出
            }
            power *= p;
        }
    }
}

ll gcd(ll a, ll b) {
    while (b != 0) {
        ll t = a % b;
        a = b;
        b = t;
    }
    return a;
}

int main() {
    build_omega();

    ll n;
    if (scanf("%lld", &n) != 1) {
        return 0;
    }

    // 边读边累加 ΣΩ(a_i) 与全体 a_i 的 gcd,不保存整个序列
    ll sum_omega = 0;
    ll common = 0;
    for (ll i = 1; i <= n; i++) {
        ll value;
        if (scanf("%lld", &value) != 1) {
            break;
        }
        sum_omega += omega[value];
        common = gcd(common, value);
    }

    printf("%lld\n", sum_omega - n * omega[common]);
    return 0;
}

复杂度

  • 时间:预处理 Ω\Omega 表 O(Alog⁡log⁡A)O(A\log\log A)(A=106A=10^6 为值域上限),扫描输入 O(n)O(n),合计 O(Alog⁡log⁡A+n)O(A\log\log A + n),与标准 C++ 正解(筛法 + 单遍统计)同阶。
  • 空间:Ω\Omega 表 O(A)O(A)(约 8MB),输入边读边算不存整个序列,实测峰值 92MB,在 128MB 限制内。

总结

把「操作序列」翻译成 aj⋅P/qja_j\cdot P/q_j 是本题的分水岭:耦合所有元素的乘积 PP 被整体约掉,剩下每个元素只通过 qjq_j(自己当不动点时收下的素数)与方案互动。于是可行性塌缩成「CC 是公因数」,代价塌缩成「数质因子」——公共部分 gcd⁡\gcd 靠多当几次不动点免费复用,答案就是 ∑Ω(ai)−n⋅Ω(g)\sum\Omega(a_i)-n\cdot\Omega(g)。一次筛法预处理 + 一遍扫描即可,O(Alog⁡log⁡A+n)O(A\log\log A + n)。