D._Small_GCD题解(codeforces 911 div.2)_d. small gcd-程序员宅基地

技术标签: 算法  c++  

题干:

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 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 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<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) (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<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=1dp[ky],这里就跟一开始的做法讲的那种容斥类似了,然后就是代码中如何实现这个式子。题解里面的做法是逆向枚举 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,实际上这种枚举区别不大,核心还是一样的。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Fun_Total/article/details/134650028

智能推荐

软件测试流程包括哪些内容?测试方法有哪些?_测试过程管理中包含哪些过程-程序员宅基地

文章浏览阅读2.9k次,点赞8次,收藏14次。测试主要做什么?这完全都体现在测试流程中,同时测试流程是面试问题中出现频率最高的,这不仅是因为测试流程很重要,而是在面试过程中这短短的半小时到一个小时的时间,通过测试流程就可以判断出应聘者是否合适,故在测试流程中包含了测试工作的核心内容,例如需求分析,测试用例的设计,测试执行,缺陷等重要的过程。..._测试过程管理中包含哪些过程

政府数字化政务的人工智能与机器学习应用:如何提高政府工作效率-程序员宅基地

文章浏览阅读870次,点赞16次,收藏19次。1.背景介绍政府数字化政务是指政府利用数字技术、互联网、大数据、人工智能等新技术手段,对政府政务进行数字化改革,提高政府工作效率,提升政府服务质量的过程。随着人工智能(AI)和机器学习(ML)技术的快速发展,政府数字化政务中的人工智能与机器学习应用也逐渐成为政府改革的重要内容。政府数字化政务的人工智能与机器学习应用涉及多个领域,包括政策决策、政府服务、公共安全、社会治理等。在这些领域,人工...

ssm+mysql+微信小程序考研刷题平台_mysql刷题软件-程序员宅基地

文章浏览阅读219次,点赞2次,收藏4次。系统主要的用户为用户、管理员,他们的具体权限如下:用户:用户登录后可以对管理员上传的学习视频进行学习。用户可以选择题型进行练习。用户选择小程序提供的考研科目进行相关训练。用户可以进行水平测试,并且查看相关成绩用户可以进行错题集的整理管理员:管理员登录后可管理个人基本信息管理员登录后可管理个人基本信息管理员可以上传、发布考研的相关例题及其分析,并对题型进行管理管理员可以进行查看、搜索考研题目及错题情况。_mysql刷题软件

根据java代码描绘uml类图_Myeclipse8.5下JAVA代码导成UML类图-程序员宅基地

文章浏览阅读1.4k次。myelipse里有UML1和UML2两种方式,UML2功能更强大,但是两者生成过程差别不大1.建立Test工程,如下图,uml包存放uml类图package com.zz.domain;public class User {private int id;private String name;public int getId() {return id;}public void setId(int..._根据以下java代码画出类图

Flume自定义拦截器-程序员宅基地

文章浏览阅读174次。需求:一个topic包含很多个表信息,需要自动根据json字符串中的字段来写入到hive不同的表对应的路径中。发送到Kafka中的数据原本最外层原本没有pkDay和project,只有data和name。因为担心data里面会空值,所以根同事商量,让他们在最外层添加了project和pkDay字段。pkDay字段用于表的自动分区,proejct和name合起来用于自动拼接hive表的名称为 ..._flume拦截器自定义开发 kafka

java同时输入不同类型数据,Java Spring中同时访问多种不同数据库-程序员宅基地

文章浏览阅读380次。原标题:Java Spring中同时访问多种不同数据库 多样的工作要求,可以使用不同的工作方法,只要能获得结果,就不会徒劳。开发企业应用时我们常常遇到要同时访问多种不同数据库的问题,有时是必须把数据归档到某种数据仓库中,有时是要把数据变更推送到第三方数据库中。使用Spring框架时,使用单一数据库是非常容易的,但如果要同时访问多个数据库的话事件就变得复杂多了。本文以在Spring框架下开发一个Sp..._根据输入的不同连接不同的数据库

随便推点

EFT试验复位案例分析_eft电路图-程序员宅基地

文章浏览阅读3.6k次,点赞9次,收藏25次。本案例描述了晶振屏蔽以及开关电源变压器屏蔽对系统稳定工作的影响, 硬件设计时应考虑。_eft电路图

MR21更改价格_mr21 对于物料 zba89121 存在一个当前或未来标准价格-程序员宅基地

文章浏览阅读1.1k次。对于物料价格的更改,可以采取不同的手段:首先,我们来介绍MR21的方式。 需要说明的是,如果要对某一产品进行价格修改,必须满足的前提条件是: ■ 1、必须对价格生效的物料期间与对应会计期间进行开启; ■ 2、该产品在该物料期间未发生物料移动。执行MR21,例如更改物料1180051689的价格为20000元,系统提示“对于物料1180051689 存在一个当前或未来标准价格”,这是因为已经对该..._mr21 对于物料 zba89121 存在一个当前或未来标准价格

联想启天m420刷bios_联想启天M420台式机怎么装win7系统(完美解决usb)-程序员宅基地

文章浏览阅读7.4k次,点赞3次,收藏13次。[文章导读]联想启天M420是一款商用台式电脑,预装的是win10系统,用户还是喜欢win7系统,该台式机采用的intel 8代i5 8500CPU,在安装安装win7时有很多问题,在安装win7时要在BIOS中“关闭安全启动”和“开启兼容模式”,并且安装过程中usb不能使用,要采用联想win7新机型安装,且默认采用的uefi+gpt模式,要改成legacy+mbr引导,那么联想启天M420台式电..._启天m420刷bios

冗余数据一致性,到底如何保证?-程序员宅基地

文章浏览阅读2.7k次,点赞2次,收藏9次。一,为什么要冗余数据互联网数据量很大的业务场景,往往数据库需要进行水平切分来降低单库数据量。水平切分会有一个patition key,通过patition key的查询能..._保证冗余性

java 打包插件-程序员宅基地

文章浏览阅读88次。是时候闭环Java应用了 原创 2016-08-16 张开涛 你曾经因为部署/上线而痛苦吗?你曾经因为要去运维那改配置而烦恼吗?在我接触过的一些部署/上线方式中,曾碰到过以下一些问题:1、程序代码和依赖都是人工上传到服务器,不是通过工具进行部署和发布;2、目录结构没有规范,jar启动时通过-classpath任意指定;3、fat jar,把程序代码、配置文件和依赖jar都打包到一个jar中,改配置..._那么需要把上面的defaultjavatyperesolver类打包到插件中

VS2015,Microsoft Visual Studio 2005,SourceInsight4.0使用经验,Visual AssistX番茄助手的安装与基本使用9_番茄助手颜色-程序员宅基地

文章浏览阅读909次。1.得下载一个番茄插件,按alt+g才可以有函数跳转功能。2.不安装番茄插件,按F12也可以有跳转功能。3.进公司的VS工程是D:\sync\build\win路径,.sln才是打开工程的方式,一个是VS2005打开的,一个是VS2013打开的。4.公司库里的线程接口,在CmThreadManager.h 里,这个里面是我们的线程库,可以直接拿来用。CreateUserTaskThre..._番茄助手颜色

推荐文章

热门文章

相关标签