多项式
对于多项式$ f\left(x\right)=\sum_{i=0}^{|f|}{f_ix^i} $,其中|f|表示多项式的阶数,fi表示多项式f中x^i的系数。
多项式的加法定义为$ c\left(x\right)=a\left(x\right)+b\left(x\right)=\sum_{i=0}^{\max\left(|a|,|b|\right)}{\left(a_i+b_i\right)x^i} $,即$ c_k=a_k+b_k $。
多项式的乘法定义为$ c\left(x\right)=a\left(x\right)\cdot b\left(x\right)=\sum_{k=0}^{|a|+|b|}{\left(\sum_{i+j=k}^{}{a_ib_j}\right)x^k} $,即$ c_k=\sum_{i+j=k}^{}{a_ib_j} $。
显然要计算两个多项式a(x),b(x)的乘积,程序的时间复杂度为O(|a||b|)。
naive(a, b) c = 0 for(i = 0; i <= |a|; i++) for(j = 0; j <= |b|; j++) c[i + j] = c[i + j] + a[i] * b[j]
在多项式阶数超过10w的时候,这个方法就完全顶不住了。不过幸好还有很多加快多项式乘运算速度的算法,而快速傅立叶变换就是其中之一。
先了解一下多项式的其它操作的时间复杂度:多项式的乘法虽然很慢,但是求解一个多项式f在x=x0的时候的取值f(x0)是可以在O(|f|)时间复杂度内做到的。以及多项式的加法a(x)+b(x)也可以在O(max(|a|,|b|))的时间复杂度内做到。
再了解一下多项式的表示方法:多项式的表示方法基本有两种,一种是通过系数序列(f0,f1,...,fk)来表示一个k阶多项式f,这种方法称为系数表示法,还有一种就是点值表示法,即用k+1个不同的点来表示一个k阶多项式。系数表示法大家都很了解,下面说一下点值表示法。
在代数中,说明过n个不同的点可以唯一确定一个k阶多项式,其中k<n。而如果从n个点还原出原来的多项式,则需要使用到插值算法,著名的插值算法有拉格朗日插值法和牛顿插值法,这里不多加介绍,二者的时间复杂度均为O(n^2)。
点值表示法非常适合用于计算多项式乘法,对于多项式乘法c(x)=a(x)*b(x),假设我们已经确认了|a|+|b|+1个不同的x值x0,x1,...,且分别计算出了a(x0),b(x0),a(x1),b(x1),...,那么我们就得知点(xi,a(xi)b(xi))是多项式c上的点,而这组点的数目为|a|+|b|+1>|c|,故c被这组点唯一确认,在前提下多项式乘法可以以O(|c|)的时间复杂度运行。
当然点值表达式的前提并不好满足,我们往往需要先通过插值取回多项式,之后再计算在额外的点的多项式值,之后再利用点值乘法算出新的多项式的点值表达式。这整个过程的时间复杂度为O(|c|^2)。
我们设n为大于等于c长度的最小2的幂次(即n=2^k>=|c|>2^(k-1)),在运算多项式乘法前,我们先将a与b通过前面补0将长度扩充到n,之后再运行多项式乘法。下面我们说明如何在O(nlog2n)的时间复杂度内将长度为n的多项式从系数表达式转换为点值表达式,并在O(nlog2n)的时间复杂度内将n个不同的点插值会系数表达式的多项式,而这一算法就是快速傅立叶变换,很显然这一过程的时间复杂度为O(nlog2n+n+nlog2n)=O(nlog2n)。
单位复数根
首先我们要谨慎地选取n个点的x值。在复数域中,n次单位复数根是满足w^n=1的所有复数w。由欧拉公式$ e^{iu}=\cos\left(u\right)+i\sin\left(u\right) $可知n次复数根分别为复数$ e^{\left(2\pi k/n\right)i} $,其中k分别取值0,1,...,n-1的,我们记为w(n,0),w(n,1),...,w(n,n-1)。很显然w(n,i)w(n,j)=w(n,i+j),由于w(n,0)=w(n,n)=1,故我们得知w(n,i)=w(n,n-i)=w(n,i-n)。下面说明这n个复数的有趣性质:
消去引理:w(dn,dk)=w(n,k)。
证明:略
折半引理:2n次单位复数根的平方组成的集合与n次单位复数根组成的集合相同。
证明:对于0<=k<n,$ w\left(2n,k\right)^2=\left(e^{\left(2\pi k/2n\right)i}\right)^2=e^{\left(2\pi k/n\right)i}=w\left(n,k\right) $。
对于n<=k<2n,由于w(2n,n)=-1(看欧拉公式),故$ w\left(2n,k\right)^2=\left(-w\left(2n,k-n\right)\right)^2=w\left(2n,k-n\right)^2=w\left(n,k-n\right) $。
求和引理:对于任意n>=1和不能被n整除的非负整数k,有$ \sum_{j=0}^{n-1}{w\left(n,k\right)^j}=0 $。
证明:$ \sum_{j=0}^{n-1}{w\left(n,k\right)^j}=\frac{w\left(n,k\right)^n-1}{w\left(n,k\right)-1}=\frac{1-1}{w\left(n,k\right)-1}=0 $。
离散傅立叶变换DFT
对于一个长度为n的多项式f(x),其在n个n次单位复数根w(n,0),w(n,1),...,w(n,n-1)上的取值组成的序列y=(f(w(n,0)),f(w(n,1)),...,f(w(n,n-1)))称为f的离散傅立叶变换(DFT),也记作y=DFT(f)。很显然DFT(f)可以唯一确定f。
要计算DFT(f),我们可以分治策略。
我们将f切分为长度为n/2的两个多项式even和odd,其中even中仅包含多项式的偶数项系数,而odd中仅包含奇数项系数:
even=f0*x^0+f2*x^1+f4*x^2+...+fn-2x^(n/2-1)
odd=f1*x^0+f3*x^1+...+fn-1x^(n/2-1)
而很显然f=even(x^2)+x*odd(x^2)。因此要计算f在w(n,0),w(n,1),...,w(n,n-1)上的取值,只需要计算even和odd在w(n,0)^2,w(n,1)^2,...,w(n,n-1)^2上的取值即可。由折半引理可知{w(n,0)^2,w(n,1)^2,...,w(n,n-1)^2}={w(n/2,0),w(n/2,1),...,w(n/2,n/2-1)},故我们实际上要计算的仅为DFT(even)和DFT(odd)。在得到DFT(even)和DFT(odd)后,仅需使用O(n)的时间复杂度即可算出DFT(f)。
我们记T(f)表示DFT(f)的时间复杂度,则T(f)=O(n)+2T(f/2)=...=O(kn)+(2^k)*T(f/(2^k))=...=O(nlog2(n))。
DFT(f) n = f.length
if(n == 1)
return f[0]
y = empty-array even = 0 odd = 0 for(i = 0; i < n / 2; i++) even[i] = f[i * 2] odd[i] = f[i * 2 + 1] yEven = DFT(even) yOdd = DFT(odd) wn1 = e^((2*PI/n)i) w = 1 for(i = 0; i < n / 2; i++) y[i]= yEven[i] + w * yOdd[i] y[i + n / 2] = yEven[i] - w * yOdd[i] w = w * wn1 return y
由于计算机计算正弦和余弦函数要花费大量的时间,因此可以将wn1作为参数传入,而在计算DFT(even)时,将wn1*wn1作为参数传入即可(w(n,1)^2=w(n/2,1)),这样可以节省时间。
离散傅立叶逆变换IDFT
离散傅立叶逆变换IDFT将多项式从点值表达式转换为系数表达式。即IDFT(DFT(c))=c。观察下面等式:
$$ \left[\begin{array}{c} y_0\\ y_1\\ \vdots\\ y_{n-1} \end{array}\right]=\left[\begin{matrix} w\left(n,0\right)^0 & w\left(n,0\right)^1 &\cdots & w\left(n,0\right)^{n-1}\\ w\left(n,1\right)^0 & w\left(n,1\right)^1 &\cdots & w\left(n,1\right)^{n-1}\\ \vdots &\vdots &\ddots &\vdots\\ w\left(n,n-1\right)^0 & w\left(n,n-1\right)^1 &\cdots & w\left(n,n-1\right)^{n-1} \end{matrix}\right]\left[\begin{array}{c} c_0\\ c_1\\ \vdots\\ c_{n-1} \end{array}\right] $$
等式左边为DFT(c),而做右边的向量为c,方阵为范德蒙德矩阵,由于w(n,0),...,w(n,n-1)互不相同(欧拉公式),故方阵可逆。我们将等式简记y=Mc。记IM=M^(-1)。
定理:IM的j'行j列元素为IMj'j=w(n,-j'j)/n
证明:记P=IM·M,显然$ P_{j'j}=\sum_{k=0}^{n-1}{w\left(n,-j'k\right)/n\cdot w\left(n,kj\right)}=\frac{1}{n}\sum_{k=0}^{n-1}{w\left(n,k\right)^{j-j'}} $。当j等于j'时,Pj'j=n/n=1,而当j不等于j'时,由求和引理可知Pj'j=0。故P是单位矩阵,因此IM=M^(-1)。
现在我们可以利用c=IM·y来计算c了。观察矩阵下隐含的等式关系:
$$ c_j=\sum_{k=0}^{n-1}{y_k\cdot w\left(n,-jk\right)/n}=\frac{1}{n}\sum_{k=0}^{n-1}{y_kw\left(n,n-j\right)^k} $$
这给了我们一个启发,c和DFT(y)/n中包含的值是相同的,只是顺序不同而已。因此我们可以利用DFT以及一些常数时间的操作实现IDFT。
IDFT(y) c = 0 n = y.length dftY = DFT(y) c[0] = dftY[0] / n for(i = 1; i < n; i++) c[i] = dftY[n - i] / n return c
IDFT和DFT共享相同的时间复杂度O(nlog2n)。
快速傅立叶变换FFT
快速傅立叶变换利用DFT和IDFT计算两个多项式a(x)和b(x)的乘积。
FFT的第一步首先是找到一个合适的二次幂n,并将a和b通过添加0系数项的方式扩展到长度n。之后计算DFT(a)和DFT(b),在利用点乘计算DFT(c)=DFT(a)·DFT(b)。最后利用IDFT从点值表达式DFT(c)复原出多项式c,即c=FFT(a,b)=IDFT(DFT(a)·DFT(b))。
显然FFT的时间复杂度为DFT和IDFT的总和,也是O(nlog2n)。而由于n<2*|c|=2*(|a|+|b|),因此我们可以忽略n与|c|之间的误差,即时间复杂度可以写作O(|c|log2|c|)。是相当优秀的时间复杂度。
FFT(a, b) n = 1 while(n < |a| + |b|) n = n * 2 extend(a, n) extend(b, n) return IDFT(DFT(a)·DFT(b))
非递归版本DFT
实际中,如果你直接使用上面的快速傅立叶变换,你将会发现性能相当不理想。我们仔细观察一下FFT的流程,我们能发现我们总共做了三次DFT计算(其中一次在调用IDFT中),和一次点乘运算,其中点乘运算是线性时间复杂度,而DFT的时间复杂度为O(nlogn)。如果我们能优化DFT,那么FFT的性能将得到最大幅度的提升。
那么DFT的缺陷在哪里,我们很容易发现每次我们都需要建立一个多项式y,以保存最终结果,我们完全可以在FFT中复制多项式a,b,之后在DFT中将结果直接写入到f中并作为返回值。之后我们还可以发现,每次用f调用DFT,需要创建两个子多项式odd和even,其长度总和为|f|。因此我们总共分配的多项式长度为O(nlog2n),这是相当大的空间复杂度。而如果我们能不用分配even和odd,那么自然能达到优化DFT的结果。
观察对于多项式(a0,a1,a2,a3,a4,a5,a6,a7)调用DFT的过程:
(a0,a1,a2,a3,a4,a5,a6,a7)
| \
(a0,a2,a4,a8) (a1,a3,a5,a7)
| \ | \
(a0,a4) (a2,a8) (a1,a5) (a3,a7)
| \ | \ | \ | \
a0 a4 a2 a8 a1 a5 a3 a7
如果我们能将多项式(a0,a1,a2,a3,a4,a5,a6,a7)在DFT的一开始转换为(a0,a4,a2,a8,a1,a5,a3,a7),那么我们就可以用非递归的方式实现DFT,一开始将长度为1的两个相邻多项式合并,之后将长度为2的两个相邻多项式合并,再将长度为4两个相邻多项式合并。
观察序列,能发现以下规律,下标二进制最低位为0的排在二进制最低位为1的之前。如果最低位相同,则用相同逻辑判断次低位。容易发现这相当于比较两个整数二进制(总共log2n位)逆序的大小。因此如果我们能一开始处理得到n个数值的二进制序列,之后利用逆序对多项式各位进行重排列,那么就能实现上述的非递归版本的DFT。
而要计算所有长度为m的二进制序列的所有逆序,可以用下面的方法:
reverse(m) n = 2^m r = int[n] r[0] = 0 for(i = 1; i < n; i++) r[i] = (r[i >> 1] >> 1) | ((1 & i) << (m -1)) return r
其中我们使用了一个聪明的想法,i的二进制逆序,我们可以先将i右移1位得到j,由于j<i,我们保证r[j]已经得到,而r[j]作为j的逆序,等价于将i的逆序r[i]左移了一位,我们通过对r[j]再右移一位就可以得到r[i]的后面m-1位,但是首位始终为0。考虑到首位正好是i的最低位,我们可以将i的最低位向左移动m-1位于r[j]>>1左位或处理,从而得到r[i]。很容易得出reverse的时间复杂度为O(n)。
最后给出非递归版本的DFT:
DFT(p, m) r = reverse(m) n = 2^m for(i = 0; i < n; i++) if(r[i] > i) swap(p, i, r[i]) for(d = 0; d < m; d++) s = 2^d w1 = complex(cos(PI/s),sin(PI/s)) for(i = 0; i < n; i += 2*s) w = 1 for(j = 0; j < s; j++) a = i + j b = a + s t = w * p[b] q = p[a] p[b] = q - t p[a] = q + t w = w * w1
其中r=reverse(m)这个步骤我们可以提前执行。以及w1 = complex(cos(PI/s),sin(PI/s))中由于涉及到了cos和sin的计算,由于s始终是2的幂,因此可能的值非常少,也可以一开始就计算好。整个DFT中我们没有分配额外的空间,因此空间复杂度可以认作为O(n),而时间复杂度不变为O(nlog2n)。