che dan环节:
概述
快速傅里叶变换(Fast Fourier Transformation),简称\(FFT\),它可以在\(O(n \ log \ n)\)的时间内算两个多项式的乘积的所有系数。
是一个听起来逼格很高,实际逼格更高的算法。
一些定义
下面我们来交代一些奇怪的东西。
一个\(n\)项\(n-1\)次多项式,\(F(x) = a_0x^0 + a_1x^1 + \dots + a_{n-1}x^{n-1}\)
则多项式\(F(x)\)可以用 __系数表示法__表示成\(F(x) = \{ a_0,a_1,\dots,a_{n-1} \}\)
方便的,我们可以把\(F\)看成一个数组,用\(F[k]\)表示多项式\(F\)的\(k\)次项系数,即\(F[k] = a_k\),如果\(F\)不存在\(k\)次项的话系数就是零即\(F[k] = 0\)。
那么多项式\(F\)(\(n\)次),\(G\)(\(m\)次)的乘积会得到一个\(n + m\)次多项式\(H\)就满足\(H[k] = \sum\limits_{i = 0}^{k} F[i]G[k-i]\),按这样算多项式乘法就是\(O(n^2)\)的。
如果您是个明白人的话会发现多项式乘法就是一个加法卷积(当然如果不知道也没关系)。
多项式还有一个比较有用的表示法叫 点值表示法,就是把多项式看做函数。
记一个结论:用\(n\)个点能确定一个\(n-1\)次多项式 意会,意会~。
即代\(n\)个\(x\)到\(F(x)\)里,就可以确定这个多项式$ F(x)\(的\)n$个系数(不要忘了常数项)。
假设我们代入的是\(\{ x_0,x_1, \dots ,x_{n-1} \}\),令\(y_i = F(x_i)\)
那么\(F(x)\)就可以表示为\(F(x) = \{ (x_0,y_0),(x_1,y_1), \dots,(x_{n-1},y_{n-1}) \}\)
总结
总结一下,多项式\(F(x) = a_0x^0 + a_1x^1 + \dots + a_{n-1}x^{n-1}\)有两种表示方法
- 系数表示法:\(F(x) = \{ a_0,a_1,\dots,a_{n-1} \}\)
- 点值表示法:即把\(F(x)\)看做一个函数,用\(n\)个\(x\)就可以确定\(F(x)\),\(F(x) = \{ (x_0,y_0),(x_1,y_1), \dots,(x_{n-1},y_{n-1}) \}\)
那这些有什么用呢?我们用系数表示法做乘法的复杂度是铁定\(n\)方的。
那用点值表示法呢?
如果要计算\(F(x) \cdot G(x)\)(\(F\)有\(n\)次,\(G\)有\(m\)次)的话,我们先把同一组\(\{ x_i : 0 \le i \le n+m \}\)(注意是\(n + m + 1\)个\(x\),实际上多代了也不亏。)代入\(F,G\),那么设\(H(x) = F(x) \cdot G(x)\),多项式\(H\)的点值表示法就是
\[
H = \{ (x_0, F(x_0) \cdot G(x_0)),\dots,(x_{n+m},F(x_{n+m})\cdot G(x_{n+m})) \}
\]
这样求出\(F\)和\(G\)的点至表示法后我们就可以通过\(O(m +n)\)的算出\(H\)的点值表示。
然后呢?然后啥啊。。。
我们会得到$n + m + 1 $个点,但我们要算各项的系数啊。
那咋算呢?\(FFT\)就是做的这个。
提前交代一下\(FFT\)的流程
- \(1.\) 先分别算出两个多项式的点值表示法(我们称之为
绝活\(DFT\)) - \(2.\) 把这些对应点分别乘起来,得到乘积多项式\(H\)的点值表示法
- \(3.\) 再把\(H\)的点值表示法转回系数表示法(即\(DFT\)的逆变换,我们叫它\(IDFT\))
前置姿势
复数
定义
形如\(z = a + bi\)的数我们管他叫复数,其中\(i = \sqrt{-1}\),我们称为虚数单位,实数\(a\)称为\(z\)的实部,实数\(b\)称为\(z\)的虚部。
一个复数\(z = a + bi\)可以用复平面上的一个点\((a,b)\)表示。
复平面:
可以这么理解,把他当成直角坐标系的话,上面的一点\((x,y)\)就对应了一个复数\(x + iy\)。
原来直角坐标系下的\(x\)轴在复平面下对应实轴(上面的数表示实部\(a\)),\(y\)轴上的数对应虚部\(b\),我们叫虚轴。
模长:
复数所对应的点即到点\((0,0)\)的距离,记为\(|z|\)。
辐角:
复数\(a + bi\)的辐角即实轴按逆时针方向旋转到点\((a,b)\)的旋转角度,类似直角坐标系下的极角。
记为\(arg (z)\)
运算
加减法:
这个应该很好理解吧,实部虚部分别加减就好了,即\((a + bi) \pm (c+di) = (a\pm c) + (b\pm d)i\),
乘法
恩,这个比较重要。
可以暴力的乘起来:\((a + bi)(c + di) = ac + adi + cbi + bdi^2 = (ac - bd) + (ad + cb)i\)(因为\(i^2 = -1\))。
即点\((a,b)\)与点\((c,d)\)通过乘法得到点\((ac - bd,ad + cb)\)
再记个结论:
设\(z = z_0 \times z_1\),则\(|z| = |z_0| \cdot |z_1|,arg(z) = arg(z_0) + arg(z_1)\)
即 复数乘法,模长相乘,辐角相加(这玩意儿非常有用)
Code
这是复数运算的一些\(Code\),并没有涉及到复数的除法,因为我不会\(FFT\)并不会用除法。
struct cpl{ db x,y; cpl operator + (cpl k1)const{return (cpl){x+k1.x,y+k1.y};} cpl operator - (cpl k1)const{return (cpl){x-k1.x,y-k1.y};} cpl operator * (cpl k1)const{return (cpl){x*k1.x-y*k1.y,x*k1.y+y*k1.x};} }
单位根
这个有点东西啊。
定义
可以理解为\(x^n = 1\)这个方程的根,我们称之为 \(n\)次单位根。
在实数域中,这个方程当\(n\)为奇数时有一个解\(x = 1\),\(n\)为偶数时有两个解\(x_0 = 1, x_1 =-1\)
那扩展到复数域呢?
在复平面下,复数的乘法有一个非常\(nb\)的性质,叫 模长相乘,辐角相加(不知道的话百度一下吧!)
那么能成为\(x^n = 1\)这个方程的解的复数\(x\),有一个前提条件叫\(|x | = 1\)(\(x\)的模长为\(1\))。
那么单位根就只会在复平面的单位圆上(蛤?啥是单位圆)。
上图(单位圆):
还有一点叫 辐角相加,那么\(n\)次单位根就一定是单位圆周上的\(n\)个\(n\)等分点。
因为这样的点辐角一定可以表示为\(\frac{2 \pi}{n} \cdot k\)(弧度制),自乘\(n\)次后辐角为\(2 \pi k\),恰好绕圆周\(k\)圈后到点\((1,0)\)
那么就表明\(n\)次单位根有且仅有\(n\)个,即单位圆周上的\(n\)等分点,一般的,我们把\(n\)次单位根记为\(\omega_{n}^0,\omega_{n}^{1},\dots,\omega_{n}^{n-1}\)
举个例子,\(n = 4\)时,有\(4\)个单位根,如图
性质
\((1)\) \(\omega_{n}^{0} = 1\),显然。
\((2)\) \(\omega_{n}^{k} = \left( \omega_{n}^{1} \right) ^ k\)更一般的\((\omega_{n}^{k}) ^ i = \omega_{n}^{ik}\),结合复数乘法的性质,辐角相加,也非常显然。
\((3)\) \(\omega_{n}^{i} \cdot \omega_{n}^{j} = \omega_{n}^{i + j}\)
由\((2)\)知\(\omega_{n}^{i} = \left( \omega_{n}^{1} \right)^i, \omega_{n}^{j} = \left( \omega_{n}^{1} \right)^j\),则\(\omega_{n}^{i} \cdot \omega_{n}^{j} = \left ( \omega_{n}^{1} \right)^{i+j} = \omega_{n}^{i + j}\)
\((4)\) \(\omega_{n}^{k} = \omega_{n}^{k \ mod \ n}\),即绕了一圈又回来了。那么\(\omega_{n}^{-k} = \omega_{n}^{n-k}\)。
- \((5)\) 如果\(n\)是偶数,\(\omega_{n}^{k+n/2} = -\omega_{n}^{k}\),跑了半个圆周,刚好到相反方向。
\((6)\) \(\omega_{mn}^{mk} = \omega_{n}^{k}\),这个也挺显然的。
还有一些等用到了再说吧(其实鉴于复数乘法的优秀特性,这些性质都比较显然)。
好啦,前置姿势胡完了!
坐稳了!
DFT & IDFT
下面终于要讲一些有用的东西了。。。
DFT
上文也说了,\(DFT\)可以快速求多项式\(F(x) = a_0x_0 + a_1x_1 + \dots + a_{n-1}x^{n-1}\)(下面我们特别的规定\(n\)能表示成\(2^k\))的一组点值表示。
上古时期,有一位名叫傅里叶的大佬,他发现把\(\omega_{n}^{0},\dots,\omega_{n}^{n-1}\)代入\(F(x)\),鉴于单位根的一些性质,可以很快的完成\(DFT\)求出一组\(F(x)\)的点值表达。
为了方便,我们令\(DFT(F(x)) = \{ (x_0,y_0),(x_1,y_1), \dots,(x_{n-1},y_{n-1}) \}\),其中$x_i = \omega_{n}^{i},y_i = F(x_i) $
我们也把\(F(x)\)的一个\(DFT\)看做一个数组,并设\(G = DFT(F)\),有\(G[k] = y_k\)
傅里叶先把\(F(x)\)按次数奇偶分组,得到两个小一点大小均为\(n/2\)的多项式
\[
f(x) = F[0]x_0 + F[2]x^1 + \cdots + F[n-2]x^{n/2-1}
\]
\[ g(x) = F[1]x_0 + F[3]x^1 + \dots + F[n-1]x^{n/2-1} \]
有\(F(x) = f(x^2) + x\cdot g(x^2)\)。
发现如果\(x = \omega_{n}^{k} (k < n/2)\)的话
\[
\begin{aligned}F(x) &= f((\omega_{n}^{k}) ^ 2) + \omega_{n}^{k} g((\omega_{n}^{k}) ^ 2) \\&= f(\omega_{n/2}^{k}) + \omega_{n}^{k} g(\omega_{n/2}^{k})\end{aligned}
\]
我们设\(f' = DFT(f), g' = DFT(g)\),那么有\(G[k] = f'[k] + \omega_{n}^{k}g'[k] (k<n/2)\)
那如果代入\(x = \omega_{n}^{k + n/2}(k < n/2)\)呢?
\[
\begin{aligned}F(x) &= f((\omega_{n}^{k+n/2}) ^ 2) + \omega_{n}^{k+n/2} g((\omega_{n}^{k+n/2}) ^ 2) \\&= f(\omega_{n/2}^{k+n/2}) + \omega_{n}^{k+n/2} g(\omega_{n/2}^{k+n/2}) \\&= f(\omega_{n/2}^{k}) - \omega_{n}^{k}g(\omega_{n/2}^{k})\end{aligned}
\]
换句话说\(G[k+n/2] = f'[k] - \omega_{n}^{k}g'[k] (k < n/2)\)。
那么我们就有了两个柿子\(k < n/2\):
\[
G[k] = f'[k] + \omega_{n}^{k}g'[k]
\]
\[ G[k+n/2] = f'[k] - \omega_{n}^{k}g'[k] \]
我们只需要求出\(f'[0...n/2-1]\)和\(g'[0...n/2-1]\)就可以算出\(G[0...n]\)了。
这样我们就可以分治算\(G\)就可以了。
啊,忘了。可以用三角函数来算出\(\omega_{n}^{1}\),然后自乘\(k\)次就可以得到\(\omega_{n}^{k}\)了。
Code
所以代码:
#include <bits/stdc++.h> using namespace std; typedef double db; const db PI=acos(-1.0); const int N=4e6+10; struct cpl{ db x,y; cpl operator + (cpl k1)const{return (cpl){x+k1.x,y+k1.y};} cpl operator - (cpl k1)const{return (cpl){x-k1.x,y-k1.y};} cpl operator * (cpl k1)const{return (cpl){x*k1.x-y*k1.y,x*k1.y+y*k1.x};} }buf[N]; void dft(cpl *f,int n){ if (n==1)return; for (int i=0;i<n;i++) buf[i]=f[i]; // 别忘了保存下来。 cpl *L=f,*R=f+(n>>1); // 直接用f的空间。 for (int i=0;i<(n>>1);i++) L[i]=buf[i<<1],R[i]=buf[i<<1|1]; dft(L,n>>1),dft(R,n>>1); cpl w=(cpl){1,0},w1=(cpl){cos(2*PI/n),sin(2*PI/n)}; // w是不是很像$\omega$? for (int i=0;i<(n>>1);i++){ // 上文中的k buf[i]=L[i]+w*R[i]; buf[i+(n>>1)]=L[i]-w*R[i]; w=w*w1; } for (int i=0;i<n;i++) f[i]=buf[i]; // 复制回来。 } cpl f[N]; int main(){ int n; scanf("%d",&n); // n次n-1项柿。 for (int i=0;i<n;i++) scanf("%lf",&f[i].x); // 各项系数。 int limit=1; while (limit<n) limit<<=1; // 注意补成n=2^k的形式 dft(f,limit); for (int i=0;i<n;i++) printf("(%.3f,%.3f)\n",f[i].x,f[i].y); return 0; }
IDFT
但只有\(DFT\)还是不够啊。。。我们还要把它再搞回系数表示。
于是就有了\(DFT\)的逆变换$IDFT $。
记个结论:
\(DFT\)那里我们已经知道了\(G = DFT(F)\),且\(G[k] = \sum\limits_{i = 0}^{n-1} (\omega_{n}^{k})^i F[i]\)。
结论就是\(n \cdot F[k] = \sum_{i=0}^{n-1} (\omega_{n}^{-k})^iG[i]\),证明还是要有滴~
等式右边
\[
\begin{aligned}\sum_{i=0}^{n-1} (\omega_{n}^{-k})^iG[i]&= \sum\limits_{i = 0} ^ {n - 1} \omega_{n}^{-ki} \sum\limits_{j = 0}^{n-1} (\omega_{n}^{i}) ^ j F[j] \\&= \sum\limits_{i = 0} ^ {n - 1} \omega_{n}^{-ki} \sum\limits_{j = 0}^{n-1} \omega_{n}^{ji} F[j] \\&= \sum\limits_{j = 0} ^ {n - 1} F[j] \sum\limits_{i=0}^{n-1} (\omega_{n}^{j-k})^i\end{aligned}
\]
分类讨论一下
\(j = k\)时,因为\(\omega_{n}^{0} = 1\),所以那一项的贡献就是
\[
F[j] \sum\limits_{i=0}^{n-1} (\omega_{n}^{j-k})^i = F[j] \cdot n \cdot 1 = n F[k]
\]
\(j \not = k\)呢?观察到第二个\(\sum\)是个公比为\(\omega_{n}^{j-k}\)的等比数列。
求和一下
\[
\sum\limits_{i=0}^{n-1} (\omega_{n}^{j-k})^i = \frac{(\omega_{n}^{j-k})^n - (\omega_{n}^{j-k})^0}{\omega_{n}^{j-k} - 1}
\]
由于\((\omega_{n}^{j-k})^n = \omega_{n}^{n(j-k)} = \omega_{n}^{0} = 1,(\omega_{n}^{j-k})^0 = 1\),所以分子是\(0\),这一项的贡献也是\(0\)。
因此\(n \cdot F[k] = \sum_{i=0}^{n-1} (\omega_{n}^{-k})^iG[i]\)。
这东西的本质好像是什么 复读机单位根反演?不会。。。
这玩意儿直接在$DFT $的代码中改一个符号就好了。
Code
void idft(cpl *f,int n){ if (n==1)return; for (int i=0;i<n;i++) buf[i]=f[i]; // 别忘了保存下来。 cpl *L=f,*R=f+(n>>1); // 直接用f的空间。 for (int i=0;i<(n>>1);i++) L[i]=buf[i<<1],R[i]=buf[i<<1|1]; idft(L,n>>1),idft(R,n>>1); cpl w=(cpl){1,0},w1=(cpl){cos(2*PI/n), - sin(2*PI/n)}; // w是不是很像$\omega$? // 就是这个负号! for (int i=0;i<(n>>1);i++){ buf[i]=L[i]+w*R[i]; buf[i+(n>>1)]=L[i]-w*R[i]; w=w*w1; } for (int i=0;i<n;i++) f[i]=buf[i]; // 复制回来。 }
main
里面别忘了\(IDFT\)后把每一项的值除以一个\(n\)。
Code
其实我们完全可以把\(DFT \& IDFT\)写一起,这样我们就得到了板子题的代码。
#include <bits/stdc++.h> using namespace std; typedef double db; const db PI=acos(-1.0); const int N=4e6+10; struct cpl{ db x,y; cpl operator + (cpl k1)const{return (cpl){x+k1.x,y+k1.y};} cpl operator - (cpl k1)const{return (cpl){x-k1.x,y-k1.y};} cpl operator * (cpl k1)const{return (cpl){x*k1.x-y*k1.y,x*k1.y+y*k1.x};} }buf[N]; void fft(cpl *f,int n,int k1){ // k1==1 ? DFT : IDFT if (n==1)return; for (int i=0;i<n;i++) buf[i]=f[i]; cpl *L=f,*R=f+(n>>1); for (int i=0;i<(n>>1);i++) L[i]=buf[i<<1],R[i]=buf[i<<1|1]; fft(L,n>>1,k1),fft(R,n>>1,k1); cpl w=(cpl){1,0},w1=(cpl){cos(2*PI/n),k1*sin(2*PI/n)}; for (int i=0;i<(n>>1);i++){ buf[i]=L[i]+w*R[i]; buf[i+(n>>1)]=L[i]-w*R[i]; w=w*w1; } for (int i=0;i<n;i++) f[i]=buf[i]; } cpl f[N],g[N]; int main(){ int n,m; scanf("%d%d",&n,&m); for (int i=0;i<=n;i++) scanf("%lf",&f[i].x); for (int i=0;i<=m;i++) scanf("%lf",&g[i].x); int limit=1; while (limit<=n+m)limit<<=1; // 补项 fft(f,limit,1),fft(g,limit,1); // DFT for (int i=0;i<limit;i++) f[i]=f[i]*g[i]; // 点值相乘,求出答案的点值表示。 fft(f,limit,-1); // IDFT回系数表示 for (int i=0;i<=m+n;i++) printf("%d ",(int)(f[i].x/limit+0.5)); // 别忘了除以n return 0; }
然后愉快的交一发:23333
淦。。。我写了一个假的\(FFT\)......
常数优化
我\(O(n \ log \ n)\)的那么优秀的复杂度啊!咋\(T\)了呢?
冷静分析.jpg......
注意到板子题里\(n \le 10^6\),再加上我们要做两边\(DFT\)和一边\(IDFT\),每次复杂度是\(n \ log \ n\)的,还有系统栈的巨大常数。。。
好吧,下面我们来卡卡常。
首先观察我们\(DFT \& IDFT\)的过程。
第一层:0 1 2 3 4 5 6 7 (f数组的下标) 第二层:0 2 4 6|1 3 5 7 第三层:0 4|2 6|1 5|3 7 第四层:0|4|2|6|1|5|3|7
发现原来\(f\)数组中的第\(i\)个数会出现在最后一层中第(\(i\)的二进制翻转)个数中。
举个例子,\(1:(001)_2\)出现在了\(4: (100)_2\)上,\(3: (011)_2\)出现在了\(6:(110)_2\)上。
这样我们就可以把最后一层的情况求出来,然后再递归分治。
那怎么求最后一层呢?也就是求\(i\)的二进制翻转。(这个好像叫蝴蝶变换)
设补完项后的多项式长度为limit
,令rev[i]
表示\(i\)的二进制翻转,则rev[i]=rev[i>>1]>>1|((i&1)?limit>>1:0)
,即不看\(i\)的最低位,把i>>1
翻转过来后再把\(i\)的最低位拼上去。另外我们还能省几个数组。
#include <bits/stdc++.h> using namespace std; const int N=4e6+10; typedef double db; const db PI=acos(-1.0); struct cpl{ db x,y; cpl(db xx=0,db yy=0):x(xx),y(yy){} cpl operator + (cpl k1)const{return cpl(x+k1.x,y+k1.y);} cpl operator - (cpl k1)const{return cpl(x-k1.x,y-k1.y);} cpl operator * (cpl k1)const{return cpl(x*k1.x-y*k1.y,x*k1.y+y*k1.x);} }; void fft(cpl *f,int n,int k1){ if (n==1) return; fft(f,n>>1,k1),fft(f+(n>>1),n>>1,k1); cpl w(1,0),w1(cos(2*PI/n),sin(2*PI/n)*k1); for (int i=0;i<(n>>1);i++){ cpl tmp=w*f[i+(n>>1)]; f[i+(n>>1)]=f[i]-tmp; // 注意这里的赋值顺序! f[i]=f[i]+tmp; w=w*w1; } } int rev[N]; void change(cpl *f,int n){ for (int i=0;i<n;i++) if (i<rev[i]) swap(f[i],f[rev[i]]); } cpl f[N],g[N]; int main(){ int n,m; scanf("%d%d",&n,&m); for (int i=0;i<=n;i++) scanf("%lf",&f[i].x); for (int i=0;i<=m;i++) scanf("%lf",&g[i].x); int limit=1,len=0; while (limit<=n+m) limit<<=1,len++; for (int i=0;i<limit;i++) rev[i]=rev[i>>1]>>1|((i&1)?limit>>1:0); change(f,len),change(g,len); fft(f,limit,1),fft(g,limit,1); for (int i=0;i<limit;i++) f[i]=f[i]*g[i]; change(f,len),fft(f,limit,-1); for (int i=0;i<=n+m;i++) printf("%d ",(int)(f[i].x/limit+0.5)); return 0; }
欸!那我们为什么不能直接自底向上合并呢?这样还省去了系统栈的常数。
最终Code
#include <bits/stdc++.h> using namespace std; const int N=4e6+10; typedef double db; const db PI=acos(-1.0); struct cpl{ db x,y; cpl operator + (cpl k1)const{return (cpl){x+k1.x,y+k1.y};} cpl operator - (cpl k1)const{return (cpl){x-k1.x,y-k1.y};} cpl operator * (cpl k1)const{return (cpl){x*k1.x-y*k1.y,x*k1.y+y*k1.x};} }; int rev[N]; void fft(cpl *f,int n,int k1){ for (int i=0;i<n;i++) if (rev[i]>i)swap(f[rev[i]],f[i]); for (int len=2;len<=n;len<<=1){ cpl w1=(cpl){cos(2*PI/len),sin(2*PI/len)*k1}; for (int i=0;i<n;i+=len){ cpl w=(cpl){1,0}; for (int j=i;j<i+(len>>1);j++){ cpl tmp=w*f[j+(len>>1)]; f[j+(len>>1)]=f[j]-tmp; f[j]=f[j]+tmp; w=w*w1; } } } } cpl f[N],g[N]; int main(){ int n,m; scanf("%d%d",&n,&m); for (int i=0;i<=n;i++) scanf("%lf",&f[i].x); for (int i=0;i<=m;i++) scanf("%lf",&g[i].x); int limit=1; for(;limit<=n+m;limit<<=1); for (int i=0;i<limit;i++) rev[i]=rev[i>>1]>>1|((i&1)?limit>>1:0); fft(f,limit,1),fft(g,limit,1); for (int i=0;i<limit;i++) f[i]=f[i]*g[i]; fft(f,limit,-1); for (int i=0;i<=n+m;i++) printf("%d ",(int)(f[i].x/limit+0.5)); return 0; }
这样我们就能过模板题了。
来源:https://www.cnblogs.com/wxq1229/p/12232247.html