FFT 学习笔记

应用

FFT,中文“快速傅里叶变换”,用来加速多项式乘法和卷积,可以将 O(n2) 的复杂度优化至 O(nlogn)

多项式

系数表示法

一个 nn+1 项的多项式 f(x) 可以表示为 f(x)=i=0naixi

也可以用每一项的系数表示 f(x),即 f(x)={a0,a1,,an}。这种表示方法称为系数表示法.

点值表示法

把多项式 f(x) 看成一个平面直角坐标系中的函数 y=f(x),再代入 n+1不同x 得到 n+1y,那么这 n 个点就可以唯一确定多项式 f(x),即有且仅有一个多项式 f(x),满足 i[0,n],f(xi)=yi。因为可以看成是一个 n+1n+1 次方程组,一定能解出该方程组。

那么 f(x) 还可以用 f(x)={(x0,f(x0)),(x1,f(x1)),,(xn,f(xn))} 表示,这就是点值表示法。

点值表示法与系数表示法的互相转化

  • 对于一个 n 次多项式,在已知系数的情况下,将其转化为点值表示法,称为 DFT(离散傅里叶变换)。
    n+1x 分别代入计算,每次 O(n),总复杂度是 O(n2)

  • 对于一个由点值表示法表示的 n 次多项式,将其转化为系数表示法,称为 IDFT(离散傅里叶逆变换)。
    即解一个 n+1n+1 次的方程组,使用高斯消元是 O(n3) 的,可以用拉格朗日插值做到 O(n2)

两种表示法在多项式乘法上的区别

  • 对于两个用系数表示法表示的多项式 A(x),B(x),需要分别枚举两个多项式的次数再将系数相乘,系数表示法做多项式乘法的复杂度为 O(n2)

  • 对于两个用点值表示法表示的多项式:

    f(x)={(x0,f(x0)),,(xn,f(xn))},g(x)={(x0,g(x0)),,(xn,g(xn))}

    两者相乘的结果记为 h(x),则:

    h(x)={(x0,f(x0)g(x0)),,(xn,f(xn)g(xn))}

    时间复杂度 O(n)

那么将系数表示法转化为点值,做乘法后再转换为系数表示法会不会更快呢?在使用 DFTIDFT 的情况下显然是不会的,因为转化是 O(n2) 的。如果想要更快,就需要 FFT 了。

快速傅里叶变换

复数

复数基础详见高中数学必修二第七章。

复数可以写成 a+bi 的形式,在复平面上表示为 (a,b)

在快速傅里叶变换中,我们考虑更换点值表示法中代入的 x,使得能通过 x 的一些性质减少部分运算。而这些 x 即为 xn=1n 个根,根据高中数学知识,这些根的分布情况应为:(n=8

即这 n 个点与原点的线段平分了单位圆,将编号为 1 的根称作 xn=1 的单位复数根,记为 ωn,根据欧拉公式,有:

eiθ=cosθ+isinθ

ωn=ei(2π/n),其它根为 ωn 的若干次幂。

选择这 n 个点作为点值转化原因是它们满足以下两点性质。

  • 消去引理:nN,kN,dN,有 ωdndk=ωnk

    证明:

    ωdndk=(ei(2π/dn))dk=(ei(2π/n))k=ωnk

  • 折半引理:nN,kNn 为偶数,有 (ωnk+n/2)2=ωn/2k

    证明:

    (ωnk+n/2)2=ωn2k+n=ωn2k=ωn/2k

FFT

考虑将 ωn0,ωn1,,ωnn1 代入求值。

(由于下面的推导需要用到折半定理,所以将 n 视为 2 的次幂)

有多项式:

A(x)=a0+a1x+a2x2++an1xn1

A(x) 中的单项按照下标奇偶性分开:

A(x)=(a0+a2x2++an2xn2)+(a1x+a3x3++an1xn1)=(a0+a2x2++an2xn2)+x(a1+a3x2++an1xn2)

令:

A1(x)=a0+a2x++an2xn/21A2(x)=a1+a3x++an1xn/21

则:

A(x)=A1(x2)+xA2(x2)

k[0,n/21],将 ωnk 代入 A(x) 后得:

A(ωnk)=A1((ωnk)2)+ωnkA2((ωnk)2)=A1(ωn2k)+ωnkA2(ωn2k)=A1(ωn/2k)+ωnkA2(ωn/2k)

接着将 ωnk+n/2 代入 A(x) 后得:

A(ωnk+n/2)=A1((ωnk+n/2)2)+ωnk+n/2A2((ωnk+n/2)2)=A1(ωn/2k)+ωnk+n/2A2(ωn/2k)=A1(ωn/2k)ωnkA2(ωn/2k)

可以观察到 A(ωnk)A(ωnk+n/2) 化简后只有符号不同,也就是说,只要求出了 A1(ωn/2k)A2(ωn/2k) 的值,就能求出 A(ωnk)A(ωnk+n/2) 的值。

问题就转化为了求 A1(x)A2(x)x=ωn/20,ωn/21,,ωn/2n/21 的取值,递归求解即可,时间复杂度 O(nlogn)

IFFT

通过 FFT,我们已经能在 O(nlogn) 的时间复杂度下将系数转化为点值,接下来,考虑如何将多项式的点值转化为系数。

对于前面的 FFT,其过程可以用矩阵乘法来表示:

[11111ωn1ωn2ωnn11ωn2ωn4ωn2n21ωnn1ωn2n2ωn(n1)2]×[a0a1a2an1]=[A(a0)A(a1)A(a2)A(an1)]

所以构造出左边矩阵的逆矩阵即可,左边的矩阵满足 Vi,j=ωnij,它的逆矩阵是 Vi,j1=ωnijn。所以就有:

[11111ωn1ωn2ωnn+11ωn2ωn4ωn2n+21ωnn+1ωn2n+2ωn(n1)2]×[A(a0)A(a1)A(a2)A(an1)]=n×[a0a1a2an1]

相当于做 FFT 时将 ωnk 变成 ωnk,最后将结果除以 n 即可,时间复杂度也为 O(nlogn)

有了 FFTIFFT 就可以写出递归版本的多项式乘法,下面是洛谷 P3803 的代码:

#include <bits/stdc++.h>

using i64 = long long;
using namespace std::complex_literals;
using complex = std::complex<double>;

int n, _m;
std::vector<complex> a, b;

const double pi = acos(-1.0);
void FFT(std::vector<complex> &a, int coef) {
    int n = a.size();
    if (n == 1)
        return ;
    std::vector<complex> a1, a2;
    for (int i = 0; i < n; ++i) {
        (i & 1 ? a2 : a1).push_back(a[i]);
    }
    FFT(a1, coef);
    FFT(a2, coef);
    double theta = coef * 2 * pi / n;
    complex wn{cos(theta), sin(theta)};
    complex w = 1;
    for (int k = 0; k < n / 2; ++k, w *= wn) {
        a[k] = a1[k] + w * a2[k];
        a[k + n / 2] = a1[k] - w * a2[k];
    }
}

std::vector<int> multiply(std::vector<int> _a, std::vector<int> _b) {
    std::vector<complex> a(_a.begin(), _a.end()), b(_b.begin(), _b.end());
    int n = a.size(), _m = b.size(), len = n + _m - 2;
    for (n += _m; n != (n & -n); ++n) ;
    a.resize(n);
    b.resize(n);

    FFT(a, 1);
    FFT(b, 1);
    for (int i = 0; i < n; ++i)
        a[i] = a[i] * b[i];
    FFT(a, -1);

    std::vector<int> c(len + 1);
    for (int i = 0; i <= len; ++i) {
        c[i] = (int)(a[i].real() / n + 0.5);
    }
    return c;
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m;
    std::cin >> n >> m;
    ++n, ++m;
    std::vector<int> a, b;
    while (n--) {
        int x;
        std::cin >> x;
        a.push_back(x);
    }
    while (m--) {
        int x;
        std::cin >> x;
        b.push_back(x);
    }

    auto ans = multiply(a, b);
    for (auto x : ans)
        std::cout << x << ' ';
}

交上去能够通过,但常数较大(最慢的点跑了 1.37s)。考虑优化常数,下面介绍小常数迭代写法。

蝴蝶变换

考虑 FFT 过程中下标的变化:

观察到后序列的下标其实就是原序列下标二进制的翻转,因为每次都把奇数放在右边,偶数放在左边。有了这个性质,我们可以预先把下标放在后序列对应的下标上,然后对区间进行合并即可。

代码:

#include <bits/stdc++.h>

using i64 = long long;
using namespace std::complex_literals;
using complex = std::complex<double>;

const double pi = acos(-1.0);
void FFT(std::vector<complex> &a, int coef, const std::vector<int> &rev) {
    int n = a.size();
    for (int i = 0; i < n; ++i)
        if (i < rev[i])
            std::swap(a[i], a[rev[i]]);

    for (int k = 1; k < n; k <<= 1) {
        double theta = coef * pi / k;
        complex wn{cos(theta), sin(theta)};
        for (int i = 0; i < n; i += k * 2) {
            complex w = 1;
            for (int j = 0; j < k; ++j, w *= wn) {
                auto x = a[i + j], y = a[i + j + k] * w;
                a[i + j] = x + y;
                a[i + j + k] = x - y;
            }
        }
    }
}

std::vector<int> multiply(std::vector<int> _a, std::vector<int> _b) {
    std::vector<complex> a(_a.begin(), _a.end()), b(_b.begin(), _b.end());
    int n = a.size(), _m = b.size(), len = n + _m - 2;
    for (n += _m; n != (n & -n); ++n) ;
    a.resize(n);
    b.resize(n);
    int s = std::__lg(n);
    std::vector<int> rev(n);
    for (int i = 1; i < n; ++i)
        rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (s - 1));

    FFT(a, 1, rev);
    FFT(b, 1, rev);
    for (int i = 0; i < n; ++i)
        a[i] = a[i] * b[i];
    FFT(a, -1, rev);

    std::vector<int> c(len + 1);
    for (int i = 0; i <= len; ++i) {
        c[i] = (int)(a[i].real() / n + 0.5);
    }
    return c;
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m;
    std::cin >> n >> m;
    ++n, ++m;
    std::vector<int> a, b;
    while (n--) {
        int x;
        std::cin >> x;
        a.push_back(x);
    }
    while (m--) {
        int x;
        std::cin >> x;
        b.push_back(x);
    }

    auto ans = multiply(a, b);
    for (auto x : ans)
        std::cout << x << ' ';
}

交上去最慢的点跑了 590ms,效率超过递归写法的两倍。

作者:CTHOOH

出处:https://www.cnblogs.com/CTHOOH/p/18184021

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   CTHOOH  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
more_horiz
keyboard_arrow_up light_mode palette
选择主题
点击右上角即可分享
微信分享提示