CF1900D - Small GCD【数论超好题】
CF1900D - Small GCD【数论超好题】
前置知识
正题
题意
给定一个数组 $a$,找出 $a$ 中所有不同的三元组 $(x, y, z)$,求所有 $\gcd(min1, min2)$ 的和,$min1$ 为三元组中最小的数,$min2$ 为三元组中次小的数。
思路
由于该题是取三元组中较小的两个数,所以说最大的那个数并不重要,只需要找出所有二元组,并乘上可以为第三个数的数的个数即可。
再而,取三元组并不需要考虑下标,所以我们可以将 $a$ 数组升序排序,快速得到可以为第三个数的数的个数,即 $n - j$($j$ 为次小的数的排序后下标)。
显然,我们现在就可以得到一个 $\text{O}(n^2\log n)$ 的做法。
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i ++) {
for (int j = i + 1; j <= n; j ++) {
ans += gcd(a[i], a[j]) * (n - j);
}
}
那怎么优化呢?
一种优化思路是:仅枚举三元组中一个数,再用每种算法快速求出可能的三元组的值。
那枚举哪个数呢?
中间值:枚举出它之后,最大数的个数已知,且最小数的范围也已知。
那么我们需要快速求出的值就变为了 $\sum_{i = 1}^{j - 1} \gcd(a_i, a_j)$。
接着,我们对 $\gcd(a_i,a_j)$ 进行欧拉反演:$$ \gcd(a_i,a_j) = \sum_{d \mid \gcd(a_i, a_j)} \varphi(d) = \sum_{d \mid a_j} \varphi(d) [d \mid a_i] $$ 再将其代回原式:$$ \sum_{i = 1} ^ {j - 1} \sum_{d \mid a_j} \varphi(d)[d | a_i] $$ 将统计可以相约的循环移进来:$$ \sum_{d \mid a_j} \varphi(d) \sum_{i = 1}^{j - 1}[d | a_i] $$ 然后我们惊奇的发现,$ \sum_{i = 1}^{j - 1}[d | a_i]$ ,为小于 $a_j$ 的数中包含因数 $d$ 的数的个数,可以递推出来!
所以复杂度瓶颈仅在于 $\sum_{d \mid a_j} \varphi(d)$。
又因为 $\varphi(d)$ 可以用线性筛快速求出,且一个数的因数个数近似于 $\log(n)$。
所以时间复杂度近似于 $\text{O}(n+t\times\log(n))$,可以通过此题。
Code
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 8e4 + 10, M = 1e5 + 10;
typedef long long ll;
ll n, t, ans;
ll a[N], pri[M], tot, phi[M], cnt[M];
vector<ll> evn[M];
bool isp[M];
void pre(int Max){//线性筛求欧拉函数
for(int i = 1; i <= Max; i ++) isp[i] = 1;
isp[1] = 0;
phi[1] = 1;
for(int i = 2; i <= Max; i ++){
if(isp[i]){
pri[++ tot] = i;
phi[i] = i - 1;
}
for(int j = 1;j <= tot && i * pri[j] <= Max;j ++){
isp[i * pri[j]] = 0;
if(i % pri[j]) phi[i * pri[j]] = phi[i] * phi[pri[j]];//积性函数的性质
else{
phi[i * pri[j]] = phi[i] * pri[j];
break;
}
}
}
}
signed main() {
ios::sync_with_stdio(0);
for (int i = 1; i <= 1e5; i ++) {//预处理因数
for (int j = 1; j * j <= i; j ++) {
if (i % j == 0) {
evn[i].push_back(j);
if (j * j != i) evn[i].push_back(i / j);
}
}
}
pre(1e5);
for (cin >> t; t; t --) {
cin >> n;
ans = 0;
for (int i = 1; i <= 1e5; i ++) cnt[i] = 0;//多测清空
for (int i = 1; i <= n; i ++) cin >> a[i];
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i ++) {
// cntd * phi[d] * (n - i)
for (auto d : evn[a[i]]) ans += cnt[d] * phi[d] * (n - i);//上文推论
for (auto d : evn[a[i]]) cnt[d] ++;//更新 cnt[d]
}
cout << ans << endl;
}
return 0;
}