原文鏈接https://www.cnblogs.com/zhouzhendong/p/Fast-Fourier-Transform.html
多項式 之 快速傅里葉變換(FFT)/數論變換(NTT)/例題與常用套路【入門】
前置技能
- 對復數以及復平面有一定的了解
- 對數論要求了解:逆元,原根,中國剩余定理
- 對分治有充足的認識
- 對多項式有一定的認識,並會寫 $O(n^2)$ 的高精度乘法
本文概要
- 多項式定義及基本卷積形式
- $Karatsuba$ 乘法
- 多項式的系數表示與點值表示,以及拉格朗日插值法
- 復數與單位根
- 快速傅里葉變換 $(FFT)$
- 數論變換 $(NTT)$
- 分治 $FFT$
- 拆系數 $FFT$ 和三模數 $NTT$
- 例題與套路
前言
近年來,多項式理論進入中國,在中國 $OI$ 界逐漸占據一方,是一個值得我們去研究的理論。現在, $OI$ 題中出現次數越來越頻繁的多項式題,也鼓勵了許多 $OIer$ 去學習多項式。
作為多項式的一個重要算法—— $FFT$ ,它有着極其優越的作用。比如,對於初學高精度時的你,是否聽說過高精度乘法可以 $O(n\log n)$ ? $FFT$ 可以來解決一類多項式卷積,是生成函數一系列操作的基礎,可以解決很多計數問題。
於是,菜雞博主去學了一下 $FFT$ ,寫了這篇總結。
多項式定義及基本卷積形式
多項式
定義 多項式 為形如下式的代數表達式。
$$P(x)=\sum_{i=0}^{n}a_ix^i=a_0+a_1x+a_2x^2+\cdots+a_nx^n$$
其中 $a_0,a_1,a_2,\cdots,a_n$ 稱為多項式的 系數。
$x$ 沒有確定的值。
最高次項的指數 $n$ 叫做多項式的 度 $(Degree,n=deg\ P)$ ,也可以說是多項式的 次數。
多項式基本卷積形式
下面的這個多項式卷積就是多項式乘法。
定義兩個多項式 $g(x),f(x)$ ,設他們的度數分別為 $n,m$ ,則卷積具有如下形式:(設 $g_i$ 為 $g$ 的 $i$ 次項系數, $f_i$ 為 $f$ 的 $i$ 次項系數)
$$h(x)=g(x)f(x)=\sum_{i=0}^{n}\sum_{j=0}^{m}g_if_jx^{i+j}$$
$$=\sum_{i=0}^{n+m}\sum_{j=0}^{i}g_jf_{i-j}x^i$$
請務必理解並記住第二行的卷積式,這將會在后面不停的出現。
我們顯然可以通過公式來 $O(nm)$ 得到卷積結果。
$Karatsuba$ 乘法
針對這種卷積, $Karatsuba$ 提出了下面的這種方法:
對於多項式 $F$ ,我們令 $n=deg\ F+1$ 。
即多項式可以寫成這個形式: $F(x)=\sum_{i=0}^{n-1}a_ix^i$ 。
令 $F(x)=F_0(x)+x^{\frac n2}F_1(x),G(x)=G_0(x)+x^{\frac n2}G_1(x)$ 。
其中, $deg\ F_0=deg\ F_1=deg\ G_0=deg\ G_1=\frac n2$ 。
則
$$(F\times G)(x)=(F_0\times G_0)(x)+x^{\frac n2}(F_0\times G_1+F_1\times G_0)(x)+x^n(F_1\times G_1)(x)$$
令 $M(x)=((F_0+F_1)\times(G_0+G_1))(x)$
我們會開心的發現:
$$(F_0\times G_1+F_1\times G_0)(x)=M(x)-(F_0\times G_0)(x)-(F_1\times G_1)(x)$$
於是我們只需要計算三個多項式卷積 $M(x),(F_0\times G_0)(x)-(F_1\times G_1)(x)$ 即可。
我們采用分治的做法,於是時間復雜度為:
$$T(n)=3T(\frac n2)+O(n)=n^{\log_23}\approx n^{1.585}$$
多項式的系數表示與點值表示,以及拉格朗日插值法
系數表示
(令 $n=deg\ F$ )
這里拿出了最開始提到的多項式:
$$f(x)=a_0+a_1x+a_2x^2+\cdots+a_nx^n$$
把 $a$ 數組看作 $n+1$ 維向量 $\vec{a}=(a_0,a_1,\cdots,a_n)$ ,其 系數表示 就是向量 $\vec{a}$ 。
點值表示
(令 $n=deg\ F$ )
取任意 $n+1$ 個不同的 $x_0,x_1,\cdots,x_n$ 代入多項式進行求值,得到了 $n+1$ 個不同的二元組 $(x_0,F(x_0)),(x_1,F(x_1)),\cdots,(x_n,F(x_n))$ 。
我們稱這 $n+1$ 個點表示多項式的方法叫做多項式的 點值表示 。
這里要注意,多項式的點值表示有無數種,但是多項式的系數表示是唯一的。
拉格朗日插值法
實現系數表示到點值表示的變換,我們可以直接把 $x$ 代入求解,復雜度 $O(n^2)$ 。
但是點值表示轉系數表示就沒這么簡單了。
這里首先提一點:雖然同一個多項式的點值表示有無數種,但是這些點值表示都能唯一的確定一個多項式,唯一的確定一個系數表示。
從點值表示轉到系數表示,我們可以使用插值法。
其中拉格朗日插值法能夠在 $O(n^2)$ 的復雜度內得到系數表示。
具體在這里不展開介紹了,可以參見:
https://www.zhihu.com/question/58333118/answer/262507694
復數與單位根
復數的定義
復數 $(Complex)$ 由實部和虛部組成。
可以寫成如下形式:
$$A=a+ib,(a,b\in \mathbb R,A\in\mathbb C)$$
其中 $A$ 就是一個復數。
其中 $i=\sqrt{-1}$ ,為虛數單位。
在后面的公式中要注意區分虛數單位 $i$ 和求和指標 ($\sum$) $i$ 。
顯然這里可以列舉三個 $FFT$ 常用的復數運算:
$(A_1,A_2\in\mathbb C,a_1,b_1,a_2,b_2\in\mathbb R)$
$$A_1+A_2=(a_1+ib_1)+(a_2+ib_2)=(a_1+a_2)+i(b_1+b_2)$$
$$A_1-A_2=(a_1+ib_1)-(a_2+ib_2)=(a_1-a_2)+i(b_1-b_2)$$
$$A_1\times A_2=(a_1+ib_1)(a_2+ib_2)=a_1a_2+i^2b_1b_2+ia_1b_2+ia_2b_1\\=(a_1a_2-b_1b_2)+i(a_1b_2+a_2b_1)$$
復平面
復平面是個二維平面,其中橫軸代表實數,稱為實軸。縱軸代表虛數,稱為虛軸。
定義復平面上從原點出發的向量$\vec{a}=(a,b)$表示虛數$a+ib$。
例如:
其中的 $5$ 條藍色向量就代表了 $5$ 個虛數。
關於復數乘法,有一個口訣:模長相乘,幅角相加。
模長就是復數在復平面上表示的向量的模長,幅角就是以正實軸為始邊,這個向量為終邊所構成的角。
單位根
$n$ 次單位根 $\omega_n\in\mathbb C$ ,滿足 $\omega_n^n=1$ 。
顯然 $n$ 次方程有 $n$ 個解,把他們寫在復平面上面,可以把單位圓等分成 $n$ 份。由於復數乘法模長相乘,所以模長永遠是 $1$ ,說明他們總在單位圓上。由於幅角相加,得到他們等分單位圓。
於是我們可以寫出單位根的表達式:
記 $\omega_n=\cos(\frac{2\pi}{n})+i\sin(\frac{2\pi}{n})$ ,則 $n$ 次單位根就是 $\omega_n^0, \omega_n^1, \cdots, \omega_n^{n-1}$ 。
通項公式, $\omega_n^i=\cos(\frac{2i\pi}{n})+i\sin(\frac{2i\pi}{n})\ \ \ \ (0\leq i<n)$
當然,在數學上,單位根還有這種定義: $\omega_n=e^{\frac{2\pi i}{n}}$ ,讀者可以嘗試通過這個來推導前幾個式子,這里不展開介紹。
單位根有一些優秀的性質。
定理1:相消定理
對於任意整數 $n\geq 0,k\geq 0,d\geq 0$ ,有:
$$\omega_{dn}^{dk}=e^{\frac{2\pi dki}{dn}}=e^{\frac{2\pi ki}{n}}=\omega_n^k$$
即:
$$\omega_{dn}^{dk}=\omega_{n}^{k}$$
定理2:折半定理
對於任意的偶數 $n\geq 0,k\geq 0$ ,有
$$\omega_{n}^{k}=\omega_{\frac n2}^{\frac k2}$$
這個由定理1顯然可得。
此外,我們還可以從復平面圖像的角度理解單位根。
觀察任何一個單位根 $\omega_{n}^{i}$ ,
$$\omega_{n}^{i+1}=\omega_{n}^{i}\times\omega_{n}^{1}$$
觀察其圖像,會發現 $\times\omega_{n}^{1}$ 的效果就是把原復數在復平面上的向量繞原點逆時針旋轉$\frac{2\pi}{n}$的角度。
類似的, $\times\omega_{n}^{1}$ 的效果相反,為順時針方向。
再有,顯然, $\omega_{n}^{-i}=\omega_{n}^{n-i}$ 。
於是對於整數 $i$ ,如果從 $\omega_{n}^{0}$ 逆時針旋轉一定角度得到的向量為單位根 $\omega_{n}^{i}$ ,那么順時針旋轉相同的角度也必然會得到 $\omega_{n}^{-i}=\omega_{n}^{n-i}$ 。
於是如果把所有 $n$ 次單位根在復平面上畫出來,他們是上下對稱的。
性質3
所以如果令 $\omega_{n}^{k}=a+ib$ ,則 $\omega_{n}^{-k}=a-ib\ \ \ \ (a,b\in \mathbb R,\sqrt{a^2+b^2}=1)$ ,即 $\omega_n^{-k}=conj(\omega_n^k)$ 。
考慮到 $\omega_{n}^{n}$ 是繞着原點轉了一圈,那么 $\omega_{n}^{\frac n2}$ 就是轉了半圈,所以 $\omega_{n}^{\frac n2}=-1$ 。
所以 $\omega_{n}^{i}$ 與 $\omega_{n}^{i+\frac n2}$ 在單位圓上的位置是相對的,因為轉了半圈。換句話說:
性質4
$$\omega_{n}^{i+\frac n2}=\omega_{n}^{\frac n2}\cdot\omega_{n}^{i}=-\omega_{n}^{i}$$
快速傅里葉變換(Fast Fourier Transform, FFT)
回憶一下之前的多項式卷積:
$$h(x)=\sum_{i=0}^{n+m}\sum_{j=0}^{i}g_jf_{j-i}x^i$$
我們要快速求 $h(x)$ 的每一項系數。
系數表示並不能支持快速的卷積。
但是在點值表示下,卻可以在 $O(n)$ 復雜度內快速卷積。
目前的瓶頸在於系數表示與點值表示的快速的相互轉化。
由於點值表示有很多種,只要你選擇的 $x$ 互不相同即可。於是我們可以考慮選擇一些特殊的 $x$ 。
注意,后面為了方便,設多項式的度為 $n-1,(n=2^a,a\in\mathbb Z)$ ,如果次數不足則高位補上系數 $0$ ,保證任何運算過程中多項式的真的度都小於 $n$ 。
離散傅里葉變換
設有多項式
$$F(x)=\sum_{i=0}^{n-1}a_ix^i$$
將 $n$ 個不同的 $n$ 次單位根 $\omega_{n}^{0},\omega_{n}^{1},\cdots,\omega_{n}^{n-1}$ 代入到 $F(x)$ 中,得到點值表達式:
$$\vec{y}=(F(\omega_{n}^{0}),F(\omega_{n}^{1}),\cdots,F(\omega_{n}^{n-1}))$$
於是點值向量 $\vec{y}$ 就叫做系數向量 $\vec{a}=(a_0,a_1,\cdots,a_{n-1})$ 的離散傅里葉變換 $(Discrete\ Fourier\ Transform,\ DFT)$ 。
但是直接代入求值是 $O(n^2)$ 的,我們需要一個更快的求值方法。
令 $F_0(x)=\sum_{i=0}^{\frac n2-1}a_{2i}x^i,F_1(x)=\sum_{i=0}^{\frac n2-1}a_{2i+1}x^i$
則:(下面的推導會用到之前介紹過的單位根性質的第2條和第4條)
$$F(x)=F_0(x^2)+xF_1(x^2)$$
$$F(\omega_{n}^{i})=F_0(\omega_{n}^{2i})+\omega_{n}^{i}F_1(\omega_{n}^{2i})=F_0(\omega_{\frac n2}^{i})+\omega_{n}^{i}F_1(\omega_{\frac n2}^{i})$$
$$F(\omega_{n}^{i+\frac n2})=F(-\omega_{n}^{i})=F_0(\omega_{n}^{2i})-\omega_{n}^{i}F_1(\omega_{n}^{2i})=F_0(\omega_{\frac n2}^{i})-\omega_{n}^{i}F_1(\omega_{\frac n2}^{i})$$
注意: $deg\ F_0=deg\ F_1=\frac n2$ 。
於是我們可以繼續分治,對於 $F_0$ 和 $F_1$ 再繼續同樣的操作。
時間復雜度:
$$T(n)=2T(\frac n2)+O(n)=O(n\log n)$$
逆離散傅里葉變換
現在你需要快速的把點值表達式轉換成系數表達式。
與剛才的離散傅里葉變換相反,系數向量 $\vec{a}=(a_0,a_1,\cdots,a_{n-1})$ 就叫做點值向量 $\vec{y}$ 的逆離散傅里葉變換 $(Inverse\ Discrete\ Fourier\ Transform,\ IDFT)$
首先,我們把剛才的 $DFT$ 過程用矩陣來表示:
$$\begin{equation}\begin{bmatrix} (\omega_n^0)^0 & (\omega_n^0)^1 & \cdots & (\omega_n^0)^{n-1} \\ (\omega_n^1)^0 & (\omega_n^1)^1 & \cdots & (\omega_n^1)^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{n-1})^0 & (\omega_n^{n-1})^1 & \cdots & (\omega_n^{n-1})^{n-1} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \begin{bmatrix} A(\omega_n^0) \\ A(\omega_n^1) \\ \vdots \\ A(\omega_n^{n-1}) \end{bmatrix}\end{equation}$$
我們記左側的系數矩陣為 $\mathbf V$ ,構造矩陣 $d_{i,j}=(\omega_{n}^{-i})^j$
$$\mathbf D = \begin{bmatrix} (\omega_n^{-0})^0 & (\omega_n^{-0})^1 & \cdots & (\omega_n^{-0})^{n-1} \\ (\omega_n^{-1})^0 & (\omega_n^{-1})^1 & \cdots & (\omega_n^{-1})^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{-(n-1)})^0 & (\omega_n^{-(n-1)})^1 & \cdots & (\omega_n^{-(n-1)})^{n-1} \end{bmatrix}$$
設 $\mathbf E=\mathbf D\cdot\mathbf V$ ,則:
$$\begin{eqnarray*} e_{ij} &=& \sum_{k=0}^{n-1} d_{ik} v_{kj} \\ &=& \sum_{k=0}^{n-1} \omega_n^{-ik}\omega_n^{kj} \\ &=& \sum_{k=0}^{n-1} \omega_n^{k(j-i)}\\&=&\large{\begin{cases}n&\text{$(i=j)$}\\ \sum_{k=0}^{n-1}(\omega_{n}^{j-i})^k=\frac{1-(\omega_{n}^{j-i})^n}{1-\omega_{n}^{j-i}}=0&\text{$(i\neq j)$}\end{cases}}\end{eqnarray*}$$
於是我們發現了一個非常特殊的性質:
$\mathbf E$ 是單位矩陣的 $n$ 倍。
也就是 $\frac 1n\mathbf D=\mathbf V^{-1}$ 。
於是我們將 $\frac 1n\mathbf D$ 在 $(1)$ 式兩側左乘,得到:
void fft(int n, complex<double>* buffer, int offset, int step, complex<double>* epsilon) { if(n == 1) return; int m = n >> 1; fft(m, buffer, offset, step << 1, epsilon); fft(m, buffer, offset + step, step << 1, epsilon); for(int k = 0; k != m; ++k) { int pos = 2 * step * k; temp[k] = buffer[pos + offset] + epsilon[k * step] * buffer[pos + offset + step]; temp[k + m] = buffer[pos + offset] - epsilon[k * step] * buffer[pos + offset + step]; } for(int i = 0; i != n; ++i) buffer[i * step + offset] = temp[i]; } //http://blog.miskcoo.com/2015/04/polynomial-multiplication-and-fast-fourier-transform#IDFT
$$a_k\leftarrow a_k + tmp$$
這個操作被叫做蝴蝶操作。
迭代版FFT模版 -UOJ#34多項式乘法
#include <bits/stdc++.h> using namespace std; const int N=1<<20; const double PI=acos(-1.0); struct C{ double r,i; C(){} C(double a,double b){r=a,i=b;} C operator + (C x){return C(r+x.r,i+x.i);} C operator - (C x){return C(r-x.r,i-x.i);} C operator * (C x){return C(r*x.r-i*x.i,r*x.i+i*x.r);} }a[N],b[N],w[N]; int A,B,n,L,R[N]; void FFT(C a[],int n){ for (int i=0;i<n;i++) if (R[i]>i) swap(a[R[i]],a[i]); for (int t=n>>1,d=1;d<n;d<<=1,t>>=1) for (int i=0;i<n;i+=(d<<1)) for (int j=0;j<d;j++){ C tmp=w[t*j]*a[i+j+d]; a[i+j+d]=a[i+j]-tmp; a[i+j]=a[i+j]+tmp; } } int main(){ scanf("%d",&A);A++; scanf("%d",&B);B++; for (int i=0;i<A;i++) scanf("%lf",&a[i].r); for (int i=0;i<B;i++) scanf("%lf",&b[i].r); for (n=1,L=0;n<=A+B;n<<=1,L++); for (int i=0;i<n;i++){ R[i]=(R[i>>1]>>1)|((i&1)<<(L-1)); w[i]=C(cos(2.0*i*PI/n),sin(2.0*i*PI/n)); } FFT(a,n),FFT(b,n); for (int i=0;i<n;i++) a[i]=a[i]*b[i],w[i].i=-w[i].i; FFT(a,n); A--,B--; for (int i=0;i<=A+B;i++) printf("%d ",int(a[i].r/n+0.5)); return 0; }
數論變換(Number-Theoretic Transform,NTT)
$FFT$ 雖然能快速處理卷積,但是它也有很大的弊端。精度問題有時會導致一些錯誤。而且,有許多題目涉及了取模,比如 $998244353$ ,復數域下的 $DFT$ 精度更是暴露無遺。於是考慮是否有模意義下的這種算法。於是,便出現了快速數論變換($Fast\ Number-Theoretic\ Transform,\ FNT$)。
雖然之前列舉了一些單位根的性質,但是 $FFT$ 用到的卻和我列舉的有些差別。
於是我們回顧一下 $FFT$ 利用了單位根的什么性質。
1. $\omega_{n}^{n}=1$
2. $\omega_{n}^{0},\omega_{n}^1,\cdots,\omega_{n}^{n-1}$ 互不相同,滿足了點值表示的條件。
3. $\omega_n^2=\omega_{\frac{n}{2}}, \omega_n^{\frac{n}{2}+k}=-\omega_n^k$ ,這個是分治的基礎。
4. $\omega_{n}^{-k}=conj(\omega_n^k)$
$\sum_{k=0}^{n-1}(\omega_n^{j-i})^k=\begin{cases}0&\text{$(i\neq j)$}\\n&\text{$(i=j)$}\end{cases}$
這個是$IDFT$的基礎。
原根
我們需要找滿足上面的性質的整數。
於是我們找到了原根。
對於一個質數 $p$ ,其 原根 $g$ 滿足 $g^0,g^1,g^2,\cdots,g^{p-2}$ 在模 $p$ 意義下兩兩不同。
可以發現, $n$ 需要是 $p-1$ 的因數,才能滿足條件。由於 $n$ 是 $2$ 的冪,所以對 $p$ 也有一定的要求。
$p$ 得是形如 $r\cdot 2^k+1$ 的素數,其中 $2^k\geq n$ 。
有一些非常常見的 $NTT$ 模數:
$998244353=119\times 2^{23}+1$ ,原根為 $3$ 。
$1004535809=479\times 2^{21}+1$ ,原根為 $3$ 。
更多 $NTT$ 模數 $\Longrightarrow$ http://blog.miskcoo.com/2014/07/fft-prime-table
現在我們來證明一下原根滿足上面的那些性質。
令 $g_n^n\equiv 1\ \pmod p$ 且 $g_n ^ 0,g_n ^ 1,\cdots g_n ^ {n-1}$ 互不相同,則 $g_n \equiv g^r \pmod p$(這里的 $r=(p-1)/n$ 與之前提到的無關),等價於 $\omega_n$ 。
1. $\omega_{n}^{n}=1$
根據定義,顯然成立。
2. $\omega_{n}^{0},\omega_{n}^1,\cdots,\omega_{n}^{n-1}$互不相同
根據原根的定義,也顯然成立。
3. $\omega_n^2=\omega_{\frac{n}{2}}, \omega_n^{\frac{n}{2}+k}=-\omega_n^k$
由於 $g_n\equiv g^r\ \pmod p$ ,其中由於 $nr=p-1$ ,當 $n$ 取 $\frac n2$ 時, $r$ 取 $2r$ ,所以 $g_{\frac n2}\equiv g^{2r}\equiv(g^r)^2\equiv g_n^2\ \pmod p$ 。
由費馬小定理可得 $g^{p-1}-1\equiv (g^{\frac{p-1}2}+1)(g^{\frac{p-1}2}-1)\equiv 0\pmod p$ ,所以 $g^{\frac{p-1}2}\equiv \pm 1\pmod p$ 。又由於 $g$ 為原根,滿足第 $2$ 條性質。由於 $g^0\equiv 1\pmod p$ ,所以 $g^{\frac{p-1}2}\equiv -1\pmod p$ 。於是:
$$g_{n}^{\frac n2}\equiv g^{\frac{p-1}2}\equiv -1\pmod p$$
即 $g_n^{\frac n2+k}\equiv g_n^k\times g_n^{\frac n2}\equiv -g_n^k\pmod p$ 。
4. $\sum_{k=0}^{n-1}(\omega_n^{j-i})^k=\begin{cases}0&\text{$(i\neq j)$}\\n&\text{$(i=j)$}\end{cases}$
由於 $4$ 的第一項不是很重要,所以可以不管(直接逆元算算就好了)。
$$\sum_{k=0}^{n-1}g_n^{k(j-i)}\equiv\begin{cases}n&\text{$(i=j)$}\\ \sum_{k=0}^{n-1}(g_{n}^{j-i})^k\equiv\frac{1-(g_{n}^{j-i})^n}{1-g_{n}^{j-i}}\equiv 0 &\text{$(i\neq j)$}\end{cases}\pmod p$$
於是,綜上所述,原根滿足了 $FFT$ 要用到的單位根的性質,於是我們可以把單位復根替換成原根,再寫個和 $FFT$ 差不多的就可以了。
NTT參考代碼 - 51Nod1028 大數乘法 V2
#include <cstdio> #include <algorithm> #include <cstdlib> #include <cmath> #include <cstring> using namespace std; typedef long long LL; const LL mod=998244353; const int N=1<<20; LL Pow(LL x,LL y){ if (!y) return 1LL; LL xx=Pow(x,y/2); xx=xx*xx%mod; if (y&1LL) xx=xx*x%mod; return xx; } LL A,B,a[N],b[N],R[N],g[N],n,L; char str[N]; void read(){ scanf("%s",str); A=strlen(str); for (int i=0;i<A;i++) a[A-i-1]=str[i]-'0'; scanf("%s",str); B=strlen(str); for (int i=0;i<B;i++) b[B-i-1]=str[i]-'0'; } void NTT(LL a[N],int n){ for (int i=0;i<n;i++) if (i<R[i]) swap(a[i],a[R[i]]); for (int d=1,t=n>>1;d<n;d<<=1,t>>=1) for (int i=0;i<n;i+=(d<<1)) for (int j=0;j<d;j++){ LL tmp=g[t*j]*a[i+j+d]%mod; a[i+j+d]=(a[i+j]-tmp+mod)%mod; a[i+j]=(a[i+j]+tmp)%mod; } } int main(){ read(); for (n=1,L=0;n<=A+B;n<<=1,L++); for (int i=0;i<n;i++) R[i]=(R[i>>1]>>1)|((i&1)<<(L-1)); g[0]=1,g[1]=Pow(3,(mod-1)/n); for (int i=2;i<n;i++) g[i]=g[i-1]*g[1]%mod; NTT(a,n),NTT(b,n); for (int i=0;i<n;i++) a[i]=a[i]*b[i]%mod; g[0]=1,g[1]=Pow(g[1],mod-2); for (int i=2;i<n;i++) g[i]=g[i-1]*g[1]%mod; NTT(a,n); LL Inv=Pow(n,mod-2); for (int i=0;i<n;i++) a[i]=a[i]*Inv%mod; for (int i=0;i<n-1;i++) a[i+1]+=a[i]/10,a[i]%=10; int d; for (d=n-1;d&&!a[d];d--); for (int i=d;i>=0;i--) putchar(a[i]+'0'); return 0; }
注意,從本節以后,請注意觀察式子中是否有卷積形式
"$\Huge{a_i=\sum_{j=0}^{i}b_jc_{i-j}}$"。
分治FFT
顧名思義,分治 $FFT$ 就是分治再加上 $FFT$ 嘛。
考慮有如下的遞推式:
$$a_i=\sum_{j=1}^{i}k_ja_{i-j}$$
其中 $k$ 數組以及 $a_0$ 給出。
我們發現這個式子非常像多項式卷積的形式,但是顯然不能所有的 $a_i$ 一起計算,就是說一次 $FFT$ 顯然不能解決問題。
於是我們可以分治 $FFT$ 。
定義 $solve(L,R)$ 為“在 $L$ 之前的 $a_i$ 都已經得到答案的基礎上完成 $L$ ~ $R$ 的計算”。
於是,我們可以寫出下面的這個偽代碼:
過程 solve(L,R) |----mid←(L+R)/2 |----solve(L,mid) |----FFT(a[L...mid]×k[1...R-L]→a[mid+1...R]) |----solve(mid+1,R) 過程結束
其中在寫代碼的時候邊界情況可能會有所不同。
但是讀者請注意,上面 $FFT()$ 里處理的不是右區間受到的全部貢獻,只是完成了左區間對於右區間的貢獻,事實上,一個 $a_i$ 會分成大約 $O(\log n)$ 次來貢獻。
拆系數$FFT$和三模數$NTT$
毛嘯2016的集訓隊論文有寫,本節內容許多摘自他的論文,讀者可以直接查閱他的論文。
經典的 $FFT$ 的雖然很好用,但是在一些數據范圍較大的題目之下,還是要掛。
當兩個長度為 $10^5$ 級別的序列卷積,模數為 $10^9$ 級別(不為 $NTT$ 模數),直接 $FFT$ 的話,每個數的結果大約在 $10^{23}$ 左右,超出了 $2^{64}$ ,一方面會使浮點數出現較大的誤差,一方面你也不可能把他轉成 $64$ 位整形然后取模,這個時候就要用下面的兩種方法了。
拆系數$FFT$
我們設 $M$ 為模數的大小,則取 $M_0=\left\lceil\sqrt{M}\ \right\rceil$ ,根據帶余除法將所有的 $x$ 表示成 $x=k[x]M_0+b[x]$ 。其中 $k[x]$ 和 $b[x]$ 為整數。
我們假設多項式 $A(x)$ 的系數序列為 $a_i$ ,多項式 $B(x)$ 的系數序列為 $b_i$ ,那么我們把 $k[a_i],b[a_i],k[b_i],b[b_i]$ 形成的四個序列兩兩卷積,卷積結果大約在 $10^{14}$ 級別,可以接受。並先取個模,然后乘上相應的系數,一起加到答案里面就可以了。這樣要 $7$ 次 $(I)DFT$ 。通過 myy 論文里面講的優化方法可以優化到 $4$ 次甚至 $3.5$ 次$DFT$。
三模數$NTT$
我們找 $3$ 個大小在 $10^9$ 級別的 $NTT$ 模數。然后分別在這三個模數意義下求卷積結果,然后中國剩余定理合並即可。
具體方法:我們假設模數分別是 $mod_0,mod_1,mod_2$ ,先合並前兩個模數,也就是求出答案在模 $mod_0\times mod_1$ 意義下的值,然后用逆元將模 $mod_0\times mod_1\times mod_2$ 意義下的數,也就是答案表示成 $k\times mod_0\times mod_1+b$ 的形式,這個東西我們不必真正求出,我們只需要在模 $M$ 意義下求即可,這樣只需要使用 $64$ 位整型即可。
但這個需要 $9$ 次 $DFT$ 效率沒有上面的那個快。
(而且博主太菜了沒寫過,目前只寫過 $9$ 次 $DFT$ 的拆系數 $FFT$ (截至2018-04-17))
套路與例題
以下例題的鏈接是詳細題解,題解里面有題目鏈接
套路1 - 字符串匹配
當我們從母串的某一個位置開始和模式串匹配的時候:
首先,我們發現需要匹配的字符對的下標差會是定值,所以我們一般會把其中一個字符串進行翻轉,因為翻轉之后,需要匹配的字符對的下標之和為定值,這樣才能滿足卷積形式。(下面是翻轉字符串之后,匹配連線的示意圖)
放例題:
(注意,在此之后,我默認你已經能對是否翻轉有敏感的認識)
BZOJ4053 兩個串
題意
給定兩個字符串 S 和 T ,回答 T 在 S 中出現了幾次,在哪些位置出現。注意 T 中可能有 ? 字符,可以匹配任何字符。串長 $\leq 10^5$
提示
考慮到有通配符 $?$ ,我們不能直接 KMP 。
但是我們可以通過構造卷積,通過判斷卷積結果的某一位是否為 0 來確定某一個位置開始是否可以匹配。
可以從相同字符值差為 0 以及通配符與其他字符永遠匹配方面來考慮。
第一道題,可以看看題解。
BZOJ4259 殘缺的字符串
題意
給你兩個串,用其中一個來匹配另一個。問從母串的那些位置開始可以匹配模式串。注意有"*"可以匹配任何字符。
串長 $\leq 3\times 10^5$ 。
提示
和上一題唯一的區別就是兩個串中都有通配符。基本上一樣的。只是要稍微卡下時間和空間。
CodeForces 528D Fuzzy Search
題意
給你兩個串 $A,B(|A|\geq|B|)$ ,以及一個 $k$ 。
其中 $A_i$ 與 $B_j$ 匹配的條件是 $A_{i-k\dots i+k}$ 中至少有一個與 $B_j$ 相同。
問 $B$ 能在 $A$ 中匹配多少次。
字符集: $\{'A','C','G','T'\}$ 。
$|B|\leq|A|\leq 2\times 10^5,k\leq 2\times 10^5$
提示
可以預處理每一個位置可以匹配到 $\{'A','C','G','T'\}$ 的哪些。
然后,如果按照之前兩題的套路來構造卷積,先不談常數巨大,手工展開都很困難。
注意到字符集非常小,我們可以考慮對於每一個字符分開考慮,構造卷積。
套路2 - 卷積形式變形
BZOJ3527 [ZJOI2014]力
題意
給出長度為 $m$ 的序列 $q_{1..m}$ ,讓你輸出長度為 $m$ 的序列 $E_{1..m}$ 。
其中:
$$E_i=\sum_{j=1}^{i-1}\frac{q_j}{(i-j)^2}-\sum_{j=i+1}^{m}\frac{q_j}{(i-j)^2}$$
提示
將原式寫成兩個卷積式。其中一個很容易 $FFT$ ,另外一個可以通過翻轉和更改求和指標等一系列推導變成 $FFT$ 擅長的卷積形式。
套路3 - 背包問題相關
有時候,我們會碰到類似無限背包的問題。給定一些物品的體積,問用這些物品可以拼出某個范圍內的哪些體積。
構造多項式:如果有體積為 $i$ 的物品,則 $i$ 次項系數為 $1$ ,否則為 $0$ 。特別的,我們手工添加一個 $0$ 體積的物品。然后多項式平方一下,有值的位置所代表的體積就是可以通過至多 $2$ 個體積值相加得到。然后我們順手把所有有值的改成 $1$ 。再平方,得到的是至多 $4$ 個體積值相加得到的體積。平方 $k$ 次,就能得到至多 $2^k$ 個物品體積相加可以得到的所有體積。復雜度 $O(n\log^2 n)$ 。其中 $n$ 為體積范圍。
CodeForces 268E Ladies' Shop
題意
首先,給你 $n$ 個數(並告訴你 $m$ ),分別為 $p_{1\dots n}$ 。
讓你求一個數的集合,滿足:
當且僅當從這個數的集合中取數(可以重復)求和時(設得到的和為 $sum$ ),如果 $sum\leq m$ ,則數 $sum$ 在給你的 $n$ 個數之中。
如果沒有這種集合,輸出 $NO$ 。
否則,先輸出 $YES$ ,然后輸出這個集合最小時的元素個數,並輸出集合中的所有元素。
$1\leq n,m\leq 10^6,1\leq p_i\leq 10^6$
提示
考慮思考本題的特殊性,在本題之前的小例子的基礎上,舍去無用的運算。
套路4 - 分治$FFT$
BZOJ4836 [Lydsy1704月賽]二元運算
題意
定義二元運算 $opt$ 滿足
$$x\ opt\ y=\begin{cases}x+y & \text{$(x<y)$} \\ x-y & \text{$(x\geq y)$}\end{cases}$$
現在給定一個長為 $n$ 的數列 $a$ 和一個長為 $m$ 的數列 $b$ ,接下來有 $q$ 次詢問。每次詢問給定一個數字 $c$ 你需要求出有多少對 $(i, j)$ 使得 $a_i\ opt\ b_j=c$ 。
提示
在分治 $a_i$ 的值域的時候,左右區間內的數會滿足左區間嚴格小於右區間,這是個很好的性質,會便於你按照上面的式子分類貢獻, $FFT$ 優化。
CodeForces 553E Kyoya and Train
題意
一個有 $n$ 個節點 $m$ 條邊的有向圖,每條邊連接了 $a_i$ 和 $b_i$ ,花費為 $c_i$ 。
每次經過某一條邊就要花費該邊的 $c_i$ 。
第 $i$ 條邊耗時為 $j$ 的概率為 $p_{i,j}$ 。
現在你從 $1$ 開始走到 $n$ ,如果你在 $t$ 單位時間內(包括 $t$ )到了 $n$ ,不需要任何額外花費,否則你要額外花費 $x$ 。
問你在最優策略下的期望花費最小為多少。
(注意你每走一步都會根據當前情況制定最好的下一步)
提示
本題是 myy 的論文題,思維含量較大。
首先我告訴你 $O(mT\log^2 T)$ 的復雜度可以過。
先考慮 $DP$ ,然后通過設一個期望貢獻累加器,來化簡 $DP$ 轉移方程,並從中挖掘 $FFT$ 擅長的卷積形式,並通過分治 $FFT$ 優化。
雜題
BZOJ3160 萬徑人蹤滅
題意
給你一個只含 $a,b$ 的字符串,讓你選擇一個子序列,使得:
$1.$ 位置和字符都關於某一條對稱軸對稱。
$2.$ 不能是連續的一段。
問原來的字符串中能找出多少個這樣的子序列。答案對 $10^9+7$ 取模。
串長 $\leq 10^5$ 。
提示
先避開條件2考慮如何解題。考慮一個點為中心,在其兩側能互相匹配的字符對數。每一對可以互相匹配的都可以選擇選或者不選。從中尋找卷積形式。
對於不滿足2的,顯然是連續回文子串個數, $Manachar$ 裸題。
BZOJ4451 [Cerc2015]Frightful Formula
題意
給你一個 $n\times n$ 矩陣的第一行和第一列,其余的數通過如下公式推出:
$$f_{i,j}=a\cdot f_{i,j-1}+b\cdot f_{i-1,j}+c$$
求 $f_{n,n}\mod (10^6+3)$ 。提示
考慮每一個格子各自對於 $f_{n,n}$ 的貢獻。
對於除了第一行和第一列的格子,性質相似,可以列出求和式子。再通過推導,尋找利於 $FFT$ 的卷積形式。
BZOJ4827 [Hnoi2017]禮物
題意
有兩個長為 $n$ 的序列 $x$ 和 $y$ ,序列 $x,y$ 的第 $i$ 項分別是 $x_i,y_i$ 。
選擇一個序列 $A$ ,現在你可以對它進行如下兩種操作:
$1.$ 得到一個和 $A$ 循環同構的序列 $A'$ 。
$2.$ 給所有的 $A'_i$ 都加上 $c(c\in N^+)$ ,得到序列 $A''$ 。
你進行上面兩個操作之后,得到的序列分別為 $x'',y''$ (注意 $x,y$ 兩個序列中至少有一個序列沒有發生任何變化)
序列 $x''$ 和 $y''$ 的差異值為
$$\sum_{i=1}^{n}(x''_i-y''_i)^2$$
問差異值最小為多少。
提示
考慮先寫出一個一般的結果式子,然后略微展開,得到一些常數,一個關於 $c$ 的二次函數和一個卷積式。
對於二次函數我們求一下最值即可。
對於卷積式,我們考慮求其最值。先倍長某一個串,再翻轉某一個串, $FFT$ 優化,計算出你需要的東西。
CodeForces 958F3 Lightsabers (hard)
題意
有 $n$ 個球,球有 $m$ 種顏色,分別編號為 $1\cdots m$ ,現在讓你從中拿 $k$ 個球,問拿到的球的顏色所構成的可重集合有多少種不同的可能。
注意同種顏色球是等價的,但是兩個顏色為 $x$ 的球不等價於一個。
$1\leq n\leq 2\times 10^5,\ \ \ \ \ 1\leq m,k\leq n$。
提示
一道比較新的題目,是我寫這篇博文前幾天的 $CodeForces$ 上的 $ACM$ 比賽題。
考慮構造一些小的多項式,然后把他們全部乘起來得到最終的解。
需要分治或者啟發式合並優化。建議啟發式合並。
CodeForces 623E Transforming Sequence
題意
給定 $n,k$ 。
讓你構造序列 $a(0<a_i<2^k)$ ,滿足 $b_i(b_i=a_1\ or\ a_2\ or\ \cdots\ or\ a_i)$ 嚴格單調遞增。( $or$ 為按位或)
問你方案總數。對 $10^9+7$ 取模。
$n\leq 10^{18},k\leq 30000$
提示
又是一道 myy 論文題。
思維含量也挺大的。
先考慮暴力 $DP$ ,然后考慮加大轉移的步長,從已經得到的 $dp$ 值中狀態轉移得到新的 $dp$ 值。需要尋找你得到的加大步長后的 $dp$ 轉移方程的利於 $FFT$ 的卷積形式,然后倍增 $FFT$ 優化。
參考文章與博客&鳴謝
(特別鳴謝)http://blog.miskcoo.com/2015/04/polynomial-multiplication-and-fast-fourier-transform#comment-37058
2016國家隊候選隊員論文 - 毛嘯 - 再探快速傅里葉變換
《多項式導論》 - picks
https://oi.men.ci/fft-notes/
http://picks.logdown.com/posts/177631-fast-fourier-transform
http://picks.logdown.com/posts/247168-fast-fourier-transform-modulo-prime
后記
寫了好幾天真累啊。感謝所有給我提供幫助的文章、博客,以及寫它們的人,以及讀完這篇學習筆記、看到這里的你。
希望這篇博文能帶給您幫助。
由於博主學識短淺,如果您在閱讀的過程中發現任何錯誤,麻煩您在百忙之中給我留言指出,謝謝。
當然,多項式的運用遠不止於此。關於多項式求逆、多項式除法、多項式開根、多項式 exp/ln 、多項式求導/求不定積分、牛頓迭代、泰勒展開等等,也許我會陸續推出關於這些算法學習筆記,敬請期待。
UPD(2018-04-18 15:00):自行驗稿一遍,修改了約 10 處細節錯誤,比如空格沒打或者同於打了等於這類的,以及一處 $DFT$ 寫成了 $FFT$ ,均已修改。
UPD(2018-04-19 15:15):發現有一個題意概括里的細節錯誤,已經改正。
UPD(2018-04-20 20:06):感謝 Emoairx 指出,博主當時手殘了,把拉格朗日插值法的復雜度寫錯了。現在已經修改。
UPD(2018-07-15 20:24):修正 3 處錯誤。
UPD(2018-09-23 18:45):補上了一個漏打的字