By Long Luo
之前的文章 傅里叶变换(Fourier Transform) 详细介绍了傅里叶变换 是什么和做什么,相信你已经感受到了傅里叶变换的强大之处。
但理论要联系实际,今天我们就来学习 快速傅里叶变换 的实际运用:多项式乘法。
快速傅里叶变换 是一种可在 时间内完成的 离散傅里叶变换 算法。
FFT可以做什么?
傅里叶变换 本质上是信号与三角函数进行卷积运算,而快速傅里叶变换 就是提高卷积的计算效率,时间复杂度从 降低到 。
在算法中的运用主要是用来加速多项式乘法或大数乘法。
多项式乘法
正如之前的文章 卷积(Convolution) 所说,多项式乘法也是一种卷积运算。
在计算中,泰勒级数 可以使用多项式函数来逼近一个函数,所以计算中多项式乘法非常重要。
大数乘法
超大数字乘法,可以参考 超大数字的四则运算是如何实现的呢? 。朴素的算法就是列竖式做乘法,算法时间复杂度为 ,如果数字太大的话,效率也不够高,如果应用 则可以使算法时间复杂度降至 。
不妨设十进制数字 ,很容易知道:
令 ,则可以转化为:
所以大数乘法就是 情况下的多项式乘法!
那下面我们就以多项式乘法的为例来学习快速傅里叶变换 具体是如何做的。
多项式
在学习多项式乘法之前,我们需要先学习下有关多项式的知识。
多项式有两种表示方法: 系数表示法与点值表示法。
系数表示法
设多项式 为一个 项 次的多项式,显然,所有项的系数组成的系数向量 唯一确定了这个多项式。
点值表示法
点值表示法是把这个多项式看成一个函数,从其中选取 个不同的点,从而利用这 个点来唯一地表示这个函数。
设
那么用点值表示法表示 如下:
为什么用 个不同点就能唯一地表示一个 次函数?
证明如下:
两点确定一条直线。再来一个点,能确定这个直线中的另一个参数,那么也就是说 个点能确定 个参数(不考虑倍数点之类的没用点)。
假设原命题不成立,则存在两个不同的 次多项式函数 , ,那么 , 有 个交点,即任何 ,有 。
令 ,则 也是一个 次多项式。对于任何 ,都有 。
即 有 个根,这与代数基本定理(一个 次多项式在复数域上有且仅有 个根)相矛盾,故 并不是一个 次多项式,推导矛盾。
故原命题成立。
多项式乘法
考虑两个多项式 , ,其乘积 。
假设 的项数为 ,其系数构成的 维向量为 ; 的项数为 ,其系数构成的 维向量为 。
我们要求 的系数构成的 维的向量,先考虑暴力做法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public int[] mutply(int[] A, int[] B) { int n = A.length; int m = B.length;
int[] C = new int[n + m];
for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { C[i + j] += A[i] * B[j]; } }
return C; }
|
可见时间复杂度是 。
如何加速多项式乘法?
实际运用中多项式的项数非常多,比如 这种级别,那么有没有什么方法可以加速运算呢?
已知在一组插值节点 中 , (假设多项式的项数相同,没有则视为 。)的点值向量分别为 , ,则:
那么 ,那么其点值表示法可以在 的时间内求出:
多项式乘法的系数表示法的时间复杂度是 ,而点值表示法的时间复杂度是 。
其实可以观察到,多项式乘法的系数表示法就是做卷积运算,而点值表示法是做乘法运算。卷积定理 告诉我们在一个域中的卷积相当于另一个域中的乘积。
还记得傅立叶变换的本质就是从另外一个维度看待世界吗?
在这里,系数表示法就是时域上表达,很复杂,而点值表示法就是频域上表示,就非常简单。那么我们将其转换为频域上表示,就可以大大降低其复杂度!
但在得到了 的点值表达式之后,还是不行,因为我们要的是系数表达式。
接下来问题变成了从点值回到系数。如果我们带入到高斯消元法的方程组中去,会把复杂度变得非常高。光是计算 就是 项,这就已经 了,更别说还要把 个方程进行消元…
不过我们暂时忽略如何将点值表达式变成系数表达式,关注点放在乘法运算上。因为系数表达式和点值表达式运算复杂度差距如此之大,如果能在运算之前把多项式变成点值表示法,做完乘法之后,再将点值表示法变成系数表示法不就可以大大提高效率了吗?
点值表示法 和 系数表示法 如何转换?
想象有个魔法黑匣子,可以实现多项式的点值表示法和系数表示法的互换。
每次运算前,我们先向黑匣子里输入 , 系数表达式,黑匣子内部先将 , 都变成点值表达式,黑匣子进行乘法运算之后,再在内部转换为系数表达式,返回给我们,不就行了吗?
所以这个黑匣子里面是什么?能不能实现这个黑匣子呢?
其实这个魔法黑匣子的本质就是我们今天要研究的快速傅里叶变换 ,其思路就是由系数表达式到点值表达式,生成结果的点值表达式,再将点值表达式转换为结果的系数表达式。
上一章讲到了一个魔法黑匣子可以实现多项式的点值表达式和系数表达式的转换,这一章我们就来研究如何将多项式系数表达式转换为点值表达式。
DFT
考虑一个 项( )的多项式 ,其系数向量为 :
将 次单位根的 次幂分别带入 得到其点值向量 。
这个过程称为离散傅里叶变换 。
如果朴素代入计算,时间复杂度也是 ,所以我们必须要利用到单位根 的特殊性质以减少运算。
奇偶次幂分组
对于 ,
将其按照奇偶次幂分组:
令 , ,
那么易得: 。
分类讨论
- 当 ,代入单位根 ,则:
由上文提到的折半引理
- 当时,
其中 。
由消去引理 ,
那么:
注意: 与 取遍了 中的 个整数,保证了可以由这 个点值反推解出系数。
DFT小结
综合这两个式子,如果知道 , 分别在 处的点值,就可以 的时间内求出 在 处的点值。
, 都是 一半的规模,可以转化为子问题递归求解。
所以时间复杂度:
所以明白在一开始的时候 吧?
分治 能处理的多项式长度只能是 ,否则在分治的时候左右不一样长。
上一节我们讲了离散傅里叶变换 ,即实现了魔法黑匣子的一半,实现了多项式系数表达式转换成点值表达式,那么这一章我们就来实现魔法黑匣子的另一半,将多项式点值表达式转化为系数表达式。
将点值表达式的多项式转化为系数表达式,这个过程叫做离散傅里叶逆变换 。
IDFT求解过程
问题:
对于多项式 ,已知 个点,其 维点值向量为 ,请求解其 维系数向量 ?
- 设 为 得到的离散傅里叶变换的结果。
构造一个多项式:
- 设向量 ,其中 为 在 的点值表示,即 。
如何得到?
因为
由和式的性质
令 ,对其进行化简:
设 ,则:
可见 构成等比数列,其公比为 。
当 即 时, 此时 ;
当 即 时,由等比数列求和公式:
,此时 。
综合可得: 。
求出
将 带入原式:
所以: ,最终我们得到了原多项式 的系数向量 中的 。
小结
对于多项式 由插值节点 做离散傅里叶变换得到的点值向量 。
将 作为插值节点, 作为系数向量,做一次离散傅里叶变换得到的向量每一项都除以 之后得到的 就是多项式的系数向量 。
注意: 是 的共轭复数。
通过这个过程我们就实现了将点值转换为系数表示。
FFT的本质
通过上面两节的 和 ,我们实现了魔法黑匣子的功能,也就是实现了多项式的点值表达式和系数表达式的互相转换。
下面进行总结:
从时域到频域:DFT
:
从时域到频域:IDFT
:
矩阵运算
我们可以将 写成矩阵乘法的形式:
其中的 矩阵叫做 Vandermonde Matrix 。
已知 点值表达式,也就是已知 和 ,求 的系数向量 , 。
的 逆矩阵(Inverse Matrix) 为:
因此我们可以得到以下公式:
的获取:
注意到我们只要对矩阵 中每一个元素取共轭复数并除以 ,就得到了其 逆矩阵 。
FFT 代码实现
上面讲了这么多,下面开始编码实现吧:-)
Complex Number
复数可以使用C++ STL自带的 ,依照精度要求 一般为 ,也可以自己封装实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| struct Complex { double x, y;
Complex(double _x = 0.0, double _y = 0.0) { x = _x; y = _y; }
Complex operator-(const Complex &b) const { return Complex(x - b.x, y - b.y); }
Complex operator+(const Complex &b) const { return Complex(x + b.x, y + b.y); }
Complex operator*(const Complex &b) const { return Complex(x * b.x - y * b.y, x * b.y + y * b.x); } };
|
FFT 递归写法
我们可以按照我们刚才的推导,得到递归形式写法,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| const double PI = acos(-1.0);
vector<complex<double>> FFT(vector<complex<double>> &a, bool invert) { int n = a.size();
if (n == 1) { return a; }
vector<complex<double>> Pe(n / 2), Po(n / 2);
for (int i = 0; 2 * i < n; i++) { Pe[i] = a[2 * i]; Po[i] = a[2 * i + 1]; }
vector<complex<double>> ye = FFT(Pe, invert); vector<complex<double>> yo = FFT(Po, invert);
vector<complex<double>> y(n);
double ang = 2 * PI / n * (invert ? -1 : 1); complex<double> omega(cos(ang), sin(ang)); complex<double> curRoot(1, 0);
for (int i = 0; i < n / 2; i++) { y[i] = ye[i] + curRoot * yo[i]; y[i + n / 2] = ye[i] - curRoot * yo[i]; curRoot *= omega; }
return y; }
|
由于 和 操作流都是系数不一致,所以我们可以将其写成一个函数。值得注意的是, 之后需要都系数除于 才是最终的结果。
复杂度分析
总结
跨度一年,终于把这个 的坑填完了,虽然大学时学《信号与系统》时知道了 ,但真正理解 还是要等到写完了这几篇文章。
下一篇讲聚焦于 的优化!
文章如有错误之处,敬请批评指正。
参考资料
预览: