「快速傅里叶变换」总结

本文详细介绍了快速傅里叶变换(FFT)及其变种DFT和IDFT,涉及多项式乘法的高效计算方法。文章还讨论了单位根的概念,以及如何通过NTT处理模意义下的问题,包括原根的应用。最后给出了FFT和NTT的代码模板。
摘要由CSDN通过智能技术生成

前言

作为 NOI 大纲里的十级算法,FFT拥有着逼格十分高的名称,也十分令人头疼?(雾
通俗地讲,FFT旨在以优异的时间复杂度,解决两个多项式相乘的问题,即:
c k = ∑ i = 0 k a i b k − i c_k=\sum_{i=0}^{k}a_ib_{k-i} ck=i=0kaibki
FFT的核心思想在于:将一个多项式转化成点值表示法,再由点值表示法推出原多项式。

如果觉得抽象可以看图在这里插入图片描述

前置知识

  • 多项式的点值表示法和系数表示法
  • 向量
  • 复数

复数可以自行查阅 Oi-wiki。
这里简略介绍一下多项式表示法(不妨令 A ( x ) A(x) A(x) 表示一个 n − 1 n-1 n1 次的多项式)。
系数表示法:若 A ( x ) = ∑ i = 0 n a i × x i A(x)=\sum_{i=0}^{n}a_i\times x^{i} A(x)=i=0nai×xi
点值表示法:不妨将 n n n 个互不相同的 x x x 代入多项式得到 n n n y y y,则 A A A 可被这 n n n 个值 唯一确定,其中 y i = ∑ j = 0 n − 1 a j × x i j y_i=\sum_{j=0}^{n-1}a_j\times x_{i}^{j} yi=j=0n1aj×xij

单位根

请注意单位根的概念是建立在 复平面 上的,因此需要熟知复数的运算法则。

定义:以原点为圆心作单位圆,并钦定圆心为起点,圆点的 n n n 等分点为终点,作 n n n 条向量,这 n n n 条向量即为 n n n 次单位根,通常将幅角为正且最小的向量对应的复数称为 ω n \omega_n ωn

一般地, n n n 次单位根可由 欧拉公式 直接得出: ω n k = c o s k 2 π n + i s i n k 2 π n \omega_{n}^{k}=cosk\frac{2\pi}{n}+isink\frac{2\pi}{n} ωnk=coskn2π+isinkn2π

ω n \omega_n ωn 显然满足以下性质:

  • 每乘一次 ω n \omega_n ωn 便逆时针转动 2 π n \frac{2\pi}{n} n2π 角度
  • ω n n = ω n \omega_{n}^{n} = \omega_{n} ωnn=ωn
  • ω 2 n 2 k = ω n k \omega_{2n}^{2k}=\omega_{n}^{k} ω2n2k=ωnk
    转载自知乎

快速傅里叶变换(DFT)

下皆令 n = 2 k ( k ∈ Z ) n=2^k(k\in Z) n=2k(kZ)
不妨令一个多项式 A ( x ) = a 0 + a 1 x + a 2 x 2 + a 3 x 3 + ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ + a n − 1 x n − 1 A(x)=a_0+a_1x+a_2x^{2}+a_3x^{3}+······+a_{n-1}x^{n-1} A(x)=a0+a1x+a2x2+a3x3+⋅⋅⋅⋅⋅⋅+an1xn1
将其按照下标奇偶分类,可得:
A 1 ( x ) = a 0 + a 2 x + a 4 x 2 + ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ + a n − 2 x n 2 A_1(x)=a_0+a_2x+a_4x^{2}+······+a_{n-2}x^{\frac{n}{2}} A1(x)=a0+a2x+a4x2+⋅⋅⋅⋅⋅⋅+an2x2n A 2 ( x ) = a 1 + a 3 x + a 5 x 2 + ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ + a n − 1 x n 2 A_2(x)=a_1+a_3x+a_5x^{2}+······+a_{n-1}x^{\frac{n}{2}} A2(x)=a1+a3x+a5x2+⋅⋅⋅⋅⋅⋅+an1x2n
显然有, A ( x ) = A 1 ( x 2 ) + x A 2 ( x 2 ) A(x)=A_1(x^2)+xA_{2}(x^2) A(x)=A1(x2)+xA2(x2)
注意到,多项式 A ( x ) A(x) A(x) 可被 n n n 个值唯一确定,所以,如果我们可以利用上述式子,代入特殊的 x ′ x\prime x 值,以高效的时间复杂度得到 A ( x ′ ) A(x\prime) A(x) 对应的值,那么便可以用 点值表示法 表示 A ( x ) A(x) A(x) 了。

代入 ω n k \omega_{n}^{k} ωnk,则有:
A ( ω n k ) = A 1 ( ω n 2 k ) + ω n k A 2 ( ω n 2 k ) A(\omega_{n}^{k})=A_1(\omega_{n}^{2k})+\omega_{n}^{k}A_2(\omega_{n}^{2k}) A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k) A ( ω n k ) = A 1 ( ω n 2 k ) + ω n k A 2 ( ω n 2 k ) A(\omega_{n}^{k})=A_1(\omega_{\frac{n}{2}}^{k})+\omega_{n}^{k}A_2(\omega_{\frac{n}{2}}^{k}) A(ωnk)=A1(ω2nk)+ωnkA2(ω2nk)
代入 ω n k + n 2 \omega_{n}^{k+\frac{n}{2}} ωnk+2n,同理可得:
A ( ω n k + n 2 ) = A 1 ( ω n 2 k ) − ω n k A 2 ( ω n 2 k ) A(\omega_{n}^{k+\frac{n}{2}})=A_1(\omega_{\frac{n}{2}}^{k})-\omega_{n}^{k}A_2(\omega_{\frac{n}{2}}^{k}) A(ωnk+2n)=A1(ω2nk)ωnkA2(ω2nk)

通过大眼观察法可以得出,当我们计算出 A ( ω n k ) A(\omega_{n}^{k}) A(ωnk) 时,我们同时也得到了 A ( ω n k + n 2 ) A(\omega_{n}^{k+\frac{n}{2}}) A(ωnk+2n) 的值,这便是又名的 蝴蝶操作
于是,我们把原问题分割成了两个等价的子问题。这也就意味着,通过不断递归,我们将 n n n 个单位根 ω n k \omega_{n}^{k} ωnk 依次代入,便可以实现以 O ( n l o g n ) O(nlogn) O(nlogn) 的时间复杂度用 点值表示法 表示多项式 A ( x ) A(x) A(x)

快速傅里叶逆变换(IDFT)

然而,题目通常要求我们用 系数表示法 表示一个多项式,而非 点值表示法。因此,我们需要将 点值 转化为 系数

结论:IDFT 求解的矩阵即为 DFT 的逆矩阵,则我们可得:搬自YLY的课件

形式化地讲,令 B ( x ) = I D F T ( A ( x ) ) B(x)=IDFT(A(x)) B(x)=IDFT(A(x)),则有:
b k = 1 n ∑ i = 0 a i ω n − i k = 1 n A ( ω n − k ) b_k=\frac{1}{n}\sum_{i=0}a_i\omega_n^{-ik}=\frac{1}{n}A(\omega_n^{-k}) bk=n1i=0aiωnik=n1A(ωnk)

由此,我们只需要将 DFT 实现过程中的 ω n \omega_n ωn 修改为 ω n − 1 \omega_n^{-1} ωn1 ,最后将所得结果乘上 1 n \frac{1}{n} n1 即可。

真正的快速傅里叶变换(FFT)

结合 DFT 与 IDFT,我们实现了 O ( n l o g n ) O(nlogn) O(nlogn) 的多项式乘法。
然而,多次递归导致程序的实际效率并不高,甚至过不了模板题。所以,我们考虑迭代实现 FFT。

正难则反,如果我们可以正向分割问题处理,为何不可以倒着合并得出答案呢?
同样地,如果觉得抽象可以看图。
在这里插入图片描述
不妨令 a a a 表示初始数组, b b b 表示分治完后的数组, r e v i rev_i revi 表示 i i i 二进制位反转后所得到的数。
通过大眼观察法,可以得到: a i = b r e v i a_i=b_{rev_i} ai=brevi,因此,我们可以通过 b b b 数组逆推得到答案。

NTT

某些题目要求我们在模意义下输出答案,这个时候,FFT便可能会产生较大的精度误差。为了解决这类模数问题,NTT 应运而生。

FFT 无法在计算中途取模,归咎于我们代入了复数意义下的单位根。因此,我们可以规避掉单位根,转而代入模意义下与单位根有着相似性质的数,这便是 NTT。
我们通常将这个数称为 原根( g g g)

注意到 FFT 处理的数组的长度一定是 2 2 2 的整次幂,因此,我们的模数必须满足 p = k 2 x + 1 p=k2^x+1 p=k2x+1 的苛刻条件。
常见地, 998244353 , 469762049 , 1004535809 998244353,469762049,1004535809 998244353,469762049,1004535809 的原根是 3 3 3

于是,我们只需要将 FFT 中的 ω n \omega_n ωn 替换为原根即可。具体地,用 g n 1 g_n^1 gn1 满足 [ g n 1 ≡ g p − 1 n ( m o d p ) ] [g_n^1 \equiv g^{\frac{p-1}{n}} \pmod{p} ] [gn1gnp1(modp)] 替换 ω n \omega_n ωn,作逆运算的时候用逆元即可。

代码

给出我常用的 FFT,NTT模板。

#include<bits/stdc++.h>
#define db double 
// #define int long long
using namespace std;
const int MAXN = 1e6 + 5;
const db Pi = acos(-1.0);
namespace poly{
    const int MOD = 998244353;
    int to[MAXN] , G0 = 3 , G1 = 332748118 , wn[MAXN][2];
    int qpow(int base , int k) {
        int res = 1;
        while(k) {
            if (k & 1) res = res * base % MOD;
            base = base * base % MOD;
            k >>= 1;
        }
        return res;
    }
    int inv(int p) {return qpow(p , MOD - 2);}
    struct Complex{
        db x , y;
        Complex() {}
        Complex(db _x , db _y) {x = _x , y = _y;}
        friend Complex operator + (Complex a , Complex b) {return Complex(a.x + b.x , a.y + b.y);};
        friend Complex operator - (Complex a , Complex b) {return Complex(a.x - b.x , a.y - b.y);};
        friend Complex operator * (Complex a , Complex b) {return Complex(a.x * b.x - a.y * b.y , a.x * b.y + a.y * b.x);};
        friend Complex operator / (Complex a , db b) {return Complex(a.x / b , a.y / b);};
        friend Complex operator * (Complex a , db b) {return Complex(a.x * b , a.y * b);};
    }a[MAXN] , b[MAXN] , q1[MAXN] , q2[MAXN] , q3[MAXN];
    void init(int len) {
        int lg = __lg(len) + 1 ,limit = (1 << lg);
        for (int i = 0 ; i < limit ; i ++) to[i] = (to[i >> 1] >> 1) | ((i & 1) << (lg - 1));
        wn[lg][1] = qpow(G1 , (MOD - 1) / limit);
        wn[lg][0] = qpow(G0 , (MOD - 1) / limit);
        for (int i = lg ; i >= 1 ; i --) {
            wn[i - 1][0] = wn[i][0] * wn[i][0] % MOD;
            wn[i - 1][1] = wn[i][1] * wn[i][1] % MOD;
        }
    }
    void FFT(Complex *A , int up , int type){
        for (int i = 0 ; i < up ; i ++) {
            if (i > to[i]) swap(A[i] , A[to[i]]);
        }
        for (int mid = 1 ; mid < up ; mid <<= 1) {
            Complex Wn(cos(Pi / mid) , type * sin(Pi / mid));
            for (int len = mid << 1 , j = 0 ; j < up ; j += len) {
                Complex W(1 , 0);
                for (int i = 0 ; i < mid ; i ++ , W = W * Wn) {
                    Complex tmpx = A[j + i] , tmpy = W * A[j + mid + i];
                    A[j + i] = tmpx + tmpy , A[j + mid + i] = tmpx - tmpy;
                }
            }
        }
    }
    void NTT(int *A , int up , int type) {
        for (int i = 0 ; i < up ; i ++) {
            if (i > to[i]) swap(A[i] , A[to[i]]);
        }
        for (int mid = 1 , lg = 1 ; mid < up ; mid <<= 1 , lg ++) {
            int Wn = wn[lg][type];
            for (int len = mid << 1 , j = 0 ; j < up ; j += len) {
                int W = 1;
                for (int i = 0 ; i < mid ; i ++ , W = W * Wn % MOD) {
                    int tmpx = A[j + i] , tmpy = W * A[j + mid + i] % MOD;
                    A[j + i] = (tmpx + tmpy) % MOD , A[j + mid + i] = (tmpx - tmpy + MOD) % MOD;
                }
            }
        }
        if (!type) return;
        int inv = qpow(up , MOD - 2);   
        for (int i = 0 ; i < up ; i ++) A[i] = A[i] * inv % MOD;
    }
    void FFT_mul(int n , db *F , int m , db *G , db *res) {
        init(n + m);
        int up = __lg(n + m) + 1;
        up = (1 << up);
        for (int i = 0 ; i <= up ; i ++) a[i].x = a[i].y = b[i].x = b[i].y = 0;
        for (int i = 0 ; i <= n ; i ++) a[i].x = F[i];
        for (int i = 0 ; i <= m ; i ++) b[i].x = G[i];
        FFT(a , up , 1) , FFT(b , up , 1);
        for (int i = 0 ; i <= up ; i ++) a[i] = a[i] * b[i];
        FFT(a , up , -1); 
        for (int i = 0 ; i <= n + m ; i ++) res[i] = a[i].x;
    }
    void NTT_mul(int n , int *F , int m , int *G , int *res) {
        init(n + m);
        int up = __lg(n + m) + 1;
        up = (1 << up);
        NTT(F , up , 0) , NTT(G , up , 0);
        for (int i = 0 ; i < up ; i ++) F[i] = F[i] * G[i] % MOD;
        NTT(F , up , 1);
        for (int i = 0 ; i < up ; i ++) res[i] = F[i];
    }
}
int n , m;
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr) , cout.tie(nullptr);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值