题干:
Let a a a, b b b, and c c c be integers. We define function f ( a , b , c ) f(a, b, c) f(a,b,c) as follows:
Order the numbers a a a, b b b, c c c in such a way that a ≤ b ≤ c a \le b \le c a≤b≤c. Then return gcd ( a , b ) \gcd(a, b) gcd(a,b), where gcd ( a , b ) \gcd(a, b) gcd(a,b) denotes the greatest common divisor (GCD) of integers a a a and b b b.
So basically, we take the gcd \gcd gcd of the 2 2 2 smaller values and ignore the biggest one.
You are given an array a a a of n n n elements. Compute the sum of f ( a i , a j , a k ) f(a_i, a_j, a_k) f(ai,aj,ak) for each i i i, j j j, k k k, such that 1 ≤ i < j < k ≤ n 1 \le i < j < k \le n 1≤i<j<k≤n.
More formally, compute
∑ i = 1 n ∑ j = i + 1 n ∑ k = j + 1 n f ( a i , a j , a k ) . \sum_{i = 1}^n \sum_{j = i+1}^n \sum_{k =j +1}^n f(a_i, a_j, a_k). i=1∑nj=i+1∑nk=j+1∑nf(ai,aj,ak).
输入要求:
Input
Each test contains multiple test cases. The first line contains the number of test cases t t t ( 1 ≤ t ≤ 10 1 \le t \le 10 1≤t≤10). The description of the test cases follows.
The first line of each test case contains a single integer n n n ( 3 ≤ n ≤ 8 ⋅ 1 0 4 3 \le n \le 8 \cdot 10^4 3≤n≤8⋅104) — length of the array a a a.
The second line of each test case contains n n n integers, a 1 , a 2 , … , a n a_1, a_2, \ldots, a_n a1,a2,…,an ( 1 ≤ a i ≤ 1 0 5 1 \le a_i \le 10^5 1≤ai≤105) — elements of the array a a a.
It is guaranteed that the sum of n n n over all test cases does not exceed 8 ⋅ 1 0 4 8 \cdot 10^4 8⋅104.
输出要求:
Output
For each test case, output a single number — the sum from the problem statement.
分析:
看一下题目球的三个
∑
\sum
∑的取值,容易发现,题意就是求数组中所有的三元组的较小两个数的gcd求和。
为了简化,明显可以先对数组进行排序,那么不妨假设
f
(
a
i
,
a
j
,
a
k
)
中,有
f(a_i,a_j,a_k)中,有
f(ai,aj,ak)中,有
i
<
j
<
k
i<j<k
i<j<k,那么该函数的值也就等于
g
c
d
(
a
i
,
a
j
)
gcd(a_i,a_j)
gcd(ai,aj)。从暴力的角度来看,可以两层for循环枚举i和j,求一次gcd,并且乘上n-j,也就是k可以从j到n中任取,其三元组的f函数值均为
g
c
d
(
a
i
,
a
j
)
gcd(a_i,a_j)
gcd(ai,aj)。
那么很显然这样两层for循环,加上求gcd,复杂度是
O
(
n
2
l
o
g
n
)
O(n^2logn)
O(n2logn),明显会TLE,现在来考虑如何进行优化。
下面先来讲一下我的错误思路:
一开始我忘记考虑枚举 i i i, j j j之后还要乘 n − j n-j n−j了,以为能够直接简化成求数组的 ∑ i = 1 n ∑ j = i n g c d ( a i , a j ) \sum_{i=1}^n\sum_{j=i}^ngcd(a_i,a_j) ∑i=1n∑j=ingcd(ai,aj),如果是这样,那么一下子会想起之前的codeforces有道题就是利用容斥原理和调和级数 O ( n l o g n ) O(nlogn) O(nlogn)预处理出所有的数组中 d p [ i ] 表示 g c d = i dp[i]表示gcd=i dp[i]表示gcd=i的对数。然后我就去翻那道题的代码,顺便学了下这个小知识点,把代码一贴。发现明显和本题不一样,这时候我才意识到我忘记每对 g c d gcd gcd还需要乘上右端点到 n n n的距离。于是我就想着如何修正。随手写个样例画画,发现似乎对于 g c d = x gcd=x gcd=x的对数 d p [ x ] dp[x] dp[x],只需要再去维护一个数组 d i s [ x ] dis[x] dis[x]表示 g c d = x gcd=x gcd=x的对中,到右端点 n n n的距离和,也就是每个 g c d gcd gcd的贡献,最后统计答案时候就把 i ∗ d i s [ x ] i*dis[x] i∗dis[x]累加起来。但是问题就在于这个 d i s [ x ] dis[x] dis[x]我维护不来,想照猫画虎也用容斥处理,但是失败了。
直接讲一下我从排行榜里面看的别人代码的思路
其实原理我也不是特别清楚,半懂不懂,这里我主要翻译一下他的代码行为
首先还是对数组进行排序,然后总的利用到了三个数组
c
n
t
_
d
i
v
[
x
]
cnt\_div[x]
cnt_div[x]
f
[
x
]
f[x]
f[x]
d
p
[
x
]
dp[x]
dp[x],第一个表示拥有因数
x
x
x的数量,第二个数组表示公因数为
x
x
x的到右端点距离和,第三个表示
最大公因数为
x
最大公因数为x
最大公因数为x的到右端点距离和。
然后对数组从小到大遍历,对
a
[
i
]
a[i]
a[i],新建一个vector,进行因数分解,统计出所有的因数数量,那么假如枚举的第二个数是它,它对距离的贡献,就是之前有了的这些因数的数量,乘上现在这个数到右端点的距离,所以遍历这个数的因数,记为
j
,
j
/
a
i
j,j/a_i
j,j/ai,我们需要更新的贡献也就是
f
[
j
]
+
=
c
n
t
_
d
i
v
[
j
]
∗
(
n
−
i
)
f[j]+=cnt\_div[j]*(n-i)
f[j]+=cnt_div[j]∗(n−i),最后再更新已有的所有因数数量
c
n
t
_
d
i
v
[
j
]
cnt\_div[j]
cnt_div[j]。这样处理出所有的因数的贡献和,最后一步利用容斥,将因数的贡献
f
[
x
]
f[x]
f[x],转化为最大公因数
d
p
[
x
]
dp[x]
dp[x]的贡献,见下面的代码所示:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+10;
int cnt_div[maxn],arr[maxn],dp[maxn],f[maxn];
void solve(){
memset(cnt_div,0,sizeof(cnt_div));
memset(dp,0,sizeof(dp));
memset(f,0,sizeof(f));
int n;cin>>n;
for(int i=1;i<=n;i++) cin>>arr[i];
sort(arr+1,arr+1+n);
for(int i=1;i<=n;i++){
vector<int>div;//对arr[i]质因数分解
for(int j=1;j*j<=arr[i];j++){
if(arr[i]%j==0){
div.push_back(j);
if(arr[i]/j!=j)
div.push_back(arr[i]/j);
}
}
for(int j:div)
f[j]+=cnt_div[j]*(n-i);
for(int j:div)
cnt_div[j]++;
}
for(int i=maxn-5;i;i--){
dp[i]=f[i];
for(int j=i+i;j<maxn;j+=i)
dp[i]-=dp[j];
}
int ans=0;
for(int i=1;i<maxn;i++)
ans+=dp[i]*i;
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;cin>>t;
while(t--) solve();
return 0;
}
下面是调和级数,也是这种容斥的复杂度正确性证明。
⌊
m
1
⌋
+
⌊
m
2
⌋
+
⌊
m
3
⌋
+
⌊
m
4
⌋
+
⋯
⌊
m
m
⌋
=
m
l
o
g
m
\lfloor \frac{m}{1}\rfloor+\lfloor \frac{m}{2}\rfloor+\lfloor \frac{m}{3}\rfloor+\lfloor \frac{m}{4}\rfloor+\cdots\lfloor \frac{m}{m}\rfloor=mlogm
⌊1m⌋+⌊2m⌋+⌊3m⌋+⌊4m⌋+⋯⌊mm⌋=mlogm
补充
在知乎上刷到了很多这场的题解,看了其他人的做法,又发现了两种很不错的思路,下面介绍一下,分别来自碎月和cup-pyy。
第一种,欧拉反演
看了碎月的题解之后,学到了用数论这边的方法来处理这题,或者说是怎么用迪利克雷卷积这种感觉很高端的知识点来解题。
首先是对题目需求的变形,这种做法的思路还是去枚举中间值
a
j
a_j
aj,那么对答案的贡献就会加上
(
n
−
j
)
∗
∑
i
=
1
j
−
1
g
c
d
(
a
i
,
a
j
)
(n-j)*\sum_{i=1}^{j-1}gcd(a_i,a_j)
(n−j)∗∑i=1j−1gcd(ai,aj),也就是
j
j
j到右端点的距离乘上
a
j
a_j
aj和左边所有数的
g
c
d
gcd
gcd的和,现在的问题就是如何快速地求出
∑
i
=
1
j
−
1
g
c
d
(
a
i
,
a
j
)
\sum_{i=1}^{j-1}gcd(a_i,a_j)
∑i=1j−1gcd(ai,aj)。
首先是一个公式欧拉反演
n
=
∑
d
/
n
φ
(
d
)
n=\sum_{d/n}\varphi(d)
n=∑d/nφ(d)
然后假设
g
c
d
(
a
i
,
a
j
)
=
x
gcd(a_i,a_j)=x
gcd(ai,aj)=x,那么
x
=
∑
d
/
x
φ
(
d
)
x=\sum_{d/x}\varphi(d)
x=∑d/xφ(d),那么这里面的
d
d
d肯定也是
a
i
a_i
ai和
a
j
a_j
aj的因数,所以我们不妨去枚举
a
j
a_j
aj的因数
d
d
d,对于
d
d
d,如果之前的数中也有某个数
a
x
a_x
ax的因数包含
d
d
d,那么
g
c
d
(
a
x
,
a
j
)
gcd(a_x,a_j)
gcd(ax,aj)的因数肯定包含
d
d
d,根据欧拉反演的公式,这个
d
d
d对于要求的
g
c
d
gcd
gcd求和的贡献就是
d
∗
φ
(
d
)
d*\varphi(d)
d∗φ(d),所以可以用数组
c
n
t
cnt
cnt来统计
j
j
j之前的数中的所有因数的个数,让
c
n
t
[
j
]
∗
φ
(
d
)
cnt[j]*\varphi(d)
cnt[j]∗φ(d)表示
a
j
a_j
aj的因数
d
d
d为新增的
g
c
d
gcd
gcd的和的贡献,求和之后总的再乘上距离
n
−
j
n-j
n−j,即可得到答案。总的代码如下,可以好好体会一下这种巧妙利用欧拉反演,快速求单个数和之前所有数的
g
c
d
gcd
gcd和的方法。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+10;
int phi[maxn],vis[maxn],cnt1=0,prime[maxn];
int cnt[maxn],arr[maxn];
void init_phi(){
phi[1]=1;
for(int i=2;i<maxn;i++){
if(!vis[i])
prime[++cnt1]=i,phi[i]=i-1;
for(int j=1;j<=cnt1&&i*prime[j]<maxn;j++){
vis[i*prime[j]]=1;
if(i%prime[j]==0){
phi[i*prime[j]]=phi[i]*prime[j];
break;
}
else
phi[i*prime[j]]=phi[prime[j]]*phi[i];
}
}
}
void solve(){
memset(cnt,0,sizeof(cnt));
int n;cin>>n;
for(int i=1;i<=n;i++) cin>>arr[i];
sort(arr+1,arr+1+n);
int ans=0;
for(int i=1;i<=n;i++){
int sum=0;
for(int j=1;j*j<=arr[i];j++)
if(arr[i]%j==0){
sum+=cnt[j]*phi[j],cnt[j]++;
if(j*j!=arr[i]){
sum+=cnt[arr[i]/j]*phi[arr[i]/j];
cnt[arr[i]/j]++;
}
}
ans+=sum*(n-i);
}
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
init_phi();
int t;cin>>t;
while(t--) solve();
//system("pause");
return 0;
}
第二种,容斥
这位佬也是用的容斥来做,带有讲解的话比最开始硬看排行榜的代码理解起来要容易多了,不过这种做法还是有一点小绕的,和我之前学的那种容斥刚好反向,具体代码理解起来还是有点麻烦。下面先讲一下思路:
我们不再去枚举中间数
a
j
a_j
aj,转而去枚举最大的数字
a
k
a_k
ak,那么这个
a
k
a_k
ak的贡献就是
k
k
k之前所有的数两两
g
c
d
gcd
gcd的和,用一个变量
p
r
e
pre
pre去记录,每次新枚举一个数字,对
a
n
s
ans
ans加上这个
p
r
e
pre
pre,然后再去考虑这个新的
a
k
a_k
ak能跟前面的数新增多大的
g
c
d
gcd
gcd。
具体来说,先去预处理出每个数的所有因数,存到
v
e
c
t
o
r
vector
vector里面,用一个数组
c
n
t
[
i
]
cnt[i]
cnt[i]表示枚举到的数前面的因数里
i
i
i的倍数的个数,数组
d
p
[
i
]
dp[i]
dp[i]表示与当前
a
k
a_k
ak的
g
c
d
gcd
gcd为
i
i
i的对数。
那么,逆向枚举
a
k
a_k
ak的因数
y
y
y,假设之前的所有
a
i
a_i
ai中,
g
c
d
(
a
i
,
a
k
)
=
y
gcd(a_i,a_k)=y
gcd(ai,ak)=y的数量是
d
p
[
y
]
dp[y]
dp[y],
d
p
[
y
]
dp[y]
dp[y]的表达式为
c
n
t
[
y
]
−
∑
k
=
1
∞
d
p
[
k
∗
y
]
cnt[y]-\sum_{k=1}^{\infty}dp[k*y]
cnt[y]−∑k=1∞dp[k∗y],这里就跟一开始的做法讲的那种容斥类似了,然后就是代码中如何实现这个式子。题解里面的做法是逆向枚举
y
y
y的同时,再去遍历
y
y
y的因数,给它们的
d
p
dp
dp值提前减去
d
p
[
y
]
dp[y]
dp[y],就是在枚举大的时候减去小的,而不像之前那样枚举小的,再遍历小的倍数减大的,就是这一步逆向操作,让我看了好久才理解。
具体的代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+10;
int cnt[maxn],arr[maxn],dp[maxn];
vector<int>fac[maxn];//预处理所有的因数
void solve(){
memset(cnt,0,sizeof(cnt));
memset(dp,0,sizeof(dp));
int n;cin>>n;
for(int i=1;i<=n;i++) cin>>arr[i];
sort(arr+1,arr+1+n);
int ans=0,pre=0;//pre维护之前所有的gcd的和,之后枚举的是三元组中的最大数
for(int i=1;i<=n;i++){
ans+=pre;
//下面考虑将pre增加第i个数和之前所有的gcd和
int x=arr[i];
for(int j=fac[x].size()-1;j>=0;j--){
int t=fac[x][j];//t为逆向枚举的x的因数
dp[t]+=cnt[t];
pre+=t*dp[t];
for(auto tt:fac[t])
dp[tt]-=dp[t];
}
for(auto t:fac[x])
cnt[t]++;
}
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
for(int i=1;i<maxn;i++)
for(int j=i;j<maxn;j+=i)
fac[j].push_back(i);
int t;cin>>t;
while(t--) solve();
//system("pause");
return 0;
}
总结
就在写题解的时候,我忽然意识到,上面两种做法的本质其实是一样,都是在维护新增一个数和先前所有数的 g c d gcd gcd的和,不过一种是用容斥解决,一种是用欧拉反演来解决,不过恰好是一个枚举中间数 a j a_j aj,一个是枚举后面数 a k a_k ak,实际上这种枚举区别不大,核心还是一样的。