多項式乘法(FFT)學習筆記


------------------------------------------
本文只探討多項式乘法(FFT)在信息學中的應用
如有錯誤或不明歡迎指出或提問,在此不勝感激

多項式

1.系數表示法
     一般應用最廣泛的表示方式
     用A(x)表示一個x-1次多項式,a[i]為$ x^i$的系數,則A(x)=$ \sum_0^{n-1}$ a[i] * $ x^i$

     僅利用這種方式求多項式乘法復雜度為O($ n^2$),不夠優秀
2.點值表示法
     將n個互不相同的值$ x_0$...$ x_{n-1}$帶入多項式,可以得到
     對於一個n-1次多項式,可以被n個點所唯一對應.
     因而對於A(x)*B(x),只需要得知A的2n個點值和對應B的點值即可O(n)求出多項式的乘積
     然而得知這些點值的復雜度依然在平方級別,達不到要求


考慮優化
先引一些要用到的名詞

復數
復數即為表示成a+bi的數,其中i為-1的平方根
表示:
     可以通過平面直角坐標系上的一條向量(0,0)到(x,y)表示x+yi
     其中x軸為實數軸,y軸為虛數軸
運算:
     復數運算符合四則運算,即:
     (a+bi) + (c+di) = (a+c) + (b+d)i;
     (a+bi) * (c+di) = ac + adi + bci + bd$ i^2$ =(ac - bd) + (bc + ad)i
幾何意義:
     定義模長為向量長度,幅角為從x軸正半軸逆時針轉動到向量的角
     復數相加等同於向量加法
     復數相乘,模長相乘,幅角相加
//建議complex類手寫,速度優於STL

單位根
以下默認n為$ 2^x$ 且x為非負整數
在復數平面,以原點為圓心,以1為半徑作圓
以x軸正半軸到其與原交點(0,0)到(1,0)的這條向量為起點n等分圓,圓心到每個n等分點的向量均稱為n次單位根
對於一個單位根,其標號為幅角/(360°/n),特別的,(0,0)到(1,0)的向量標號為0
以下用$ w_n^k$ 表示標號為k的n次單位根

單位根的性質
     1. $ w_n^k$ = cos($ \frac{2π}{n}$)k + sin($ \frac{2π}{n}$)ki
     證明:歐拉公式


     2. $ w_n^k$ = $ w_{2n}^{2k}$
     證明:帶入式1,等價於分子分母同乘2,


     3. $ w_n^k$ * $ w_n^k$=$ w_n^{2k}$
     證明:根據(復數相乘,模長相乘,幅角相加)
     又因為模長均為1,所以相當於只把轉動角度乘2


     4. $ w_n^{k+\frac{n}{2}}$ = -$ w_n^k$
     證明:$ w_n^{k+\frac{n}{2}}$相當於在$ w_n^k$的基礎上逆時針方向再旋轉180° (復數相乘,模長相乘,幅角相加)
     因而等價於取負

進入正題
遞歸完成快速傅里葉變換


設多項式A(x)的系數為$ a_0$...$ a_{n-1}$

        A(x)=$ a_0$ + $ a_1$*x + $ a_2$*$ x^2$ + $ a_3$*$ x^3$ + $ a_4$*$ x^4$ + ... + $ a_{n-2}$*$ x^{n-2}$ + $ a_{n-1}$*$ x^{n-1}$

按下標奇偶性分成兩組,在這里設
        A0(x) = $ a_0$ + $ a_2$ * $ x$ + $ a_4$ * $ x^2$ + ... + $ a_{n-2}$ * $ x^\frac{n-2}{2}$
        A1(x) = $ a_1$ + $ a_3$ * $ x$ + $ a_5$ * $ x^2$ + ... + $ a_{n-1}$ * $ x^\frac{n-2}{2}$

顯然得到A(x) = A0($ x^2$) + xA1($ x^2$)
我們代單位根$ w_n^k$(0<=k<$ \frac{n}{2}$)入式得

A($ w_n^k$)=A0($ w_n^{2k}$)+$ w_n^k$A1($ w_n^{2k}$)//性質3

同理代$ w_n^{k+\frac{n}{2}}$入式得

A($ w_n^{k+\frac{n}{2}}$)=A0($ w_n^{2k+n}$)+$ w_n^{k+\frac{n}{2}}$A1($ w_n^{2k+n}$)
 = A0($ w_n^{2k}$) - $ w_n^k$ * A1($ w_n^{2k}$)//性質4&&$ w_n^n$=1

容易發現上下兩式只有常數項的符號不同
因而只需求前一半即可得到后一半
遞歸形式程序結構:
對於長度為n的A(x)
分割成長度為$ \frac{n}{2}$的A0(x)和A1(x)
求A0(x)和A1(x)
通過A0(x)和A1(x)計算帶入$ w_n^k$(0<=k<$ \frac{n}{2}$)時的值
變號計算代$ w_n^{k+\frac{n}{2}}$入式的值


我們可以用數組A[i]表示某一多項式(不一定是初始多項式)代入$ w_n^i$}時的值
由於許多奧妙重重的性質,我們不需要對於每個多項式都維護整個數組A,只需要維護它需要返回的值即可
繪圖可得,當某一多項式遞歸到只有一項的時候,要返回的一定只是$ w_n^0$(1),即直接返回原多項式該項系數即可
遞歸代碼:

void FFT(const int lim,cp *A){//cp即為complex類型,lim為2^n的整型if(lim==1)return;//直接返回對應常數項
    cp A0[lim>>1],A1[lim>>1]; for(rt i=0;i<lim;i+=2)A0[i>>1]=A[i],A1[i>>1]=A[i+1]; FFT(lim>>1,A0,fla);FFT(lim>>1,A1,fla);//遞歸求解
    cp w={cos(PI*2.0/lim),sin(PI*2.0/lim)},k={1,0};//w為1號單位根
    for(rt i=0;i<lim>>1;i++,k=k*w){//k即為第i個單位根
 A[i]=A0[i]+k*A1[i];//A0,A1數組中的i號本相當於A數組中的2i號,乘2再除2后相當於沒有
        A[i+(lim>>1)]=A0[i]-k*A1[i]; } }

 

逆變換
以上求得的均為點值表示法的結果
需要將其轉回系數表示法
實際操作相當於再進行FFT時單位根逆向(順時針)計算
遞歸全代碼

 

#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define rt register int
#define ll long long
using namespace std;
ll read(){
    ll x = 0; int zf = 1; char ch;
    while (ch != '-' && (ch < '0' || ch > '9')) ch = getchar();
    if (ch == '-') zf = -1, ch = getchar();
    while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar(); return x * zf;
}
void write(ll y){
    if (y < 0) putchar('-'), y = -y;
    if (y > 9) write(y / 10);
    putchar(y % 10 + '0');
}
int i,j,k,m,n,x,y,z,cnt,all,num;
struct cp{
    double x,y;
}a[2200010],b[2200010];
inline cp operator +(const cp x,const cp y){return {x.x + y.x, x.y + y.y};}
inline cp operator *(const cp x,const cp y){return {x.x * y.x - x.y * y.y, x.x * y.y + x.y * y.x};}
inline cp operator -(const cp x,const cp y){return {x.x - y.x, x.y - y.y};}
const double PI=acos(-1.0);
void FFT(const int lim,cp *A,const int fla){//fla為-1表示逆變換
    if(lim==1)return;cp A1[lim>>1],A2[lim>>1];
    for(rt i=0;i<lim;i+=2)A1[i>>1]=A[i],A2[i>>1]=A[i+1];
    FFT(lim>>1,A1,fla);FFT(lim>>1,A2,fla);
    cp w={cos(PI*2.0/lim),sin(PI*2.0/lim)*fla},k={1,0};
    for(rt i=0;i<lim>>1;i++,k=k*w){
        A[i]=A1[i]+k*A2[i];
        A[i+(lim>>1)]=A1[i]-k*A2[i];
    }
}
int main(){
    n=read();m=read();
    for(rt i=0;i<=n;i++)a[i].x=read(),a[i].y=0;
    for(rt i=0;i<=m;i++)b[i].x=read(),b[i].y=0;
    int limit=1;while(limit<=n+m)limit<<=1;//將多項式長度湊到2^n
    FFT(limit,a,1);FFT(limit,b,1);
    for(rt i=0;i<=limit;i++)a[i]=a[i]*b[i];//a為點值表示的多項式
    FFT(limit,a,-1);//逆變換
    for(rt i=0;i<=n+m;i++)write(a[i].x/limit+0.5),putchar(' ');
    //因為有limit個單位根因而答案需要除limit,0.5是四舍五入
    return 0;
}

 

 

 

 


問題:常數巨大
解決方案:改成非遞歸(迭代)形式
觀察下圖


發現底層序列的值相當於原序列的值的二進制反轉
因而我們可以預處理底層的值然后迭代向上推

如何預處理
1.x為偶數
     x=(x>>1)<<1;
     在反轉序列中reverse[x]=reverse[x>>1]>>1;
2.x為奇數
     相當於x-1的結果再在最高位補1

     這樣我們得到底層結果之后,一層一層向上迭代合並即可
迭代代碼:

 

#include<ctime>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define rt register int
#define ll long long
using namespace std;
inline ll read(){
    ll x = 0; char zf = 1; char ch = getchar();
    while (ch != '-' && !isdigit(ch)) ch = getchar();
    if (ch == '-') zf = -1, ch = getchar();
    while (isdigit(ch)) x = x * 10 + ch - '0', ch = getchar(); return x * zf;
}
void write(ll y){if(y<0)putchar('-'),y=-y;if(y>9)write(y/10);putchar(y%10+48);}
void writeln(const ll y){write(y);putchar('\n');}
int i,j,k,m,n,x,y,z,cnt,lim;
struct cp{
    double x,y;
    cp operator +(const cp s)const{
        return (cp){x+s.x,y+s.y};
    }
    cp operator -(const cp s)const{
        return (cp){x-s.x,y-s.y};
    }
    cp operator *(const cp s)const{
        return (cp){x*s.x-y*s.y,x*s.y+y*s.x};
    }
}a[2100010],b[2100010];
int R[2100010];
const double PI=acos(-1.0);
void FFT(cp *A,int fla){
    for(rt i=0;i<lim;i++)if(i>R[i])swap(A[i],A[R[i]]);
    for(rt i=1;i<lim;i<<=1){
        cp w={cos(PI/i),fla*sin(PI/i)};
        for(rt j=0;j<lim;j+=i<<1){
            cp K={1,0};
            for(rt k=0;k<i;k++,K=K*w){
                cp x=A[j+k],y=K*A[i+j+k];
                A[j+k]=x+y,A[i+j+k]=x-y;
            }
        }
    }
}
int main(){
    n=read();m=read();lim=1;
    for(rt i=0;i<=n;i++)a[i].x=read();
    for(rt i=0;i<=m;i++)b[i].x=read();
    while(lim<=n+m)lim<<=1;
    for(rt i=1;i<lim;i++)R[i]=(R[i>>1]>>1)|((i&1)*lim>>1);
    FFT(a,1);FFT(b,1);
    for(rt i=0;i<lim;i++)a[i]=a[i]*b[i];
    FFT(a,-1);
    for(rt i=0;i<=n+m;i++)write(a[i].x/lim+0.5),putchar(' ');
    return 0;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM