1. 程式人生 > 實用技巧 >Luogu 1390 - 公約數的和 (尤拉函式/莫比烏斯反演、分塊)

Luogu 1390 - 公約數的和 (尤拉函式/莫比烏斯反演、分塊)

Luogu 1390 - 公約數的和

UVa 11426 - GCD - Extreme (II)


題意

給定數字\(n\),計算下式結果

\[\sum_{i=1}^n\sum_{j=i+1}^n\gcd(i,j) \]


限制

\(Case=1,\ n\leq2\times10^6,\ 1000ms\) (洛谷)

\(Case\leq100,\ n\leq4\times10^6,\ 10000ms\) (UVa)




思路一(尤拉函式)

劉汝佳藍書 P124-P125

\(f(n)=\gcd(1,n)+\gcd(2,n)+...+\gcd(n-1,n)=\sum_{i=1}^{n-1}\gcd(i,n)\)

則所求答案即為\(\sum_{i=1}^n\sum_{j=i+1}^n\gcd(i,j)=f(2)+f(3)+...+f(n)=\sum_{i=2}^nf(i)\)

洛谷僅單測試點,故最後直接\(O(n)\)累加所有\(f(i)\)輸出即可

UVa有多測試點,需要預處理字首和後再輸出


\(g(n,i)\)為滿足\(\gcd(n,j)=i,\ j<n\)的所有滿足條件的\(j\)的個數

\(g(n,i)=\sum_{j=1}^{n-1}[\gcd(n,j)=i]\)

又因為\(gcd(n,j)=i\iff gcd(n/i,j/i)=1\)

\(g(n,i)\)表示所有小於\(n/i\)的並與\(n/i\)互質的數的個數

根據尤拉函式可得,所有滿足\(gcd(n/i,j/i)=1\)\(j/i\)個數即為\(\varphi(n/i)\)

所以\(g(n,i)=\varphi(n/i)\)

,可以根據尤拉篩法預處理求出尤拉函式先

然後再考慮\(f(n)=\sum i\times g(n,i),\ i|n\)

要想得到\(f(n)\),首先就得找到所有小於\(n\)的因子(不包括\(n\)

所以可以根據埃氏篩法,從小到大列舉因子,將答案加給因子的倍數即可



程式碼一 - Luogu 1390

Case Max (291ms/1000ms)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2000000;

ll eular[N+50],prim[N+50];
ll f[N+50];
bool vis[N+50];

void init(int n)
{
	memset(vis,false,sizeof vis);
    int p=0;
    for(int i=2;i<=n;i++)
    {
        if(!vis[i])
        {
            prim[p++]=i;
            eular[i]=i-1;
        }
        for(int j=0;j<p;j++)
        {
            int k=i*prim[j];
            if(k>n)
                break;
            vis[k]=true;
            if(i%prim[j]==0)
            {
                eular[k]=eular[i]*prim[j];
                break;
            }
            eular[k]=eular[i]*eular[prim[j]];
        }
    }
}

int main()
{
    int n;
    scanf("%d",&n);
    init(n);
    for(int i=1;i<=n;i++)
    {
        for(int j=i*2;j<=n;j+=i)
            f[j]+=eular[j/i]*i;
    }
    ll ans=0;
    for(int i=2;i<=n;i++)
        ans+=f[i];
    printf("%lld\n",ans);
    
    return 0;
}


程式碼一 - UVa 11426

(630ms/10000ms)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=4000000;

ll eular[N+50],prim[N+50];
ll f[N+50],ans[N+50];
bool vis[N+50];

void init(int n)
{
	memset(vis,false,sizeof vis);
    int p=0;
    for(int i=2;i<=n;i++)
    {
        if(!vis[i])
        {
            prim[p++]=i;
            eular[i]=i-1;
        }
        for(int j=0;j<p;j++)
        {
            int k=i*prim[j];
            if(k>n)
                break;
            vis[k]=true;
            if(i%prim[j]==0)
            {
                eular[k]=eular[i]*prim[j];
                break;
            }
            eular[k]=eular[i]*eular[prim[j]];
        }
    }
}

int main()
{
    init(N);
    for(int i=1;i<=N;i++)
    {
        for(int j=i*2;j<=N;j+=i)
            f[j]+=eular[j/i]*i;
    }
    for(int i=2;i<=N;i++)
        ans[i]=ans[i-1]+f[i];
    
    int n;
    while(scanf("%d",&n)!=EOF&&n)
    {
        printf("%lld\n",ans[n]);
    }
    
    return 0;
}



思路二(莫比烏斯反演)(簡單)

類似於思路一,我們將所有不同的\(\gcd(i,j)=k\)分開來考慮,以“數量*乘積”的方式表示答案

則原式則會變成

\[\sum_{i=1}^n\sum_{j=i+1}^n\gcd(i,j)=\sum_{k=1}^nk\sum_{i=1}^n\sum_{j=i+1}^n[\gcd(i,j)=k] \]

這裡我們需要列舉\(k\)來求和所有\(\gcd(i,j)=k\)的情況

因為\(gcd(a,b)=c\iff gcd(\frac{a}{c},\frac{b}{c})=1\)

我們還能繼續化簡上式,得到

\[\sum_{k=1}^nk\sum_{i=1}^n\sum_{j=i+1}^n[\gcd(i,j)=k]=\sum_{k=1}^nk\sum_{i=1}^{\frac{n}{k}}\sum_{j=i+1}^{\frac{n}{k}}[\gcd(i,j)=1] \]

明顯發現,該式子後半部分即為莫比烏斯反演的模板題——求兩區間內互質數的對數

直接套板子即可



程式碼二 - Luogu 1390

Case Max(139ms/1000ms)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2000000;

ll mu[N+50],prim[N+50];
bool vis[N+50];

void init(int n)
{
	memset(vis,false,sizeof vis);
    mu[1]=1;
    int p=0;
    for(int i=2;i<=n;i++)
    {
        if(!vis[i])
        {
            prim[p++]=i;
            mu[i]=-1;
        }
        for(int j=0;j<p;j++)
        {
            int k=i*prim[j];
            if(k>n)
                break;
            vis[k]=true;
            if(i%prim[j]==0)
            {
                mu[k]=0;
                break;
            }
            else
                mu[i*prim[j]]=-mu[i];
        }
    }
}

int main()
{
    int n;
    scanf("%d",&n);
    init(n);
    ll ans=0;
    for(int d=1;d<=n;d++)
    {
        ll ansd=0;
        int m=n/d;
        for(int i=1;i<=m;i++)
            ansd+=mu[i]*(m/i)*(m/i);
        ans+=(ansd>>1)*d;
    }
    printf("%lld\n",ans);
    
    return 0;
}


程式碼二 - UVa 11426

(2310ms/10000ms)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=4000000;

ll mu[N+50],prim[N+50];
bool vis[N+50];

void init(int n)
{
	memset(vis,false,sizeof vis);
    mu[1]=1;
    int p=0;
    for(int i=2;i<=n;i++)
    {
        if(!vis[i])
        {
            prim[p++]=i;
            mu[i]=-1;
        }
        for(int j=0;j<p;j++)
        {
            int k=i*prim[j];
            if(k>n)
                break;
            vis[k]=true;
            if(i%prim[j]==0)
            {
                mu[k]=0;
                break;
            }
            else
                mu[i*prim[j]]=-mu[i];
        }
    }
}

int main()
{
    init(N);
    int n;
    while(scanf("%d",&n)&&n)
    {
        ll ans=0;
        for(int d=1;d<=n;d++)
        {
            ll ansd=0;
            int m=n/d;
            for(int i=1;i<=m;i++)
                ansd+=mu[i]*(m/i)*(m/i);
            ans+=(ansd>>1)*d;
        }
        printf("%lld\n",ans);
    }
    
    return 0;
}



思路三(莫比烏斯反演、分塊)(進階)

在思路二中,我們將式子化簡到了直接套板子的情況

但是會發現,我們每次列舉\(d\)時,對於\(\gcd=d\)的種類數就必須計算\(\frac n d\)次才能出結果

總時間複雜度嚴格為\(O(nlogn)\)

有沒有辦法再繼續降低複雜度呢?當然,我們可以引入分塊的思想

可以發現,雖然列舉的\(d\)在數值小的時候\(\frac n d\)變化很大,以至於我們每次都需要重新計算一次

但是如果\(d\)逐漸變大,由於整除的關係,會有一段連續的\(d\)使得\(\frac n d\)相同

那我們就可以根據這個性質,使得這一整段只計算一次就得出結果

假設當前\(d\)列舉到某個分塊的左邊界,那麼\(d_r=\lfloor \frac n {\lfloor \frac n d \rfloor} \rfloor\)即為這一分塊的右邊界,即滿足\(\frac n d=\frac n {d_r}\)的最大的\(d_r\)



程式碼三 - Luogu 1390

Case Max(79ms/1000ms)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2000000;

ll mu[N+50],prim[N+50];
bool vis[N+50];

void init(int n)
{
	memset(vis,false,sizeof vis);
    mu[1]=1;
    int p=0;
    for(int i=2;i<=n;i++)
    {
        if(!vis[i])
        {
            prim[p++]=i;
            mu[i]=-1;
        }
        for(int j=0;j<p;j++)
        {
            int k=i*prim[j];
            if(k>n)
                break;
            vis[k]=true;
            if(i%prim[j]==0)
            {
                mu[k]=0;
                break;
            }
            else
                mu[i*prim[j]]=-mu[i];
        }
    }
}

int main()
{
    int n;
    scanf("%d",&n);
    init(N);
    ll ans=0;
    for(int d=1;d<=n;)
    {
        int m=n/d;
        ll ansd=0;
        for(int i=1;i<=m;i++)
            ansd+=mu[i]*(m/i)*(m/i);
        ansd>>=1;
        int nxt=n/(n/d); //計算這一塊的右邊界
        ans+=ansd*d;
        for(int i=d+1;i<=nxt;i++)
            ans+=ansd*i; //需要遍歷乘上權值
        d=nxt+1; //d跳到下一分塊的左邊界
    }
    printf("%lld\n",ans);
    
    return 0;
}


程式碼三 - UVa 11426

(1650ms/10000ms)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=4000000;

ll mu[N+50],prim[N+50];
bool vis[N+50];

void init(int n)
{
	memset(vis,false,sizeof vis);
    mu[1]=1;
    int p=0;
    for(int i=2;i<=n;i++)
    {
        if(!vis[i])
        {
            prim[p++]=i;
            mu[i]=-1;
        }
        for(int j=0;j<p;j++)
        {
            int k=i*prim[j];
            if(k>n)
                break;
            vis[k]=true;
            if(i%prim[j]==0)
            {
                mu[k]=0;
                break;
            }
            else
                mu[i*prim[j]]=-mu[i];
        }
    }
}

int main()
{
    init(N);
    int n;
    while(scanf("%d",&n)&&n)
    {
        ll ans=0;
        for(int d=1;d<=n;)
        {
            int m=n/d;
            ll ansd=0;
            for(int i=1;i<=m;i++)
                ansd+=mu[i]*(m/i)*(m/i);
            ansd>>=1;
            int nxt=n/(n/d); //計算這一塊的右邊界
            ans+=ansd*d;
            for(int i=d+1;i<=nxt;i++)
                ans+=ansd*i; //需要遍歷乘上權值
            d=nxt+1; //d跳到下一分塊的左邊界
        }
        printf("%lld\n",ans);
    }
    
    return 0;
}



思路四(莫比烏斯反演、分塊)(再進階)

啊好……還沒學呢

最後的公式還沒看懂(公式還能再化簡)