時間域上的函數f(t)通過傅里葉變換(Fourier Transform)變成頻率域上的F(w),也就是用一些不一樣頻率正弦曲線的加 權疊加獲得時間域上的信號。html
\[ F(\omega)=\mathcal{F}[f(t)]=\int\limits_{-\infty}^\infty f(t)e^{-iwt}dt \]c++
傅里葉逆變換是將頻率域上的F(w)變成時間域上的函數f(t),通常稱\(f(t)\)爲原函數,稱\(F(w)\)爲象函數。原函數和象函數構成一個傅里葉變換對。web
\[ f(t)=\mathcal{F^{-1}}[F(w)]=\frac 1 {2\pi}\int\limits_{-\infty}^\infty F(w)e^{iwt}dw \]算法
離散傅里葉變換(DFT)是傅里葉變換在時域和頻域都呈現離散的形式。數組
\[ x_n=\sum_{k=0}^{N-1}X_ke^{\frac{2\pi i}{N} kn},n=0,...,N-1 \]函數
固然也就有離散傅里葉逆變換(IDFT):ui
\[ x_n=\frac 1 N\sum_{k=0}^{N-1}X_ke^{-\frac{2\pi i}{N} kn},n=0,...,N-1 \]spa
快速傅里葉變換(FFT)利用分治的思想,將 DFT 分解成更小的 DFT進行計算,能夠在\(O(n\log n)\)時間複雜度內完成 DFT 的算法。可用來加速卷積和多項式乘法。code
對於\(C(x)=A(x)B(x)=\sum_{i=0}^{n-1}\sum_{j=0}^{i}a_jb_{i-j}x^i\),(A的最高項次數加上B的最高項次數爲n-1)直接計算是\(O(n^2)\)的。咱們將它們變成點值表達式orm
\[ A(x):{(x_0,y_0),(x_1,y_1),...,(x_{n-1},y_{n-1})}\\\\ B(x):{(x_0,y_0'),(x_1,y_1'),...,(x_{n-1},y_{n-1}')}\\\\ C(x):{(x_0,y_0y_0'),(x_1,y_1y_1'),...,(x_{n-1},y_{n-1}y_{n-1}')} \]
那麼就能夠\(O(n)\)計算出C(x)。
將其轉爲點值表達式須要帶入n個不一樣的x,利用霍納法則,帶入一個x須要\(O(n)\)時間:
\[ A(x_0)=a_0+x_0(a_1+x_0(a_2+...+x_0(a_{n-2}+x_0(a_{n-1}))...)) \]
從點值表達式轉換爲係數表達式,用插值法,將n個不一樣的值帶入A,惟一肯定了一個係數表達式(證實可見《算法導論》)。
利用 FFT 能夠在\(O(n\log n)\) 內完成係數到點值,以及從點值到係數表達式。
n次單位複數根\(w\)是知足\(w^n=1\)的複數。 主n次單位根爲\(w_n=e^{\frac {2\pi } ni}\),其它全部n次單位複數根都是\(w_n\)的冪次。
咱們知道複數的指數形式的定義是:
\[ e^{ui}=\cos(u)+i\sin(u) \]
因此n個複數根均勻分佈在以複平面原點爲圓心的單位圓上。
消去引理 \(w^{dk}_{dn}=w^k_n\),帶入定義便可證實。推論:\(w^{n/2}_n=w_2=-1\)
折半引理\((w^{k+n/2}_n)^2=(w_n^k)^2\),展開便可證實。推論:\(w^{k+n/2}_n=-w^{k}_n\)
爲了方便分治,咱們將n補到2的次冪(多項式後面加0)。將n個n次單位根帶入A(x)的過程就是對係數向量進行 DFT 。
\[ A(w_n^k)=\sum_{j=0}^{n-1}a_jw^{kj}_n,k=0,1,..,n-1 \]
考慮按指數奇偶分類:
\[ A_{even}(x)=a_0+a_2x+a_4x^2+...\\ A_{odd}(x)=a_1+a_3x+a_5x^2+...\\ \]
那麼
\[ A(x)=A_{even}(x^2)+xA_{odd}(x^2)\\ A(w_n^k)=A_{even}(w_{n}^{2k})+w_n^kA_{odd}(w_n^{2k})\\ \]
當\(k< n/2\)時,由消去引理
\[ A_{even}(w_n^{2k})+w_n^kA_{odd}(w_n^{2k})=A_{even}(w_{n/2}^k)+w_n^kA_{odd}(w_{n/2}^k) \]
也就是至關於對兩個長度爲n/2的多項式\(A_{even}(x)\),\(A_{odd}(x)\)進行 DFT。
指數不小於\(n/2\)的部分與\(k+n/2\)一一對應,由折半引理
\[ A_{even}((w_n^{k+n/2})^2)+w_n^{k+n/2}A_{odd}((w_n^{k+n/2})^2)=A_{even}(w_{n/2}^k)-w_n^kA_{odd}(w_{n/2}^k) \]
因此這個過程是能夠遞歸的,且\(T(n)=2T(n/2)+O(n)\)。
typedef complex<double> CD; const double pi = acos(-1); CD tmp[N],epsilon[N]; void init_epsilon(int n){ for(int i = 0; i < n; ++i){ epsilon[i] = CD(cos(2.0 * pi * i / n), sin(2.0 * pi * i / n)); arti_epsilon[i] = conj(epsilon[i]); } } void recursive_fft(int n, CD* A,int offset, int step, CD* w){ if(n==1)return; int m=n>>1; recursive_fft(m,A,offset,step<<1,w); recursive_fft(m,A,offset+step,step<<1,w); for(int k=0;k<m;++k){ int pos=2*step*k; tmp[k] =A[pos+offset]+w[k*step]*A[pos+offset+step]; tmp[k+m]=A[pos+offset]-w[k*step]*A[pos+offset+step]; } for(int i=0;i<n;++i) A[i*step+offset]=tmp[i]; }
可是遞歸須要比較大的空間,如何實現迭代的寫法?
觀察遞歸的過程,第一步:
0(000)2(010)4(100)6(110),1(001)3(011)5(101)7(111)
第二步:
0 (000)4(100),2(010) 6(110),1(001)5(101)3(011)7(111)
將下標的二進制翻轉過來就是:
000,001,010,011,100,101,110,111
對應了0,1,2,3,...
用reverse(i)表示i的二進制翻轉後的數。就至關於二進制從高位到低位的+1,是從左往右遇到第一個0,就改成1,左邊的1改成0。
int reverse(int x){ for(int l=1<<bit_length;(x^=l)<l;l>>=l); return x; }
咱們獲得遞歸最後一步的數組:
0,4,2,6,1,5,3,7
就能夠從下到上迭代了。
void bit_reverse(CD* A,int n){ for(int i=0,j=0;i<n;++i){ if(i>j)swap(A[i],A[j]); for(int l=n>>1;(j^=l)<l;l>>=1); } } void fft(CD* A, int n, CD* w){ bit_reverse(A,n); for(int i=2;i<=n;i<<=1)//自下到上,i爲每一層的步長,或者說子問題長度 for(int j=0,m=i>>1;j<n;j+=i)//j爲偏移量,或者說這一層的每個子問題的起點 for(int k=0;k<m;++k){//k=0..i/2,計算出子問題的第k個和第k+i/2個的值 CD b=w[n/i*k]*A[j+m+k]; A[j+m+k]=A[j+k]-b; A[j+k]+=b; } }
將點值轉回係數表達式,至關於解方程組\(\vec{y}=V_n\vec{a}\),其中\(\vec {a}\)是係數向量。\(V_n\)是以下範德蒙德矩陣:
\[ \begin{bmatrix} 1&1&1&1&\cdots &1\\ 1&w;_n&w;_n^2&w;_n^3&\cdots &w;_n^{n-1}\\ 1&w;_n^2&w;_n^4&w;_n^6&\cdots &w;_n^{2(n-1)}\\ \vdots&\vdots&\vdots&\vdots&\ddots&\vdots\\ 1&w;_n^{n-1}&w;_n^{2(n-1)}&w;_n^{3(n-1)}&\cdots &w;_n^{(n-1)(n-1)}\\ \end{bmatrix} \]
那麼\(V_n^{-1}\vec{y}=\vec{a}\)。
可令\([V_n^{-1}]_{kj}=w^{-kj}_n/n\),只需證實\(V_n^{-1}V_n=I_n\):
\[ [V_n^{-1}V_n]_{jj'}=\sum_{k=0}^{n-1}(w_n^{-kj}/n)(w_n^{kj'})=\sum_{k=0}^{n-1}w_n^{k(j'-j)}/n = \left\{ \begin{aligned} 0,j'\neq j \\ 1,j'=j \end{aligned} \right. \]
因此只要用\(w_n^{-1}\)替換\(w_n\),並將結果每一個元素除以n,就能夠計算出\(DDF_n^{-1}\)。
注意FFT由於用了cos和sin,以及是浮點數計算,因此會有精度偏差,故加了0.5。
#include <bits/stdc++.h> using namespace std; #define rep(i,l,r) for(int i=l;i<r;++i) #define per(i,l,r) for(int i=r-1;i>=l;--i) #define SZ(x) ((int)(x).size()) typedef double dd; typedef complex<dd> CD; const dd PI=acos(-1.0); const int L=18,N=1<<L; CD eps[N],inv_eps[N],f[N],g[N]; void init_eps(int p){ rep(i,0,p)eps[i]=CD(cos(PI*i*2/p),sin(PI*i*2/p)),inv_eps[i]=conj(eps[i]); } void fft(CD p[], int n, CD w[]){ for(int i=0,j=0;i<n;++i){ if(i>j)swap(p[i],p[j]); for(int l=n>>1;(j^=l)<l;l>>=1); } for(int i=2;i<=n;i<<=1) for(int j=0,m=i>>1;j<n;j+=i) rep(k,0,m){ CD b=w[n/i*k]*p[j+m+k]; p[j+m+k]=p[j+k]-b; p[j+k]+=b; } } int ans[N]; int main(){ string a,b; cin>>a>>b; int n=max(SZ(a),SZ(b)),p=1; while(p<n)p<<=1;p<<=1; rep(i,0,p)f[i]=g[i]=0; n=0;per(i,0,SZ(a))f[n++]=a[i]-'0'; n=0;per(i,0,SZ(b))g[n++]=b[i]-'0'; init_eps(p); fft(f,p,eps);fft(g,p,eps); rep(i,0,p)f[i]*=g[i]; fft(f,p,inv_eps); int t=0; rep(i,0,p){ ans[i]=t+(f[i].real()+0.5)/p; if(ans[i]>9){t=ans[i]/10;ans[i]%=10;} else t=0; } bool flag=0; per(i,0,p)if(ans[i]||flag){ printf("%d",ans[i]);flag=1; } if(flag==0)puts("0"); return 0; }
爲了解決FFT產生的精度偏差,就有了一種在模意義下的方法,運算過程只有整數。它就是NTT(Number-Theoretic Transform)。
設m是正整數,a是整數,則使得同餘式\(a^r \equiv 1 \mod m\) 成立的最小正整數 \(r\) 叫作 \(a\) 對於模 \(m\) 的指數。
若a模m的指數等於φ(m)(歐拉函數),則稱a爲模m的一個原根。
咱們令\(p=c\cdot 2^k+1\) ,那麼對於2的冪n有 \(n|(p-1)\)。設
\[ g_n=g^{\frac {p-1}n} \]
考慮FFT中,須要用到單位根的如下性質:
\(w_n^k(0\le k < n)\)互不相同,保證點值表示的合法;
\(\omega_{n} ^ {2k} = \omega_{n/2} ^k\),用於分治;
\(\omega_n ^ { k + \frac{n}{2} } = -\omega_n ^ k\),用於分治;
僅當\(k \neq 0\)時,\(\sum_{j=0}^{n-1}(w_n^k)^j=0\),用於逆變換。
原根是否也有以上性質呢?
令\(p=c\cdot 2^k+1\),取\(n=2^m\),則(g_n^k(0\le k<n)=g^{\frac {(p-1)k}="" n}="g^0,g^{c\cdot" 2^{k-m}},g^{2c\cdot="" 2^{k-m}}…,g^{(p-1){c\cdot="" 2^{k-m}}})<="" span="">,互不相等。</n)=g^{\frac>
這裏有個小知識點,若 \(a_1,a_2,..a_p\) 是模p的徹底剩餘系,那麼 \(aa_1+b,aa_2+b,..aa_p+b\)也是模p的徹底剩餘系,證實:若 \(aa_j+ba\equiv a_i+b \mod p\)則 \(a_i \equiv a_j \mod p\),矛盾。
\(g_n^{2k}=g^{\frac {2k(p-1)} n}=g^{\frac{k(p-1)}{n/2}}=g_{n/2}^k\)
\(g_n^n=g^{p-1}\equiv 1\mod p\Rightarrow g^{\frac {p-1}2}\equiv\sqrt 1 \mod p\),由於\(g^k\)互不相同,因此\(g^{\frac {p-1}2}\equiv -1\mod p\),因而\(g^{k+\frac n 2}_n=g_n^k\cdot g_n^{\frac n 2}=g_n^k\cdot g^{\frac {p-1} 2}=-g_n^k\)。
當\(k \neq 0\)時,\(\sum_{j=0}^{n-1}(g_n^k)^j=\frac{1-(g_n^k)^n}{1-g_n^k}\),由3得,\(g_n^n=1\),因此\(1-(g_n^k)^n=1-(g_n^n)^k=0\)。不然等於n。
所以在FFT中用原根代替單位根就是NTT了。若是要求模數任意,只要再用 中國剩餘定理(CRT)合併便可。
仍然是上面高精度乘法那題
#include <bits/stdc++.h> using namespace std; #define rep(i,l,r) for(int i=l;i<r;++i) #define per(i,l,r) for(int i=r-1;i>=l;--i) #define SZ(x) ((int)(x).size()) typedef long long LL; const int L=18,N=1<<L; const LL C = 479; const LL P = (C << 21) + 1; const LL G = 3; LL qpow(LL a, LL b, LL m){ LL ans = 1; for(a%=m;b;b>>=1,a=a*a%m)if(b&1)ans=ans*a%m; return ans; } LL eps[N],inv_eps[N],f[N],g[N]; void init_eps(int n){ LL t=(P-1)/n, invG=qpow(G,P-2,P); rep(i,0,n) eps[i]=qpow(G,t*i,P),inv_eps[i]=qpow(invG,t*i,P); } void fft(LL p[], int n, LL w[]){ for(int i=0,j=0;i<n;++i){ if(i>j)swap(p[i],p[j]); for(int l=n>>1;(j^=l)<l;l>>=1); } for(int i=2;i<=n;i<<=1) for(int j=0,m=i>>1;j<n;j+=i) rep(k,0,m){ LL b=w[n/i*k]*p[j+m+k]%P; p[j+m+k]=(p[j+k]-b+P)%P; p[j+k]=(p[j+k]+b)%P; } } LL ans[N]; int main(){ string a,b; cin>>a>>b; int n=max(SZ(a),SZ(b)),p=1; while(p<n)p<<=1;p<<=1; rep(i,0,p)f[i]=g[i]=0; n=0;per(i,0,SZ(a))f[n++]=a[i]-'0'; n=0;per(i,0,SZ(b))g[n++]=b[i]-'0'; init_eps(p); fft(f,p,eps);fft(g,p,eps); rep(i,0,p)f[i]=f[i]*g[i]%P; fft(f,p,inv_eps); int t=0; LL invp=qpow(p,P-2,P); rep(i,0,p){ ans[i]=(t+f[i]*invp%P)%P; if(ans[i]>9){t=ans[i]/10;ans[i]%=10;} else t=0; } bool flag=0; per(i,0,p)if(ans[i]||flag){ printf("%lld",ans[i]);flag=1; } if(flag==0)puts("0"); return 0; }