1. 程式人生 > >數學(論)裡的一些定理(莫比烏斯反演,傅立葉變換,數論變換...)

數學(論)裡的一些定理(莫比烏斯反演,傅立葉變換,數論變換...)

莫比烏斯反演在數論中佔有重要的地位,許多情況下能大大簡化運算。那麼我們先來認識莫比烏斯反演公式。

定理:是定義在非負整數集合上的兩個函式,並且滿足條件,那麼我們得到結論

     

在上面的公式中有一個函式,它的定義如下:

    (1)若,那麼

    (2)若均為互異素數,那麼

    (3)其它情況下

對於函式,它有如下的常見性質:

    (1)對任意正整數

                            

        (2)對任意正整數

         

線性篩選求莫比烏斯反演函式程式碼。

void Init()
{
    memset(vis,0,sizeof(vis));
    mu[1] = 1;
    cnt = 0;
    for(int i=2; i<N; i++)
    {
        if(!vis[i])
        {
            prime[cnt++] = i;
            mu[i] = -1;
        }
        for(int j=0; j<cnt&&i*prime[j]<N; j++)
        {
            vis[i*prime[j]] = 1;
            if(i%prime[j]) mu[i*prime[j]] = -mu[i];
            else
            {
                mu[i*prime[j]] = 0;
                break;
            }
        }
    }
}

有了上面的知識,現在我們來證明莫比烏斯反演定理。

證明

證明完畢!

嗯,有了莫比烏斯反演,很多問題都可以簡化了,接下來我們來看看莫比烏斯反演在數論中如何簡化運算的。

題意:給一個正整數,其中,求使得為質數的的個數,

分析:對於本題,因為是使得為質數,所以必然要列舉小於等於的質數,那麼對於每一個質數,只

     需要求在區間中,滿足有序對互質的對數。

     也就是說,現在問題轉化為:在區間中,存在多少個有序對使得互質,這個問題就簡單啦,因為

     是有序對,不妨設,那麼我們如果列舉每一個,小於有多少個互素,這正是尤拉函式。所以

     我們可以遞推法求尤拉函式,將得到的答案乘以2即可,但是這裡乘以2後還有漏計算了的,那麼有哪些呢?

     是且為素數的情況,再加上就行了。

程式碼:

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <bitset>

using namespace std;
typedef long long LL;
const int N = 10000010;

bitset<N> prime;
LL phi[N];
LL f[N];
int p[N];
int k;

void isprime()
{
    k = 0;
    prime.set();
    for(int i=2; i<N; i++)
    {
        if(prime[i])
        {
            p[k++] = i;
            for(int j=i+i; j<N; j+=i)
                prime[j] = false;
        }
    }
}

void Init()
{
    for(int i=1; i<N; i++)  phi[i] = i;
    for(int i=2; i<N; i+=2) phi[i] >>= 1;
    for(int i=3; i<N; i+=2)
    {
        if(phi[i] == i)
        {
            for(int j=i; j<N; j+=i)
                phi[j] = phi[j] - phi[j] / i;
        }
    }
    f[1] = 0;
    for(int i=2;i<N;i++)
        f[i] = f[i-1] + (phi[i]<<1);
}

LL Solve(int n)
{
    LL ans = 0;
    for(int i=0; i<k&&p[i]<=n; i++)
        ans += 1 + f[n/p[i]];
    return ans;
}

int main()
{
    Init();
    isprime();
    int n;
    scanf("%d",&n);
    printf("%I64d\n",Solve(n));
    return 0;
}

上題不算太難,普通的尤拉函式就可以搞定,接下來我們來看看它的升級版。

題意:給定兩個數,其中,求為質數的有多少對?其中的範

     圍是

分析:本題與上題不同的是不一定相同。在這裡我們用莫比烏斯反演來解決,文章開頭也說了它能大大簡化

     運算。我們知道莫比烏斯反演的一般描述為:

     

     其實它還有另一種描述,本題也是用到這種。那就是:

     

     好了,到了這裡,我們開始進入正題。。。

     對於本題,我們設

     為滿足的對數

     為滿足的對數

     那麼,很顯然,反演後得到

     因為題目要求是為質數,那麼我們列舉每一個質數,然後得到

     

     如果直接這樣做肯定TLE,那麼我們必須優化。

     我們設,那麼繼續得到

     到了這裡,可以看出如果我們可以先預處理出所有的對應的的值,那麼本題就解決了。

     我們設,注意這裡為素數,

     那麼,我們列舉每一個,得到,現在分情況討論:

     (1)如果整除,那麼得到

       

     (2)如果不整除,那麼得到

       

程式碼:

#include <iostream>
#include <string.h>
#include <stdio.h>

using namespace std;
typedef long long LL;
const int N = 10000005;

bool vis[N];
int p[N];
int cnt;
int g[N],u[N],sum[N];

void Init()
{
    memset(vis,0,sizeof(vis));
    u[1] = 1;
    cnt = 0;
    for(int i=2;i<N;i++)
    {
        if(!vis[i])
        {
            p[cnt++] = i;
            u[i] = -1;
            g[i] = 1;
        }
        for(int j=0;j<cnt&&i*p[j]<N;j++)
        {
            vis[i*p[j]] = 1;
            if(i%p[j])
            {
                u[i*p[j]] = -u[i];
                g[i*p[j]] = u[i] - g[i];
            }
            else
            {
                u[i*p[j]] = 0;
                g[i*p[j]] = u[i];
                break;
            }
        }
    }
    sum[0] = 0;
    for(int i=1;i<N;i++)
        sum[i] = sum[i-1] + g[i];
}

int main()
{
    Init();
    int T;
    scanf("%d",&T);
    while(T--)
    {
        LL n,m;
        cin>>n>>m;
        if(n > m) swap(n,m);
        LL ans = 0;
        for(int i=1,last;i<=n;i=last+1)
        {
            last = min(n/(n/i),m/(m/i));
            ans += (n/i)*(m/i)*(sum[last]-sum[i-1]);
        }
        cout<<ans<<endl;
    }
    return 0;
}

多項式乘法運算初級版——快速傅立葉變換

快速傅立葉變換在資訊學競賽中主要用於求卷積,或者說多項式乘法。我們知道,多項式乘法的普通演算法時間複雜度

,通過快速傅立葉變換可以使時間降為,那麼接下來會詳細介紹快速傅立葉變換的原理。

首先來介紹多項式的兩種表示方法,即係數表示法點值表示法。從某種意義上說,這兩種方法是等價的。先設

    

(1)係數表示法

    對於一個次數界為的多項式來說,其係數表示法就是一個由係數組成的向量,很

    明顯,這樣的多項式乘法運算的時間複雜度為

(2)點值表示法

    對於一個次數界為的多項式來說,其點值是個點值對所形成的集合

    

    其中各不相同,並且當時,有。可以看出一個多項式可以有多種不同的點值

    表示法,而通過這個不同的點值對可以表示一個唯一的多項式。而通過點值表示法來計算多項式的乘法,時間

    複雜度為

    從原則上來說,計算多項式的點值是簡單易行的,因為我們只需要先選取個相異的點,然後通過秦九韶演算法

    以在時間內求出所有的,實際上如果我們的選得巧妙的話,就可以加速這一過程,使其執行時間變

    為

    根據多項式的係數表示法求其點值表示法的過程稱為求值,而根據點值表示法求其係數表示法的過程稱為插值

    對於求卷積或者說多項式乘法運算問題,先是通過傅立葉變換對係數表示法的多項式進行求值運算,這一步的時

    間複雜度為,然後在時間內進行點值相乘,再進行插值運算。

那麼,接下來就是我們今天的重點了,如何高效地對一個多項式進行求值運算,即將多項式的表示法變為點值表示法。

如果選取單位復根作為求值點,則可以通過對係數向量進行離散傅立葉變換(DFT),得到相應的點值表示。同樣地

也可以通過對點值對進行逆DFT運算,獲得相應的係數向量。DFT逆DFT的時間複雜度均為

一. 求DFT

    選取次單位復根作為來求點值是比較巧妙的做法。

    次單位復根是滿足的複數次單位復根恰好有個,它們是,為

    瞭解釋這一式子,利用複數冪的定義,值稱為主次單位根,所有其

    它次單位復根都是次冪。

    次單位復根在乘法運算下形成一個群,該群的結構與加法群相同。

    接下來認識幾個關於次單位復根的重要性質。

    (1)相消引理

        對於任何整數,有

    (2)折半引理

        如果且為偶數,則

    (3)求和引理

        對任意整數和不能被整除的非零整數,有

          

     回顧一下,我們希望計算次數界為的多項式

     

     在處的值,假定2的冪,因為給定的次數界總可以增大,如果需要,總可以新增值為零

     的新的高階係數。假定已知的係數形式為,對,定義結果

     如下

                 

     向量是係數向量的離散傅立葉變換,寫作

     通過使用一種稱為快速傅立葉變換(FFT)的方法,就可以在時間內計算出,而直接

     計算的方法所需時間為FFT主要是利用單位復根的特殊性質。FFT方法運用了分治策略,它用

     中偶數下標的係數與奇數下標的係數,分別定義了兩個新的次數界為的多項式

     

     則進一步有

     這樣處的值得問題就轉換為求次數界為的多項式在點

     處的值。由於在奇偶分類時導致順序發生變化,所以需要先通過Rader演算法進行

     倒位序,在FFT中最重要的一個操作是蝴蝶操作,通過蝴蝶操作可以將前半部分和後半部分的值求出。

題意:大數乘法,需要用FFT實現。

程式碼:

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <math.h>

using namespace std;
const int N = 500005;
const double PI = acos(-1.0);

struct Virt
{
    double r, i;

    Virt(double r = 0.0,double i = 0.0)
    {
        this->r = r;
        this->i = i;
    }

    Virt operator + (const Virt &x)
    {
        return Virt(r + x.r, i + x.i);
    }

    Virt operator - (const Virt &x)
    {
        return Virt(r - x.r, i - x.i);
    }

    Virt operator * (const Virt &x)
    {
        return Virt(r * x.r - i * x.i, i * x.r + r * x.i);
    }
};

//雷德演算法--倒位序
void Rader(Virt F[], int len)
{
    int j = len >> 1;
    for(int i=1; i<len-1; i++)
    {
        if(i < j) swap(F[i], F[j]);
        int k = len >> 1;
        while(j >= k)
        {
            j -= k;
            k >>= 1;
        }
        if(j < k) j += k;
    }
}

//FFT實現
void FFT(Virt F[], int len, int on)
{
    Rader(F, len);
    for(int h=2; h<=len; h<<=1) //分治後計算長度為h的DFT
    {
        Virt wn(cos(-on*2*PI/h), sin(-on*2*PI/h));  //單位復根e^(2*PI/m)用尤拉公式展開
        for(int j=0; j<len; j+=h)
        {
            Virt w(1,0);            //旋轉因子
            for(int k=j; k<j+h/2; k++)
            {
                Virt u = F[k];
                Virt t = w * F[k + h / 2];
                F[k] = u + t;     //蝴蝶合併操作
                F[k + h / 2] = u - t;
                w = w * wn;      //更新旋轉因子
            }
        }
    }
    if(on == -1)
        for(int i=0; i<len; i++)
            F[i].r /= len;
}

//求卷積
void Conv(Virt a[],Virt b[],int len)
{
    FFT(a,len,1);
    FFT(b,len,1);
    for(int i=0; i<len; i++)
        a[i] = a[i]*b[i];
    FFT(a,len,-1);
}

char str1[N],str2[N];
Virt va[N],vb[N];
int result[N];
int len;

void Init(char str1[],char str2[])
{
    int len1 = strlen(str1);
    int len2 = strlen(str2);
    len = 1;
    while(len < 2*len1 || len < 2*len2) len <<= 1;

    int i;
    for(i=0; i<len1; i++)
    {
        va[i].r = str1[len1-i-1] - '0';
        va[i].i = 0.0;
    }
    while(i < len)
    {
        va[i].r = va[i].i = 0.0;
        i++;
    }
    for(i=0; i<len2; i++)
    {
        vb[i].r = str2[len2-i-1] - '0';
        vb[i].i = 0.0;
    }
    while(i < len)
    {
        vb[i].r = vb[i].i = 0.0;
        i++;
    }
}

void Work()
{
    Conv(va,vb,len);
    for(int i=0; i<len; i++)
        result[i] = va[i].r+0.5;
}

void Export()
{
    for(int i=0; i<len; i++)
    {
        result[i+1] += result[i]/10;
        result[i] %= 10;
    }
    int high = 0;
    for(int i=len-1; i>=0; i--)
    {
        if(result[i])
        {
            high = i;
            break;
        }
    }
    for(int i=high; i>=0; i--)
        printf("%d",result[i]);
    puts("");
}

int main()
{
    while(~scanf("%s%s",str1,str2))
    {
        Init(str1,str2);
        Work();
        Export();
    }
    return 0;
}

題意:給定n條長度已知的邊,求能組成多少個三角形。

分析:用一個num陣列來記錄次數,比如num[i]表示長度為i的邊有num[i]條。然後對num[]求卷積,除去本身重

     復的和對稱的,然後再整理一下就好了。

程式碼:

#include <iostream>
#include <string.h>
#include <algorithm>
#include <stdio.h>
#include <math.h>

using namespace std;
typedef long long LL;

const int N = 400005;
const double PI = acos(-1.0);

struct Virt
{
    double r,i;

    Virt(double r = 0.0,double i = 0.0)
    {
        this->r = r;
        this->i = i;
    }

    Virt operator + (const Virt &x)
    {
        return Virt(r+x.r,i+x.i);
    }

    Virt operator - (const Virt &x)
    {
        return Virt(r-x.r,i-x.i);
    }

    Virt operator * (const Virt &x)
    {
        return Virt(r*x.r-i*x.i,i*x.r+r*x.i);
    }
};

//雷德演算法--倒位序
void Rader(Virt F[],int len)
{
    int j = len >> 1;
    for(int i=1; i<len-1; i++)
    {
        if(i < j) swap(F[i], F[j]);
        int k = len >> 1;
        while(j >= k)
        {
            j -= k;
            k >>= 1;
        }
        if(j < k) j += k;
    }
}

//FFT實現
void FFT(Virt F[],int len,int on)
{
    Rader(F,len);
    for(int h=2; h<=len; h<<=1) //分治後計算長度為h的DFT
    {
        Virt wn(cos(-on*2*PI/h),sin(-on*2*PI/h));  //單位復根e^(2*PI/m)用尤拉公式展開
        for(int j=0; j<len; j+=h)
        {
            Virt w(1,0);            //旋轉因子
            for(int k=j; k<j+h/2; k++)
            {
                Virt u = F[k];
                Virt t = w*F[k+h/2];
                F[k] = u+t;      //蝴蝶合併操作
                F[k+h/2] = u-t;
                w = w*wn;      //更新旋轉因子
            }
        }
    }
    if(on == -1)
        for(int i=0; i<len; i++)
            F[i].r /= len;
}

//求卷積
void Conv(Virt F[],int len)
{
    FFT(F,len,1);
    for(int i=0; i<len; i++)
        F[i] = F[i]*F[i];
    FFT(F,len,-1);
}

int a[N];
Virt F[N];
LL num[N],sum[N];
int len,n;

void Init()
{
    memset(num,0,sizeof(num));
    scanf("%d",&n);
    for(int i=0; i<n; i++)
    {
        scanf("%d",&a[i]);
        num[a[i]]++;
    }
    sort(a, a + n);
    int len1 = a[n-1] + 1;
    len  = 1;
    while(len < len1*2) len <<= 1;
    for(int i=0; i<len1; i++)
        F[i] = Virt(num[i],0);
    for(int i=len1; i<len; i++)
        F[i] = Virt(0,0);
}

void Work()
{
    Conv(F,len);
    for(int i=0; i<len; i++)
        num[i] = (LL)(F[i].r+0.5);
    len = a[n-1]*2;
    for(int i=0; i<n; i++)
        num[a[i]+a[i]]--;
    for(int i=1; i<=len; i++)
        num[i] >>= 1;
    sum[0] = 0;
    for(int i=1; i<=len; i++)
        sum[i] = sum[i-1] + num[i];
    LL cnt = 0;
    for(int i=0; i<n; i++)
    {
        cnt+=sum[len]-sum[a[i]];
        //減掉一個取大,一個取小的
        cnt-=(LL)(n-1-i)*i;
        //減掉一個取本身,另外一個取其它
        cnt-=(n-1);
        //減掉大於它的取兩個的組合
        cnt-=(LL)(n-1-i)*(n-i-2)/2;
    }
    LL tot = (LL)n*(n-1)*(n-2)/6;
    printf("%.7lf\n",(double)cnt/tot);
}

int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        Init();
        Work();
    }
    return 0;
}

多項式乘法運算終極版——NTT(快速數論變換)

換來求多項式的乘法。可以發現它是利用了單位復根的特殊性質,大大減少了運算,但是這種做法是對複數係數的矩陣

加以處理,每個複數係數的實部和虛部是一個正弦及餘弦函式,因此大部分系數都是浮點數,我們必須做複數及浮點數

的計算,計算量會比較大,而且浮點數的計算可能會導致誤差增大。

今天,我將來介紹另一種計算多項式乘法的演算法,叫做快速數論變換(NTT),在離散正交變換的理論中,已經證明在

複數域內,具有迴圈卷積特性的唯一變換是DFT,所以在複數域中不存在具有迴圈卷積性質的更簡單的離散正交變換。

因此提出了以數論為基礎的具有迴圈卷積性質的快速數論變換

回憶複數向量,其離散傅立葉變換公式如下

   

離散傅立葉逆變換公式為

   

今天的快速數論變換(NTT)是在上進行的,在快速傅立葉變換(FFT)中,通過次單位復根來運算的,即滿

,而對於快速數論變換來說,則是可以將看成是的等價,這裡是模素數

的原根(由於是素數,那麼原根一定存在)。即

        

所以綜上,我們得到數論變換的公式如下

    

數論變換的逆變換公式為

    

這樣就把複數對應到一個整數,之後一切都是在系統內考慮。

上述數論變換(NTT)公式中,要求是素數且必須是的因子。由於經常是2的方冪,所以可以構造形

的素數。通常來說可以選擇費馬素數,這樣的變換叫做費馬數數論變換

這裡我們選擇,這樣得到模的原根值為

分析:題目意思就是大數相乘,此處用快速數論變換(NTT)實現。

程式碼:

#include <iostream>
#include <string.h>
#include <stdio.h>

using namespace std;
typedef long long LL;

const int N = 1 << 18;
const int P = (479 << 21) + 1;
const int G = 3;
const int NUM = 20;

LL  wn[NUM];
LL  a[N], b[N];
char A[N], B[N];

LL quick_mod(LL a, LL b, LL m)
{
    LL ans = 1;
    a %= m;
    while(b)
    {
        if(b & 1)
        {
            ans = ans * a % m;
            b--;
        }
        b >>= 1;
        a = a * a % m;
    }
    return ans;
}

void GetWn()
{
    for(int i=0; i<NUM; i++)
    {
        int t = 1 << i;
        wn[i] = quick_mod(G, (P - 1) / t, P);
    }
}

void Prepare(char A[], char B[], LL a[], LL b[], int &len)
{
    len = 1;
    int len_A = strlen(A);
    int len_B = strlen(B);
    while(len <= 2 * len_A || len <= 2 * len_B) len <<= 1;
    for(int i=0; i<len_A; i++)
        A[len - 1 - i] = A[len_A - 1 - i];
    for(int i=0; i<len - len_A; i++)
        A[i] = '0';
    for(int i=0; i<len_B; i++)
        B[len - 1 - i] = B[len_B - 1 - i];
    for(int i=0; i<len - len_B; i++)
        B[i] = '0';
    for(int i=0; i<len; i++)
        a[len - 1 - i] = A[i] - '0';
    for(int i=0; i<len; i++)
        b[len - 1 - i] = B[i] - '0';
}

void Rader(LL a[], int len)
{
    int j = len >> 1;
    for(int i=1; i<len-1; i++)
    {
        if(i < j) swap(a[i], a[j]);
        int k = len >> 1;
        while(j >= k)
        {
            j -= k;
            k >>= 1;
        }
        if(j < k) j += k;
    }
}

void NTT(LL a[], int len, int on)
{
    Rader(a, len);
    int id = 0;
    for(int h = 2; h <= len; h <<= 1)
    {
        id++;
        for(int j = 0; j < len;