CC++ & Algorithm

FFT 快速傅里叶变换

极难2
语言版本:通用
概述:FFT是多项式乘法的“加速器”,它利用分治和单位根,把O(n^2)变成O(n log n),就像让你瞬间数清一大袋糖果。

FFT 快速傅里叶变换:多项式乘法的“超级加速器”

你有没有遇到过这样的问题:两个多项式相乘,比如
(3x2+2x+1)×(4x+5)(3x^2 + 2x + 1) \times (4x + 5),如果用传统方法一个一个乘、再合并同类项,项数多了会累得够呛。如果多项式有十万项,普通方法需要计算一百亿次,电脑都得卡半天。
FFT(快速傅里叶变换)就像给多项式乘法装上了“火箭引擎”,它把计算速度从 O(n²) 提升到 O(n log n),让你瞬间算出结果。它背后的数学魔法是“分治”和“单位根”。
现在,我们就来揭开它的神秘面纱吧!

为什么我们需要 FFT?

回忆一下,两个多项式相乘的朴素做法:

A(x)=a0+a1x+a2x2++an1xn1A(x) = a_0 + a_1x + a_2x^2 + \dots + a_{n-1}x^{n-1} B(x)=b0+b1x+b2x2++bm1xm1B(x) = b_0 + b_1x + b_2x^2 + \dots + b_{m-1}x^{m-1}

乘积 C(x)=A(x)B(x)C(x) = A(x) \cdot B(x) 的每一项系数需要内层循环:

ck=i+j=kaibjc_k = \sum_{i+j=k} a_i b_j

这就像统计你和小伙伴的零花钱组合:你有 3 种面额的硬币,他有 4 种,要算所有组合的总和,得把每种面额都配对一次,运算量是硬币数目的乘积。

生活中类似的例子:假如你每周有 5 元零花钱,你朋友有 4 元零花钱,你们俩合在一起买零食,需要知道每种价格(比如 1元、2元、3元……)的零食能买多少种组合?这其实就是一个卷积问题。当硬币种类非常多时,手工计算慢得让人崩溃。

而 FFT 提供了一条捷径:

  1. 系数表示 → 点值表示:把多项式看作函数,代入特殊的 x 值(单位根),算出函数值。
  2. 点值相乘:两个多项式在同一个 x 处的函数值直接相乘,就得到了乘积多项式在这个 x 处的值。
  3. 点值表示 → 系数表示:再通过逆变换(IFFT)把点值变回系数。

这个过程就像:

  • 先用“魔法镜”把一堆零钱(系数)变成它们的“影子”(点值),
  • 两个影子直接叠在一起(点乘),
  • 再用“反魔法镜”把叠好的影子变回零钱(系数)。
    因为点乘是 O(n) 的,而变换用分治只需要 O(n log n),所以整体比 O(n²) 快得多。

FFT 的核心思想:分治 + 单位根

为了快速计算多项式在 n 个点上的值(n 是 2 的幂次,不足就补 0),我们需要利用单位根的对称性,以及分治策略。

什么是单位根?

单位根就是方程 zn=1z^n = 1 的解,一共有 n 个:ωn0,ωn1,,ωnn1\omega_n^0, \omega_n^1, \dots, \omega_n^{n-1}
其中

ωnk=e2πik/n=cos(2πk/n)+isin(2πk/n)\omega_n^k = e^{2\pi i k / n} = \cos(2\pi k / n) + i \sin(2\pi k / n)

这些点均匀分布在复平面单位圆上,像时钟的刻度一样。

重要性质(蝶形操作的基础):

  • 折半定理(ωnk)2=ωn/2k(\omega_n^k)^2 = \omega_{n/2}^k
  • 对称性ωnk+n/2=ωnk\omega_n^{k + n/2} = -\omega_n^k

分治过程

假设有一个 n 次多项式(n 是 2 的幂):

A(x)=a0+a1x+a2x2++an1xn1A(x) = a_0 + a_1 x + a_2 x^2 + \dots + a_{n-1} x^{n-1}

我们想求它在 n 个单位根处的值。直接代入每个根需要 O(n²),太慢。FFT 的做法是:
按奇偶下标分成两个小多项式

  • 偶数下标部分:A0(x)=a0+a2x+a4x2++an2xn/21A_0(x) = a_0 + a_2 x + a_4 x^2 + \dots + a_{n-2} x^{n/2 - 1}
  • 奇数下标部分:A1(x)=a1+a3x+a5x2++an1xn/21A_1(x) = a_1 + a_3 x + a_5 x^2 + \dots + a_{n-1} x^{n/2 - 1}

那么原多项式可以写成:

A(x)=A0(x2)+xA1(x2)A(x) = A_0(x^2) + x \cdot A_1(x^2)

例子
A(x)=1+2x+3x2+4x3A(x) = 1 + 2x + 3x^2 + 4x^3(n=4)

  • A0(x)=1+3xA_0(x) = 1 + 3x(偶数下标:0 和 2)
  • A1(x)=2+4xA_1(x) = 2 + 4x(奇数下标:1 和 3)
    验证:A(x)=A0(x2)+xA1(x2)=(1+3x2)+x(2+4x2)=1+2x+3x2+4x3A(x) = A_0(x^2) + x \cdot A_1(x^2) = (1 + 3x^2) + x(2 + 4x^2) = 1 + 2x + 3x^2 + 4x^3,正确。

现在,我们要求 A(ωnk)A(\omega_n^k)。利用折半定理,(ωnk)2=ωn/2k(\omega_n^k)^2 = \omega_{n/2}^k,所以

A(ωnk)=A0(ωn/2k)+ωnkA1(ωn/2k)A(\omega_n^k) = A_0(\omega_{n/2}^k) + \omega_n^k \cdot A_1(\omega_{n/2}^k)

对于 k+n/2k + n/2kk 从 0 到 n/21n/2 - 1):

ωnk+n/2=ωnk\omega_n^{k+n/2} = -\omega_n^k

(ωnk+n/2)2=ωn/2k(\omega_n^{k+n/2})^2 = \omega_{n/2}^k 仍然成立,所以

A(ωnk+n/2)=A0(ωn/2k)ωnkA1(ωn/2k)A(\omega_n^{k+n/2}) = A_0(\omega_{n/2}^k) - \omega_n^k \cdot A_1(\omega_{n/2}^k)

这就是著名的蝶形操作(butterfly operation):只要知道了 A0A_0A1A_1n/2n/2 个根上的值,就能同时得到 AA 在成对的两个根上的值,一个加、一个减。

生活比喻:想象你有一排小朋友(多项式系数),你想知道每个小朋友的身高在“魔法尺”(单位根)上的读数。你把小朋友分成奇偶两排,先量出每排一半小朋友的读数,然后用“蝶形公式”快速合成全部小朋友的读数。每次分组规模减半,就像给全校学生做体检,先分班,再分小组,最后轻松汇总。

蝶形操作详解(配图想象)

蝶形操作的图形就像蝴蝶的翅膀:左边两个输入(a0a0wa1w \cdot a1),右边两个输出(加和减)。
对于每个 kk,我们计算:

  • 上半部分:anew[k]=a0k+wa1ka_{new}[k] = a0_k + w \cdot a1_k
  • 下半部分:anew[k+n/2]=a0kwa1ka_{new}[k + n/2] = a0_k - w \cdot a1_k

其中 w=ωnkw = \omega_n^k 是旋转因子。

递归实现:从顶层多项式开始,不断分成两半,到底层(1项)直接返回,然后逐层用蝶形操作合并。整个递归树深度为 log2n\log_2 n,每层有 nn 次运算,总复杂度 O(n log n)。

递归 FFT 代码详解(含中文注释)

我们先用递归版本理解原理(实际比赛中一般用非递归迭代版本以避免递归开销,但递归更直观)。

C++ 实现

#include <iostream>
#include <complex>
#include <vector>
#include <cmath>
using namespace std;

using Complex = complex<double>;       // 复数类型
const double PI = acos(-1.0);          // 圆周率

// 递归FFT,a是系数向量,长度n必须为2的幂
// invert = false 表示 DFT(正变换),true 表示 IDFT(逆变换)
void fft(vector<Complex>& a, bool invert) {
    int n = a.size();                  // 当前长度
    if (n == 1) return;                // 只有一个点,递归结束

    // 分成奇偶两部分
    vector<Complex> a0(n / 2), a1(n / 2);
    for (int i = 0; i < n / 2; ++i) {
        a0[i] = a[2 * i];              // 偶数下标:a0, a2, a4...
        a1[i] = a[2 * i + 1];          // 奇数下标:a1, a3, a5...
    }

    // 递归计算两个子多项式在 n/2 个单位根上的值
    fft(a0, invert);
    fft(a1, invert);

    // 当前层蝶形操作
    double angle = 2 * PI / n * (invert ? -1 : 1); // 正变换角度为正,逆变换为负
    Complex w(1, 0);                               // w = omega_n^0 = 1
    Complex wn(cos(angle), sin(angle));            // 步长因子 omega_n^1

    for (int i = 0; i < n / 2; ++i) {
        Complex t = w * a1[i];                     // 乘上旋转因子
        a[i] = a0[i] + t;                         // 上半部分
        a[i + n / 2] = a0[i] - t;                 // 下半部分
        w *= wn;                                  // w 更新为下一个 omega_n^k
    }
}

// 使用FFT进行两个整数多项式乘法
// a, b 是系数向量(低次到高次)
vector<long long> multiplyPolynomialsFFT(const vector<int>& a, const vector<int>& b) {
    // 1. 补成长度,找到大于等于目标长度的2的幂
    int n = 1;
    while (n < a.size() + b.size() - 1) n <<= 1;

    // 2. 复制到复数向量
    vector<Complex> fa(n, 0), fb(n, 0);
    for (size_t i = 0; i < a.size(); ++i) fa[i] = Complex(a[i], 0);
    for (size_t i = 0; i < b.size(); ++i) fb[i] = Complex(b[i], 0);

    // 3. DFT:将系数转化为点值
    fft(fa, false);
    fft(fb, false);

    // 4. 点乘:对应位置复数相乘(就是函数值相乘)
    for (int i = 0; i < n; ++i) fa[i] *= fb[i];

    // 5. IDFT:将点值转回系数
    fft(fa, true);

    // 6. 除以 n 并四舍五入取整(因为IDFT公式要除以n)
    vector<long long> result(n);
    for (int i = 0; i < n; ++i) {
        result[i] = llround(fa[i].real() / n);  // 四舍五入,消除微小浮点误差
    }

    // 7. 去掉尾部多余的0(如果乘积最高次系数为0)
    while (result.size() > 1 && result.back() == 0) result.pop_back();
    return result;
}

int main() {
    // 例子:A(x) = 1 + 2x + 3x^2,B(x) = 5 + 4x
    vector<int> A = {1, 2, 3};
    vector<int> B = {5, 4};

    auto prod = multiplyPolynomialsFFT(A, B);
    cout << "A * B = ";
    // 从高次到低次输出
    for (int i = prod.size() - 1; i >= 0; --i) {
        cout << prod[i] << (i > 0 ? " " : "");
    }
    cout << endl;
    // 输出应该是:12 23 10 5 (对应 12x^3 + 23x^2 + 10x + 5)
    return 0;
}

Python 实现

import cmath
import math

def fft(a, invert):
    """
    递归FFT
    a: 复数列表,长度必须为2的幂
    invert: False表示DFT,True表示IDFT
    """
    n = len(a)
    if n == 1:
        return

    # 分奇偶
    a0 = [a[i] for i in range(0, n, 2)]
    a1 = [a[i] for i in range(1, n, 2)]

    # 递归
    fft(a0, invert)
    fft(a1, invert)

    angle = 2 * math.pi / n * (-1 if invert else 1)
    wn = complex(math.cos(angle), math.sin(angle))
    w = complex(1, 0)

    for i in range(n // 2):
        t = w * a1[i]                # 乘旋转因子
        a[i] = a0[i] + t
        a[i + n // 2] = a0[i] - t
        w *= wn

def multiply_polynomials_fft(a, b):
    """使用FFT进行整数多项式乘法"""
    # 补成长度(2的幂)
    n = 1
    while n < len(a) + len(b) - 1:
        n <<= 1

    # 创建复数向量,后面补0
    fa = [complex(x, 0) for x in a] + [complex(0, 0)] * (n - len(a))
    fb = [complex(x, 0) for x in b] + [complex(0, 0)] * (n - len(b))

    # DFT
    fft(fa, False)
    fft(fb, False)

    # 点乘
    for i in range(n):
        fa[i] *= fb[i]

    # IDFT
    fft(fa, True)

    # 除以n并四舍五入
    result = [round(fa[i].real / n) for i in range(n)]
    # 去掉尾部多余0
    while len(result) > 1 and result[-1] == 0:
        result.pop()
    return result

# 测试
A = [1, 2, 3]   # 3x^2 + 2x + 1
B = [5, 4]      # 4x + 5
prod = multiply_polynomials_fft(A, B)
print("A * B =", prod[::-1])   # 反转显示:高次在前

完整示例:一步一步演示

假设我们要计算 A(x)=1+2xA(x) = 1 + 2xB(x)=3+4xB(x) = 3 + 4x。手工卷积:
(1+2x)(3+4x)=13+(14+23)x+24x2=3+10x+8x2(1+2x)*(3+4x) = 1*3 + (1*4+2*3)x + 2*4x^2 = 3 + 10x + 8x^2

用FFT步骤(n取4,补成2的幂):

  1. 系数向量:fa = [1,2,0,0],fb = [3,4,0,0]
  2. 计算DFT:
    • 求 ω_4^0=1,ω_4^1=i,ω_4^2=-1,ω_4^3=-i 处的值。
    • 递归分治后得到:
      fa的DFT结果:? 实际是 [1+2i, 1-2i, 1?]…… 我们直接看最终点乘后的逆变换结果。
  3. 点乘:fa[i] *= fb[i]
  4. IDFT后除以4,得到系数 [3,10,8,0],去掉尾部0即 [3,10,8]。
    这个例子虽然小,但体现了整个流程。

常见错误与调试技巧

1. 忘记补齐长度为2的幂

FFT要求输入长度必须是2的幂,否则递归时无法平分。
解决方法:用 while 循环找到不小于目标长度的最小2的幂,然后补0。

2. 逆变换忘记除以 n

DFT的逆变换公式中,每个结果要除以 n。很多新手做完IDFT后直接取整,得到错误结果。
解决方法:循环中除以 n(或者在递归最外层统一除以 n)。上面代码是在最后除以 n。

3. 浮点精度问题

复数运算有微小误差,导致四舍五入后得到 ±1 的偏差。
解决方法:使用 llround(C++)或 round(Python)四舍五入。对于整数系数,一般没问题。如果需要绝对精确,可以考虑NTT(数论变换)。

4. 递归栈溢出

当 n 很大(如 2^20 ≈ 100万)时,递归深度为 20,栈空间还够,但递归调用复制向量会消耗大量内存和时间。
解决方法:实际竞赛中用迭代版本(非递归的“位逆序置换”+“蝶形操作循环”),可以避免递归开销。

5. 输出顺序问题

多项式系数通常按低次到高次存储(例如 [1,2,3] 表示 1+2x+3x^2)。
输出时注意顺序,不要搞反。

相关指引:下一步学什么?

学会了FFT,你就掌握了一个强大的武器。但它有两个小缺点:

  • 存在浮点误差,不适合需要精确整数的场合(比如求大整数乘积、密码学中的模运算)。
  • 需要用到复数,而有些语言没有内置复数库。

NTT(数论变换) 就是解决这两个问题的“FFT整数版”。它把单位根换成模大素数下的原根,所有运算都是整数,准确无误。

另外,FFT还有很多应用:

  • 大整数乘法:把大整数看成以10为底的多项式,利用FFT快速乘。
  • 卷积与信号处理:图像模糊、音频处理等都用到FFT。
  • 生成函数:在组合数学中快速计算组合数。

你可以先从迭代FFT(非递归版本) 开始练习,再进阶到 NTT,甚至尝试实现分治 FFT 来优化多项式求逆等问题。

继续加油,数学的世界还有很多有趣的“加速魔法”等着你!

例题精讲

1单选题

FFT(快速傅里叶变换)算法的时间复杂度是多少?

AO(n)
BO(n log n)
CO(n^2)
DO(log n)
2判断题

标准FFT算法要求输入序列的长度必须是2的幂。

3填空题
在迭代FFT的实现中,最内层循环计算蝶形运算。假设当前步长为len,旋转因子为w,则核心代码为:
for j in range(0, n, len):
    for k in range(len//2):
        u = a[j+k]
        v = a[j+k+len//2] * ___  # 填空处
        a[j+k] = u + v
        a[j+k+len//2] = u - v
    w *= w_step
其中旋转因子初始值应为:
4单选题

在逆快速傅里叶变换(IFFT)中,为了从频域恢复时域信号,通常需要在变换后对每个点除以什么?

A1
BN(序列长度)
C√N
D
5判断题

FFT可以用于高效计算两个多项式的乘积。