算法知识图谱总览 / 多项式与FFT族 / FFT(快速傅里叶变换)

FFT(快速傅里叶变换)

P1advancedstandardG-C-12

What · 是什么

O(n log n)计算多项式乘法,基于DFT与蝴蝶操作,NTT为模意义版本

快速傅里叶变换(FFT)把多项式从系数表示转为在单位根处的点值表示(DFT),利用单位根性质分治加速,在 O(n log n) 内完成多项式乘法:系数→点值(正变换)→逐点相乘→点值→系数(逆变换)。核心是蝴蝶操作(奇偶分治+旋转因子),实现分递归与迭代(位逆序置换+倍增合并)两种。FFT 用复数运算,存在浮点误差;模意义版本是 NTT。应用远超多项式乘法:大整数乘法、所有可能和/差、循环相关与自相关、卷积型 DP 加速、字符串匹配(与 pattern 反转卷积)等。

Why · 为什么

直觉 · Intuition

朴素乘法把每个系数对乘一遍 O(n^2)。换个角度:多项式在 n 个点上的取值(点值)可以逐点相乘来得到乘积的点值,而求 n 个点的点值+还原系数都能用分治加速——因为取单位根时奇偶项平方重合,能成对复用,这就是'快速'的来源。

原理 · Principle

DFT 是线性变换,取 n=2^k 次单位根 ω_n=e^{-2πi/n}。分治:f(x)=G(x^2)+x·H(x^2),由 ω_n^{k+n/2}=-ω_n^k 得 f(ω^k)=G(ω^{2k})+ω^k H(ω^{2k}),f(ω^{k+n/2})=G(ω^{2k})-ω^k H(ω^{2k}),一次算出两点。递归深度 log n,总 O(n log n)。逆变换取共轭根再除 n。迭代版先用位逆序置换把输入排成可自底向上合并的顺序,每层做蝶形。

历史 · History

FFT 的思想源于 Fourier(1807),离散版本 DFT 用于信号处理。Cooley & Tukey 1965 提出分治 FFT 算法(思想可追溯至 Gauss),被列为计算数学十大算法之一。OI/ACM 中 2000 年代起成为多项式全家桶的基石,OI Wiki 与 CP-Algorithms 均有系统教程。

How · 实现

伪代码

# 迭代 FFT (in-place), n=2^k
bit_reverse_permute(a)      # 位逆序置换
for h in 2,4,8,...,n:        # 合并区间长度
    wn = exp(-2πi/h)        # 旋转因子
    for i in step h:
        w = 1
        for j in i..i+h/2-1:
            u = a[j]; v = a[j+h/2]*w
            a[j] = u+v; a[j+h/2] = u-v
            w *= wn
# 逆变换: 根取共轭, 最后整体除以 n

Python 实现

import cmath
import math
import random


def fft_recursive(a, invert=False):
    """递归 FFT。a: list[complex], 长度必须是 2 的幂。
    invert=False: DFT; invert=True: IDFT(由调用方负责除以 n)."""
    n = len(a)
    if n == 1:
        return a[:]
    w_n = cmath.exp(complex(0, -2 * math.pi / n)) if not invert else cmath.exp(complex(0, 2 * math.pi / n))
    a_even = fft_recursive(a[0::2], invert)
    a_odd = fft_recursive(a[1::2], invert)
    w = 1 + 0j
    out = [0j] * n
    half = n // 2
    for k in range(half):
        t = w * a_odd[k]
        out[k] = a_even[k] + t
        out[k + half] = a_even[k] - t
        w *= w_n
    return out


def fft_recursive_rounded(a, invert=False):
    """带归一化的递归 FFT 封装: invert 时自动除以 n. 返回新列表。"""
    out = fft_recursive(a, invert)
    if invert:
        n = len(a)
        out = [x / n for x in out]
    return out


def bit_reverse_permute(a):
    n = len(a)
    j = 0
    for i in range(1, n):
        bit = n >> 1
        while j & bit:
            j ^= bit
            bit >>= 1
        j ^= bit
        if i < j:
            a[i], a[j] = a[j], a[i]


def fft_iterative(a, invert=False):
    """迭代 FFT(位逆序置换 + 蝴蝶操作). 原地修改. 长度须为 2 的幂."""
    n = len(a)
    a = a[:]
    bit_reverse_permute(a)
    h = 2
    while h <= n:
        angle = -2 * math.pi / h
        if invert:
            angle = -angle
        w_h = cmath.exp(complex(0, angle))
        for i in range(0, n, h):
            w = 1 + 0j
            for j in range(i, i + h // 2):
                u = a[j]
                v = a[j + h // 2] * w
                a[j] = u + v
                a[j + h // 2] = u - v
                w *= w_h
        h <<= 1
    if invert:
        for i in range(n):
            a[i] /= n
    return a


def poly_multiply_fft(a, b):
    """FFT 多项式乘法(返回实部圆整的整数系数). 支持小规模正数系数."""
    n = 1
    need = len(a) + len(b) - 1
    while n < need:
        n <<= 1
    fa = [complex(x, 0) for x in a] + [0j] * (n - len(a))
    fb = [complex(x, 0) for x in b] + [0j] * (n - len(b))
    fa = fft_iterative(fa, False)
    fb = fft_iterative(fb, False)
    for i in range(n):
        fa[i] *= fb[i]
    fc = fft_iterative(fa, True)
    return [int(round(fc[i].real)) for i in range(need)]


def poly_multiply_naive(a, b):
    res = [0] * (len(a) + len(b) - 1)
    for i, x in enumerate(a):
        for j, y in enumerate(b):
            res[i + j] += x * y
    return res


if __name__ == "__main__":
    # 自测: 与朴素卷积对拍(小规模)
    for _ in range(50):
        la = random.randint(1, 8)
        lb = random.randint(1, 8)
        a = [random.randint(0, 9) for _ in range(la)]
        b = [random.randint(0, 9) for _ in range(lb)]
        assert poly_multiply_fft(a, b) == poly_multiply_naive(a, b), (a, b)
    # OI-wiki 引入样例: (5x^2+3x+7)(7x^2+2x+1)
    A = [7, 3, 5]
    B = [1, 2, 7]
    assert poly_multiply_fft(A, B) == [7, 17, 60, 31, 35], poly_multiply_fft(A, B)
    # 递归版与迭代版一致
    x = [complex(random.randint(-5, 5), random.randint(-5, 5)) for _ in range(8)]
    assert all(abs(fft_recursive(x)[k] - fft_iterative(x)[k]) < 1e-9 for k in range(8))
    # 单位复根校验: 插值还原
    y = fft_iterative(x, False)
    z = fft_iterative(y, True)
    assert all(abs(z[k] - x[k]) < 1e-9 for k in range(8))
    # 多项式乘法样例: (5x^2+3x+7)(7x^2+2x+1)=35x^4+31x^3+60x^2+17x+7
    assert poly_multiply_fft([7, 3, 5], [1, 2, 7]) == [7, 17, 60, 31, 35]
    print("FFT tests passed")
PyPy 提示Python 纯循环 FFT 常数大,1e5 长度乘法可能 TLE(复杂度 O(n log n) 但每次迭代有 Python 开销与复数对象)。教学正确。竞赛中 Python 建议:能取模就用 NTT;不能取模时可用 numpy 的 FFT(numpy 未内置不可用则手写递归版)。分块卷积(块内朴素/FFT)可缓解。
时间复杂度O(n log n) 多项式乘法
空间复杂度O(n)
注意 · 局限浮点误差: 系数较大(如 1e9 量级且 n 大)时圆整可能错, 需要 NTT/拆系数 FFT(three-FFT / 任意模数)。长度必须是 2 的幂, 需补齐。Python 纯实现性能差, N=1e5 级可能 TLE, 应改用 NTT 或 numpy。递归版在 Python 有递归深度限制, 需 sys.setrecursionlimit 或改迭代。

易错点

例题

OJ题号难度考点链接
LuoguP38033多项式乘法模板题(FFT/NTT 均可), 入门必做https://www.luogu.com.cn/problem/P3803
HDU14023A * B Problem Plus: 大整数乘法, 经典 FFT 应用http://acm.hdu.edu.cn/showproblem.php?pid=1402
Codeforces553E6Kyoya and Train: FFT 优化 DP 转移的卷积结构https://codeforces.com/contest/553/problem/E
步骤迷你图(节点 = 算法步骤 · 边 = 流程 · 绿色 = 起始步骤)

信源

✓ = 已核验 · △ = 未核验