FFT算法详解
看到 FFT,肯定有很多人崩溃了,其实没那么难,很容易就能懂。首先,我们需要了解复数。
众所周知,\(\sqrt 1=\pm 1,\sqrt 4=\pm 2\)。那么,\(\sqrt {-1}\) 是多少呢?肯定不是 \(1\),因为 \(1^2=1\),当然也不是 \(-1\),因为 \((-1)^2=1\)。所以我们约定 \(i\) 表示 \(\sqrt {-1}\)。那么,\(\sqrt {-4}=\pm 2i\)。一个复数就形如 \(a+bi\),它的共轭复数是 \(a-bi\)。
我们先看看,复数的四则运算分别长啥样。
加法:\((a+bi)+(c+di)=(a+c)+(b+d)i\),很简单。
减法:\((a+bi)-(c+di)=(a-c)+(b-d)i\),也很简单。
乘法:\((a+bi) \times (c+di)\) 是什么呢?可以用乘法分配律拆开,\((a+bi) \times (c+di)=a \times c+a \times di+bi \times c+bi \times di=(a \times c-b \times d)+(a \times d+b \times c)i=(ac-bd)+(ad+bc)i\)。
除法:因为分母是复数,所以我们需要把它乘一个共轭的复数,根据平方差公式,分母就变成了实数,然后就很简单了。\(\frac{a+bi}{c+di}=\frac{(a+bi)(c-di)}{(c+di)(c-di)}=\frac{(ac+bd)+(bc-ad)i}{c^2+d^2}=\frac{ac+bd}{c^2+d^2}+\frac{bc-ad}{c^2+d^2}i\)。
整数可以用数轴表示,复数呢?因为它是二维的,所以可以用一个平面表示,也就是复平面。\(a+bi\) 在复平面上所表示的点是 \((a,b)\),这个复数的模长是它到原点 \((0,0)\) 的距离,也就是 \(\sqrt {a^2+b^2}\),幅角是 \((a,b)\) 到 \((0,0)\) 这条线段的底角。两个复数相乘,规则是:模长相乘,幅角相加,接下来我们证明。
模长相乘:\(\sqrt {{(ac-bd)}^2+{(ad+bc)}^2}=\sqrt {a^2c^2-2abcd+b^2d^2+a^2d^2+b^2c^2+2abcd}=\sqrt {a^2c^2+a^2d^2+b^2c^2+b^2d^2}=\sqrt {a^2+b^2} \times \sqrt {c^2+d^2}\)。
幅角相加:这需要用到三角函数的和角公式,也就是 \(\cos(a+b)=cos(a) \times cos(b)-sin(a) \times sin(b)\),代入一下即可。
那么,\(1\) 的 \(n\) 次方根分别是多少呢?一个模长的 \(n\) 次方是 \(1\),它只能是 \(1\),所以我们可以以原点为圆心,\(1\) 为半径长度,画一个圆。\(1\) 的 \(n\) 次方根也可以叫做 \(n\) 次单位根。那么,这些单位根肯定在圆上,那么具体是多少呢?需要看幅角。
我们用 \(\omega_a^b\) 表示第 \(b\) 个 \(a\) 次单位根,其中第几个从 \(0\) 开始编号。
那么,\(\omega_a^b \times \omega_a^c=\omega_a^{b+c},\omega_a^{a/2}=-1\)。
一个幅角 \(\times n\) 是 \(360^{\circ}\)。那么这个幅角的度数肯定是 \(360^{\circ} \div n\) 的整倍数,所以一共有 \(n\) 个 \(n\) 次单位根。
讨论这么多关于复数的内容,该进入正题了!
首先,FFT 是干什么用的?它可以快速求多项式乘法。多项式指的是形如 \(\sum\limits_{i=0}^{n-1}x^i \times f_i\),其中 \(x\) 只是一个符号,并不是一个数。可以设两个乘数为 \(f,g\),答案是 \(h\)。我们可以把 \(n\) 设为 \(2^k\),其中 \(k\) 自己定,那么剩余的空间我们可以用 \(0\) 代替。
那么,多项式乘法就是求 \((\sum\limits_{i=0}^{n-1}x^i \times f_i)(\sum\limits_{i=0}^{n-1}x^i \times g_i)\),所以答案应该是 \(\sum\limits_{i=0}^{n-1}({\sum\limits_{j=0}^{i}f_j \times g_{i-j}}) \times x^i\)。如果枚举 \(i\),再枚举 \(j\),就超时了,所以我们需要优化。
多项式乘法属于卷积,也就是对于一个 \(i\),那么 \(h_i\) 是 \(\sum f_j \times g_k\) 的形式。如果没有这个求和,叫做点积,也就是 \(h_i=f_i \times g_i\),那么线性复杂度就能解决了。所以我们需要想办法把卷积的形式变成点积,怎么做呢?一个 \(n-1\) 次函数可以用 \(n\) 个点来确定,所以 \(f,g\) 也可以用 \(n\) 个点确定。但是,如果随便代入 \(n\) 个值进入多项式,肯定还是会很慢,所以应该用关系密切的 \(n\) 个点,当然单位根是最好不过的选择了!
所以问题转换成了求 \(f',g'\),显然 \(f'_i=\sum\limits_{j=0}^{n-1}(\omega_n^i)^j \times f_j\),\(g'\) 同理。其实,我们可以把这个多项式拆成两部分,也就是 \(j\) 是偶数一部分,奇数一部分。其中 \(f^e,f^o\) 分别代表偶数的多项式和奇数的多项式。易得 \(f^e_i=\sum\limits_{j=0}^{\frac{n}{2}-1}(\omega_{\frac{n}{2}}^i)^j \times f_{2 \times j}\),\(f^o_i\) 同理。则 \(f'=f^e+\omega_{n}^1 \times f^o\),那么我们能不能把这 \(f^e,f^o\) 交给下一层去做,这一层把它求个和呢?当然可以,我们是用分治实现的。
所以我们分这几步实现:
第一步:把 \(f\) 数组拆分成两个子数组,一个是奇数位的,一个是偶数位的。
第二步:进行下一层操作。
第三步:把 \(f^e\) 和 \(f^o\) 合并,得到 \(f'\)。
这样 \(f'\) 和 \(g'\) 就求完了,所以 \(h'\) 也很容易求得。但是,我只知道 \(h'\) 不行,需要得到 \(h\)