CF2115A - Gellyfish and Flaming Peony

· · 题解

Description

给定一个包含 N 个正整数的数组 A。任意次操作:选择两个索引 nm,然后将 A_n 的值更新为 \gcd(A_n, A_m)。求出让数组中所有元素都相等所需的最少操作次数。

官方题解给出了本题的 Bonus 版本:Try to solve n, a_i \leq 2 \times 10^5 with only one test case. 设值域为 V,期望复杂度 \mathcal{O}(V\log V)

所有元素最终必然会相等,且等于整个初始数组的 GCD。问题的核心就变成了如何求得最小的 GCD。

Notes

Step 1 迭代求解

考虑到求得最小的 GCD 需要的数不会很多,事实上最大只会是 \omega(V)=8,即 V 以内一个数的最大素因子个数。

我们采用迭代的方式求解。迭代 t 轮次后得到:恰好选出 t 个数(允许重复),它们的 GCD 可能为哪些。循环 \omega(V) 轮次后就找出答案,每轮迭代期望复杂度 \mathcal{O}(V\log V)

总的复杂度 $\mathcal{O}(V\log^{3} V\omega(V))$,跑 2e5 的数据还是有点危险的。 --- ### Step 2 辅助数组与反演 进一步优化,引入辅助数组 - $G_{t}[i]$ 表示:是否有可能从 $f$ 中选一个数 $x$,再从 $g_{t - 1}$ 选一个数 $y$,它们的最大公约数 $\gcd(x, y)$ 是 $i$ 的倍数。 这样 $x$ 和 $y$ 都一定是 $i$ 的倍数,通过枚举倍数在 $\mathcal{O}(V\log V)$ 的时间就能求出 $h$ 数组。 显然 $\displaystyle G_{t}[n]=\sum_{k} g_{t}[nk]$,为了反过来,用 $G_{t}$ 表示 $g_{t}$,就需要用到反演。 Zeta - Möbius Transform 用于计算偏序集上一个元素的所有上级(或下级)元素的某个函数值的总和。在这里我们将其应用到「倍数——因子」这个上下级关系,即 $$ \begin{aligned} \displaystyle x[n] &= \sum_{d = 1}^ {\infty} \mu[d] \cdot X[n d] \\ \displaystyle X[n] &= \sum_{d = 1}^ {\infty} x[n d] \end{aligned} $$ 因此得到 $$ \displaystyle g_{t}[n] = \sum_{k}\mu[k] \cdot G_{t}[n k] $$ 其中 $d \mid n$ 表示 $d$ 是 $n$ 的约数,莫比乌斯函数 $\mu[n] = \begin{cases} 1, & n = 1 \\ 0, & \exists d > 1, d^{2} \mid n \\ (-1)^{\omega(n)}, & \text{otherwise}\end{cases}$ 可以用欧拉筛线性预处理。 总的复杂度 $\mathcal{O}(V\log V \cdot\omega(V))$。 ```cpp vector<int> f(V); // f[i]: 初始数组中数字 i 的出现次数 vector<int> g(V); // g[i]: 数字 i 当前是否可以被表示出来 for (auto x : a) { f[x] = 1; g[x] = 1; } for (int t = 1; ; t++) { vector<int> G(V); for (int i = 1; i < V; i++) { int sumf = 0; int sumg = 0; for (int j = i; j < V; j += i) { sumf += f[j]; sumg += g[j]; } G[i] = sumf * sumg; } for (int i = 1; i < V; i++) { g[i] = 0; for (int k = 1; i * k < V; k++) { g[i] += mu[k] * G[i * k]; } } if (g[d] > 0) { cout << (t + n - 1) << endl; return; } } ``` --- ### Step 3 在变换域操作 有变换就必然有卷积与乘法的对应,在数论中,这个卷积是 GCD 卷积, $$ c[k] = \sum_{\gcd(n, m) = k}^{} a[n] \cdot b[m] \xleftrightarrow [{\displaystyle x[n] = \sum_{d = 1}^ {\infty} \mu[d] \cdot X[n d]}] {\displaystyle X[n] = \sum_{d = 1}^ {\infty} x[n d]} C[k] = A[k] \cdot B[k] $$ 上面讲解中的 $G_{t}$ 实际上是 $f$ 和 $g_{t - 1}$ GCD 卷积的变换域表示,一个域的卷积对应于另一个域的乘法,$G_{t}[k]=F[k]\cdot G_{t-1}[k]$;而从 $G_{t}$ 倒推 $g_{t}$ 的过程,实际做了一次反 Mobius 变换。 从这个角度看,没有必要每次循环都先正变换再反变换。直接一直在变换域做点值乘法, $$ G_{t} = \underbrace{ F \ast F \ast \dots \ast F }_{t \text{ times}} $$ 然后只判断 $g_{t}[d]$ 这一项是否大于 0。 复杂度 $\mathcal{O}(V\log V)$。 ```cpp vector<int> f(V); // f[i]: 初始数组中数字 i 的出现次数 for (auto x : a) { f[x] = 1; } vector<int> F(V); // f 的变换域表示,原代码的 sumf for (int i = 1; i < V; i++) { for (int j = i; j < V; j += i) { F[i] += f[j]; } } auto G = F; // g[0] 的变换域表示,原代码的 sumg for (int t = 1; ; t++) { for (int i = 1; i < V; i++) { G[i] *= F[i]; // 做一次卷积 g[t] = g[t - 1] * f <=> G[t][i] = G[t][i] * F[i] } int res = 0; // 原代码的 g[t][d],第 t 次变换后,d 的值 for (int k = 1; d * k < V; k++) { res += mu[k] * G[d * k]; } if (res > 0) { cout << (t + n - 1) << endl; return; } } ``` --- ## Code ```cpp #include <bits/stdc++.h> using namespace std; struct sieve { vector<int> minp, primes; vector<int> mu; // 莫比乌斯函数 sieve(int N = 1e6) : minp(N), mu(N) { minp[1] = 1; mu[1] = 1; for (int n = 2; n < N; n++) { if (!minp[n]) { minp[n] = n; primes.push_back(n); mu[n] = -1; } for (int p : primes) { if (1ll * n * p >= N) break; minp[n * p] = p; if (p == minp[n]) { mu[n * p] = 0; break; } mu[n * p] = -mu[n]; } } } } sieve; int main() { cin.tie(nullptr)->sync_with_stdio(false); for (int t = (cin >> t, t); t--; ) [] { int n; cin >> n; vector<int> a(n); int d = 0; for (int i = 0; i < n; i++) { cin >> a[i]; d = gcd(d, a[i]); } int cnt = ranges::count(a, d); if (cnt) { cout << (n - cnt) << endl; return; } const int V = *ranges::max_element(a) + 1; vector<int> f(V); // f[i]: 初始数组中数字 i 的出现次数 for (auto x : a) { f[x] = 1; } vector<int> F(V); // f 的变换域表示,原代码的 sumf for (int i = 1; i < V; i++) { for (int j = i; j < V; j += i) { F[i] += f[j]; } } auto G = F; // g[0] 的变换域表示,原代码的 sumg for (int t = 1; ; t++) { for (int i = 1; i < V; i++) { G[i] *= F[i]; // 做一次卷积 g[t] = g[t - 1] * f <=> G[t][i] = G[t][i] * F[i] } int res = 0; // 原代码的 g[t][d],第 t 次变换后,d 的值 for (int k = 1; d * k < V; k++) { res += sieve.mu[k] * G[d * k]; } if (res > 0) { cout << (t + n - 1) << endl; return; } } } (); return 0; } ``` ## 评注 1. 数论中很多东西不太大,比如素数距离和素因子个数等,看似有好几层循环实际上很小。 2. 「恰好」不好做,尝试转化为「至少」或「至多」,再反演得到结果。本题是把 GCD 恰好等于某数转化为 GCD 是某数的倍数。