文章

【GESP】C++ 五级真题解析,[2025年12月,第十二次认证]第二题-相等序列 luogu-p14918

GESP C++ 2025年12月,五级真题第二题,考察数论与贪心算法思想,对考试来说有一定难度。题目难度⭐⭐⭐☆☆。洛谷难度等级普及/提高−

P14918 [GESP202512 五级] 相等序列

题目要求

题目描述

小 A 有一个包含 $N$ 个正整数的序列 $A={A_1,A_2,\ldots,A_N}$。小 A 每次可以花费 $1$ 个金币执行以下任意一种操作:

  • 选择序列中一个正整数 $A_i$($1\le i\le N$),将 $A_i$ 变为 $A_i\times P$,$P$ 为任意质数;
  • 选择序列中一个正整数 $A_i$($1\le i\le N$),将 $A_i$ 变为 $\frac{A_i}{P}$,$P$ 为任意质数,要求 $A_i$ 是 $P$ 的倍数。

小 A 想请你帮他计算出令序列中所有整数都相同,最少需要花费多少金币。

输入格式

第一行一个正整数 $N$,含义如题面所示。

第二行包含 $N$ 个正整数 $A_1,A_2,\ldots,A_N$,代表序列 $A$。

输出格式

输出一行,代表最少需要花费的金币数量。

输入输出样例 #1

输入 #1
1
2
5
10 6 35 105 42
输出 #1
1
8

说明/提示

对于 $60\%$ 的测试点,保证 $1\le N,A_i\le 100$。

对于所有测试点,保证 $1\le N,A_i\le 10^5$。


题目分析

本题的本质是考察质因数分解中位数贪心

1. 问题转化

题目中允许的两种操作:

  1. 将 $A_i$ 乘以一个质数 $P$(代价 $1$)。
  2. 将 $A_i$ 除以一个质数 $P$(代价 $1$)。

这实际上是在调整 $A_i$ 的质因数分解中各个质因子的指数。

设 $A_i = p_1^{e_{i,1}} \times p_2^{e_{i,2}} \times \dots$,最终目标是将所有数变为 $T = p_1^{k_1} \times p_2^{k_2} \times \dots$。 对于某一个特定的质数 $p$,将 $A_i$ 中的指数 $e_i$ 变为目标指数 $k$ 的代价就是 $|e_i - k|$。 总代价为所有书在所有质因数上的指数变化代价之和: \(Cost = \sum_{质数p} \sum_{i=1}^{N} |e_{i,p} - k_p|\)

由于各个质因数的指数调整是相互独立的,我们可以对每一个质数单独考虑。

2. 中位数贪心

对于某一个质数 $p$,我们在 $N$ 个数字中对应的指数分别为 $e_{1}, e_{2}, \dots, e_{N}$(注意:如果某个数 $A_i$ 不包含质因子 $p$,则其对应指数为 $0$)。 我们要找一个整数 $k$,使得 \(\sum_{i=1}^{N} |e_{i} - k|\) 最小。 这是一个经典的几何中位数性质的应用:在一维数轴上,到所有点距离之和最小的点是这些点的中位数

3. 算法流程

  1. 预处理质数:使用线性筛(欧拉筛)预处理出 $1 \sim 10^5$ 范围内的素数。
  2. 分解质因数:对输入的 $N$ 个数进行质因数分解,统计每个质数在所有数中出现的指数。
    • 使用 vector<int> factors[MAXN] 来存储每个质数的非零指数列表。
  3. 计算代价
    • 遍历所有出现过的质数。
    • 对于质数 $p$,其具体的指数列表应包含所有的非零指数以及若干个 $0$(补齐 $N$ 个)。
    • 将非零指数排序,结合补齐的 $0$,找到这 $N$ 个指数的中位数。
    • 计算该质数下的总代价:所有指数与中位数的差的绝对值之和。
  4. 累加答案:将所有质数的代价累加即为最终结果。

4. 细节处理

隐式零的处理:在处理每个质数 $p$ 时,若其非零指数有 $m$ 个,则意味着有 $N-m$ 个数为 $0$。我们无需显式存储这些 $0$。

  • 设排序后的非零指数列表为 $V$(下标 $0 \dots m-1$)。
  • 全体 $N$ 个指数排序后的中位数下标为 $mid = \lfloor N/2 \rfloor$(以 0 为起始索引)。

  • 判断逻辑
    • 若 $mid < N-m$,说明中位数落在前缀的 $0$ 序列中,即中位数 $k = 0$。
    • 若 $mid \ge N-m$,说明中位数落在非零序列中,即中位数 $k = V[mid - (N-m)]$。
  • 代价计算
    • 对于 $N-m$ 个 $0$,其代价之和为 $(N-m) \cdot |0 - k| = (N-m) \cdot k$。
    • 对于 $m$ 个非零指数,遍历 $V$ 累加 $|V_i - k|$ 即可。
  • 这种方法避免了为每个质数开辟长度为 $N$ 的数组,将空间复杂度优化至与所有数的质因数总数线性相关。

5. 时间复杂度

  • 筛质数 $O(\max(A_i))$。
  • 分解质因数 $O(N \sqrt{\max(A_i)})$ 或更优(取决于实现,题目中预处理了质数,分解较快)。
  • 计算代价主要取决于排序,最坏情况下所有数的质因数个数总和相关,远小于 $O(N \log N)$ 级别。
  • 总体可以在 1s 内通过。


示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include <algorithm>
#include <cmath>
#include <iostream>
#include <vector>

typedef long long ll;

const int MAXN = 100005;
int a[MAXN];
// primes_ary 用于线性筛标记合数
int primes_ary[MAXN];
std::vector<int> primes;

// 记录每个质数在各数字中的指数列表。
// key: 质数 p (作为数组下标)
std::vector<int> factors[MAXN];

int main() {
    int N;
    std::cin >> N;

    for (int i = 1; i <= N; i++) {
        std::cin >> a[i];
    }

    // 预处理 1 到 100005 之间的所有质数
    primes_ary[1] = 1;  // 1 不是质数
    for (int i = 2; i < MAXN; i++) {
        if (primes_ary[i] == 0) {
            primes.push_back(i);
        }
        // 遍历已有的质数,标记合数
        for (int j = 0; j < primes.size(); j++) {
            // 如果超出范围则停止
            if ((ll)primes[j] * i >= MAXN) {
                break;
            }
            primes_ary[primes[j] * i] = 1;

            // 欧拉筛的核心:如果 i 是 prime[j] 的倍数,说明 i
            // 已经被更小的质因子筛过了
            // 此时停止,保证每个合数只被其最小质因子筛一次,达到 O(N) 复杂度。
            if (i % primes[j] == 0) {
                break;
            }
        }
    }

    for (int i = 1; i <= N; i++) {
        int tmp = a[i];

        // 分解质因数
        for (int p : primes) {
            // 如果 p*p > tmp,说明剩下的 tmp 要么是 1,要么是一个大于
            // sqrt(a[i]) 的质数。
            // 这样可以提前退出循环,避免无效遍历,显著降低复杂度。
            if (1LL * p * p > tmp) {
                break;
            }

            if (tmp % p == 0) {
                int exponent = 0;
                while (tmp % p == 0) {
                    tmp /= p;
                    exponent++;
                }
                // 将质因数 p 的指数 exponent(即p出现的字数) 存入对应的 vector
                // 中
                factors[p].push_back(exponent);
            }
        }
        // 处理最后剩下的那个较大的质因子
        if (tmp > 1) {
            factors[tmp].push_back(1);
        }
    }

    ll ans = 0;

    // 计算最小代价 ---
    // 遍历每一个出现过的质因数
    for (int p : primes) {
        std::vector<int> exps = factors[p];

        // 1. 对非零指数进行排序
        std::sort(exps.begin(), exps.end());

        // 2. 确定中位数
        // 序列总长度应为 N(包含 exps 中的非零值和 (N - exps.size()) 个 0)
        // 逻辑上的完整列表可以看作: [0, 0, ..., 0, exps[0], exps[1], ...]
        int num_zeros = N - exps.size();
        int mid_idx = N / 2;  // 中位数在逻辑列表中的下标(0-indexed)

        int median = 0;
        if (mid_idx >= num_zeros) {
            // 如果中位数下标落在了非零部分(即 exps 数组中)
            // 对应的 exps 下标为 mid_idx - num_zeros
            median = exps[mid_idx - num_zeros];
        } else {
            // 如果中位数下标落在 0 的区域
            median = 0;
        }

        // 3. 计算由于该质因数产生的代价
        // 代价 = sum(|x - median|)

        // (1) 那些值为 0 的数贡献的代价: |0 - median| * num_zeros = median *
        // num_zeros
        ll cost = (ll)median * num_zeros;

        // (2) 那些非零的数贡献的代价
        for (int e : exps) {
            cost += std::abs(e - median);
        }

        ans += cost;
    }

    std::cout << ans << '\n';
    return 0;
}

所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code

GESP 学习专题站:GESP WIKI

luogu-”系列题目可在洛谷题库进行在线评测。

bcqm-”系列题目可在编程启蒙题库进行在线评测。

欢迎加入Java、C++、Python技术交流QQ群(982860385),大佬免费带队,有问必答

欢迎加入C++ GESP/CSP认证学习QQ频道,考试资源总结汇总

欢迎加入C++ GESP/CSP学习交流QQ群(688906745),考试认证学员交流,互帮互助

GESP/CSP 认证学习微信公众号
GESP/CSP 认证学习微信公众号
本文由作者按照 CC BY-NC-SA 4.0 进行授权