D._Small_GCD题解(codeforces 911 div.2)

题干:

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 abc. 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 1i<j<kn.

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=1nj=i+1nk=j+1nf(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 1t10). 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 3n8104) — 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 1ai105) — 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 8104.

输出要求:

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 ii<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 nj了,以为能够直接简化成求数组的 ∑ 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=1nj=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] idis[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](ni),最后再更新已有的所有因数数量 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
#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++){
        vectordiv;//对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>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) (nj)i=1j1gcd(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=1j1gcd(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 nj,即可得到答案。总的代码如下,可以好好体会一下这种巧妙利用欧拉反演,快速求单个数和之前所有数的 g c d gcd gcd和的方法。

#include
#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>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<>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=1dp[ky],这里就跟一开始的做法讲的那种容斥类似了,然后就是代码中如何实现这个式子。题解里面的做法是逆向枚举 y y y的同时,再去遍历 y y y的因数,给它们的 d p dp dp值提前减去 d p [ y ] dp[y] dp[y],就是在枚举大的时候减去小的,而不像之前那样枚举小的,再遍历小的倍数减大的,就是这一步逆向操作,让我看了好久才理解。
具体的代码如下:

#include
#define int long long
using namespace std;
const int maxn=1e5+10;
int cnt[maxn],arr[maxn],dp[maxn];
vectorfac[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<>t;
    while(t--) solve();
    //system("pause");
    return 0;
}

总结

就在写题解的时候,我忽然意识到,上面两种做法的本质其实是一样,都是在维护新增一个数和先前所有数的 g c d gcd gcd的和,不过一种是用容斥解决,一种是用欧拉反演来解决,不过恰好是一个枚举中间数 a j a_j aj,一个是枚举后面数 a k a_k ak,实际上这种枚举区别不大,核心还是一样的。

你可能感兴趣的:(c++,算法)