202512-相等序列(luogu-P14918)

202512-相等序列(luogu-P14918)

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

luogu-P14918 [GESP202512 五级] 相等序列

题目要求

题目描述

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

  • 选择序列中一个正整数 AiA_i1iN1\le i\le N),将 AiA_i 变为 Ai×PA_i\times PPP 为任意质数;
  • 选择序列中一个正整数 AiA_i1iN1\le i\le N),将 AiA_i 变为 AiP\frac{A_i}{P}PP 为任意质数,要求 AiA_iPP 的倍数。

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

输入格式

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

第二行包含 NN 个正整数 A1,A2,,ANA_1,A_2,\ldots,A_N,代表序列 AA

输出格式

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

输入输出样例 #1

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

说明/提示

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

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


题目分析

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

1. 问题转化

题目中允许的两种操作:

  1. AiA_i 乘以一个质数 PP(代价 11)。
  2. AiA_i 除以一个质数 PP(代价 11)。

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

Ai=p1ei,1×p2ei,2×A_i = p_1^{e_{i,1}} \times p_2^{e_{i,2}} \times \dots,最终目标是将所有数变为 T=p1k1×p2k2×T = p_1^{k_1} \times p_2^{k_2} \times \dots。 对于某一个特定的质数 pp,将 AiA_i 中的指数 eie_i 变为目标指数 kk 的代价就是 eik|e_i - k|。 总代价为所有书在所有质因数上的指数变化代价之和:

Cost=质数pi=1Nei,pkp Cost = \sum_{\text{质数p}} \sum_{i=1}^{N} |e_{i,p} - k_p|

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

2. 中位数贪心

对于某一个质数 pp,我们在 NN 个数字中对应的指数分别为 e1,e2,,eNe_{1}, e_{2}, \dots, e_{N}(注意:如果某个数 AiA_i 不包含质因子 pp,则其对应指数为 00)。 我们要找一个整数 kk,使得

i=1Neik\sum_{i=1}^{N} |e_{i} - k|

最小。 这是一个经典的几何中位数性质的应用:在一维数轴上,到所有点距离之和最小的点是这些点的中位数

3. 算法流程

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

4. 细节处理

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

  • 设排序后的非零指数列表为 VV(下标 0m10 \dots m-1)。

  • 全体 NN 个指数排序后的中位数下标为 mid=N/2mid = \lfloor N/2 \rfloor(以 0 为起始索引)。

  • 判断逻辑

    • mid<Nmmid < N-m,说明中位数落在前缀的 00 序列中,即中位数 k=0k = 0
    • midNmmid \ge N-m,说明中位数落在非零序列中,即中位数 k=V[mid(Nm)]k = V[mid - (N-m)]
  • 代价计算

    • 对于 NmN-m00,其代价之和为 (Nm)0k=(Nm)k(N-m) \cdot \|0 - k\| = (N-m) \cdot k
    • 对于 mm 个非零指数,遍历 VV 累加 Vik\|V_i - k\| 即可。
  • 这种方法避免了为每个质数开辟长度为 NN 的数组,将空间复杂度优化至与所有数的质因数总数线性相关。

5. 时间复杂度

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

示例代码

#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;
}

本文由coderli.com原创,按照CC BY-NC-SA 4.0 进行授权

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

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

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

GESP/CSP认证交流QQ群: 688906745

GESP/CSP 认证学习微信公众号
GESP/CSP 认证学习微信公众号
最后更新于