Skip to content

先来回忆一下高精度乘法的原理

对于正整数相乘AB, 将A视为a0+a1101+a2102+...+an10n, B同理为b0+b1101+b2102+...+bn10m, 将A * B视作多项式相乘, 得到C的多项式形式为c0+c1101+c2102+...+cn+m10n+m, 然后逐一进位, 即可得到C的值。

其中, ck=i=0naibki

这种方法的时间复杂度为O(nm)级别, 而FFT可以以O(nlogn)的复杂度计算得到多项式乘积。

多项式的点值表示

n次多项式f(x)=a0x0+...+an1x(n1)+anxn

由线性代数知识可知, 若知道了n+1组不同的[x, f(x)]的关系, 即可得到一个n+1 x n+1的多元线性方程组, 且这个方程组有唯一解。

也就是说, 对于一个n次多项式f(x), 我们可以用{(x0,y0),(x1,y1),(x2,y2),...,(xn,yn)}来定义它, 其中yk=f(xk)

n次多项式f(x)m次多项式g(x), 由于f(x)g(x)的结果最多是n + m次, 所以我们将两个多项式进行零填充到n + m次, 此时如果

f(x)={(x0,f(x0)),(x1,f(x1)),...,(xn+m,f(xn+m))}g(x)={(x0,g(x0)),(x1,g(x1)),...,(xn+m,g(xn+m))}

f(x)g(x)={(x0,f(x0)g(x0)),(x1,f(x1)g(x1)),...,(xn+m,f(xn+m)g(xn+m)}

可以看到, 在点值形式下, 计算f(x)g(x)仅需O(n+m), 如果我们能快速地将多项式转换为点值形式, 计算后, 再将结果快速地转换为系数形式, 就能以较低的时间复杂度完成多项式的计算。

离散傅里叶变换(DFT)

我们要将n次多项式f(x)转换为点值形式, 该怎么做呢?

可以任意取n + 1个x值, 带入进行计算, 但这样时间复杂度是O(n2)

数学家傅里叶取了一组特殊的点带入, 使得其可以进行优化。

单位根

若复数ω满足ωn=1, 则称ω为n次单位根。

将单位圆n等分, 即可得到圆上的n个坐标点,而这n个坐标点表示的复数都是n次单位根。其中第k个n次单位根称为ωnk, 特变的, ωn0=ωnn=1, 即(1, 0)为起点, 逆时针. 易得ωnk=cos2kπn+isin2kπn

单位根的性质

在本文中需要用到单位根的如下性质(均可通过展开式推导得出):

  • ωnkωnm=ωnk+m

  • ωrnrk=ωnk

  • ωnk+n2=ωnk


而离散傅里叶变换就是一组单位根作为x值, 但如果还是直接代入计算的话, 复杂度仍然没有变化。

快速傅里叶变换(FFT)

快速傅里叶变换是一种能在计算机中快速地计算离散傅里叶变换的算法。

FFT的基本思想是分治。

n-1阶多项式f(x)=i=0n1aixi,且n为2的整数幂(不够则零填充),上面我们知道要求f(x)的点值形式, 也就是对每一个k求出f(ωnk)

f(x)按奇数次幂和偶数次幂分为两部分:

f(x)=a0+a1x+...+an1xn1=(a0+a2x2+...+an2xn2)+(a1x+a3x3+...+an1xn1)

g(x)=a0+a2x1+...+an2xn21h(x)=a1+a3x+...+an1xn21

f(x)=g(x2)+xh(x2)

代入x=ωnk

f(ωnk)=g((ωnk)2)+ωnkh((ωnk)2)=g(ωn2k)+ωnkh(ωn2k)=g(ωn2k)+ωnkh(ωn2k)

f(ωnk+n2)=f(ωnk)=g((ωnk)2)ωnkh((ωnk)2)=g(ωn2k)ωnkh(ωn2k)

故求出f(ωnk)f(ωnk+n2)只需要先求出g(ωn2k)h(ωn2k).

假设我们已经求出了gh{ωn20,ωn21,...,ωn2n21}的值, 就可以求出所有的f的点值表示。

gh的问题规模都是f的一半, 所以时间复杂度为O(nlog2n)

离散傅里叶逆变换(IDFT)

用矩阵乘法表示DFT的过程为

[11111ωn1(ωn1)2(ωn1)n11ωn2(ωn2)2(ωn2)n11ωnn1(ωnn1)2(ωnn1)n1][a0a1a2an1]=[y0y1y2yn1]

FFT结束后我们得到了最右边的结果, 所以要得到系数矩阵只需要让结果乘以中间大矩阵的逆矩阵即可。

怎么求这个矩阵的逆矩阵呢?这里的结论是每一项取倒数,再除以多项式的长度n, 至于证明可以参见快速傅里叶变换(FFT)超详解 - 知乎 (zhihu.com)

快速傅里叶逆变换(IFFT)

结果矩阵乘以逆矩阵这个过程显然和FFT的过程很相似,考虑怎么转化。

观察f(x)=g(x2)+xh(x2), 要求f(x)的IFFT, 首先求出g(x2)h(x2)的IFFT, 且将x取反, 最后加和后再除以n, 即可完成。

所以IFFT和FFT可以公用一套代码, 只需要用一个标志位来控制关键位置的取反。

代码实现

c++
//
// Created by trudbot on 2023/7/14.
//
#include "complex"

//sign为1时是FFT, -1是IFFT
//n必须为2的整数次幂
const double pi = acos(-1);
void FFT(std::complex<double> *a, int n, int sign) {
    if (n == 1) return;
    std::complex<double> g[n / 2], h[n / 2];
    for (int i = 0, j = 0; i < n; i += 2, j ++) {
        g[j] = a[i], h[j] = a[i + 1];
    }
    FFT(g, n / 2, sign), FFT(h, n / 2, sign);
    std::complex<double> w(1, 0), u(cos(2 * pi) / n, sign * sin(2 * pi / n));
    for (int i = 0; i < n / 2; i ++, w *= u) {
        a[i] = g[i] + w * h[i];
        a[i + n / 2] = g[i] - w * h[i];
    }
}

void DFT(std::complex<double> *a, int n) {
    FFT(a, n, 1);
}

void IDFT(std::complex<double> *a, int n) {
    FFT(a, n, -1);
    for (int i = 0; i < n; i ++) {
        a[i] /= n;
    }
}

参考

超详细易懂FFT(快速傅里叶变换)及代码实现_Trilarflagz的博客-CSDN博客

快速傅里叶变换(FFT)超详解 - 知乎 (zhihu.com)

傅里叶变换与大数乘法 - Free_Open - 博客园 (cnblogs.com)

如何通俗易懂地解释卷积? - 知乎 (zhihu.com)

如何通俗地解释什么是离散傅里叶变换? - 知乎 (zhihu.com)

快速傅里叶变换 - OI Wiki (oi-wiki.org)

欧拉公式_百度百科 (baidu.com)

如何用latex编写矩阵(包括各类复杂、大型矩阵)? - 知乎 (zhihu.com)