2023.2.9【数学】快速傅里叶变换

2023.2.9【模板】快速傅里叶变换(FFT)

好多天没写博客了qwq

题目描述

给定一个 n 次多项式 F(x),和一个 m 次多项式 G(x)。

请求出 F(x) 和 G(x) 的卷积。

朴素(正常)思路

枚举计算的每一位,交叉相乘加起来计算答案,时间复杂度O(n2)

原地爆炸

这个时候就需要用到NOIP不考的FFT了

前置芝士

有关复数详见lxw (tqqqqqqqqqqqqqqqqqql%%%%%%%%%%%%%)巨佬的博客

复数 - Ricky2007 - 博客园 (cnblogs.com)

*泰勒展开

泰勒中值定理:若f(x)x0处有n阶导数,那么存在x0邻域中的x,有

f(x)=f(x0)+f(x0)(xx0)+f(x0)2(xx0)2+...+f(n)(x0)n!(xx0)n+o((xx0)n)

泰勒展开经常用于将一个难以计算的函数逼近(或等效)于一个多项式,为了消掉(xx0)

,我们经常将函数在0处展开,即x0=0

*欧拉公式

eix=isinx+cosx

证明:将三个柿子泰勒展开可以得到:

ex=1+x+12!x2+13!x3+...+1n!xn

sinx=x13!x3+15!x517!x7+...

cosx=112!x2+14!x416!x6+...

我们(神奇地)发现:sinx+cosx=1+x12!x213!x3+14!x4+15!x5...

这难道不和ex长得很像吗?

为了将ex中一定项转为负号,我们引入i=1 :

eix=1+ix12!x13!ix3+....

isin x+cosx=1+ix12!x13!ix3...

eix=isin x+cosx

特殊地,代入x=π得:eiπ=isinπ+cosπ=1

这不就是那个被很多人当作壁纸的公式吗,我们来思考它的实际作用

在一个笛卡尔坐标系中,一个从源点指向(x,y)的向量,可以使用与x轴夹角θ和长度ρ来表达(极坐标),即(θ,ρ)

image

我们将Y坐标指定为复数域,X坐标指定为实数域,发现x + yi可以表示一个点

(ρisinθ+ρcosθ)

这样以来,再使用欧拉公式转化,就得到了:ρeiθ

一个单项就可以表达一个坐标,省去许多计算麻烦

*离散傅里叶级数

对于序列<c0,c1,c2....cn1> ,定义其k次离散傅里叶级数(DF)为:

h(ωk)=c0+c1ωk+c2ω2k+...+cn1ω(n1)k

其中ω=e2 iπn

可以看作在极坐标内,一个单位角度是2πn,每次走k单位的角度,一圈是n单位角度

image

(B站神犇up@3Blue1Brown的示意图)

*离散傅里叶变换 DFT

对于序列<c0,c1,c2,....cn1>,构造一个新的序列:

<h(ω0),h(ω1),h(ω2),...,h(ωn1)>

这个过程叫做离散傅里叶变换,反之,我们称对于一个构造好的序列,求原序列的过程叫做的逆傅里叶变换(IFT)

*定理:一个序列<c0,c1,...,cn1>的傅里叶变换后的新序列<h(ω0),h(ω1),...,h(ωn1)>(k)次DF值恰为原序列ckn

证明:设h(ωk)=Σx=0n1ωkxcx,g(ωk)=Σy=0n1ωkyh(ωy)

代入h(ωy)得:g(ωk)=Σy=0n1ωkyΣx=0n1ωxycx

提出求和符号:g(ωk)=Σy=0n1Σx=0n1ω(xk)ycx

=Σx=0n1cxΣy=0n1ω(xk)y

Key1. x=k时:

Σy=0n1ω(xk)y=Σy=0n11=n

Key2.xk时:

Σy=0n1[ω(xk)]y=[ω(xk)]0+[ω(xk)]1+...+[ω(xk)]n1=1[ ω(xk) ]n1ω(xk)

为什么x0+x1+x2+...+xn1=1xn1x

设其为f(x),将上式乘x,可以得到

x1+x2+...+xn=f(x)1+xn=xf(x)

(x1)f(x)=xn1,f(x)=xn1x1

ω=e2 iπn[ω(xk)]n=(ωn)xk=1xk=1

Σy=0n1[ωxk]y=111ωxk=0,不计入答案

g(ωk)=Σx=0n1cx[(x=k)?n:0]=nck

QED

通过*****,我们就可以构造出一种计算多项式乘法的新方法:

1.将F(x)和G(x)进行傅里叶变换

2.将两者点乘(x[0,n1]上的值分别相乘)

3.将所得序列进行(k)次方逆傅里叶变换,结果除以n

O(n2)

怎样加快计算过程?

考虑多项式h(x)=c0+c1x+...+cn1xn1

提出奇数odd(x)=c1+c3x+c5x2+...+cn1xn21

偶数even(x)=c0+c2x+c4x2+...+cn2xn21

可推得合并式h(x)=even(x2)+xodd(x2)

代入:h(ωk)=even(ω2k)+ωkodd(ω2k)

可以发现:h(ωk+n2)=even(ω2k+n)+ωk+n2odd(ω2k+n)

=even(ω2kωn)+ωk+n2odd(ω2kωn)

ω=e2 iπnωn=1,ωn2=1

h(ωk+n2)=even(ω2k)ωkodd(ω2k)

h(ωk)=even(ω2k)+ωkodd(ω2k)

其中通过ω2k,我们将要计算的h(ωk)h(ωk+n2)共计n项,转化成evenodd共计n项,每次将序列拆成奇项和偶项两部分,共拆log2n层,共计时间复杂度O(nlog2n)

FFT快速傅里叶变换

合并过程图解:例:n=8
image
小技巧:怎样快速将序列拆成奇项和偶项?

考虑到我们要将偶项也拆成其中的奇和偶、再拆为奇和偶...我们可以发现,每次拆项都是按照数字二进制的第K位排序,最后的顺序就是将序列按照最低位为第一关键字,次低位为第二关键字...来排序(或者说是拆分数组),一个数的排名就是它二进制拆分的倒序,于是我们在程序开始预处理每个数字的二进制翻转:

revi=[(revi>>1)>>1]|(i&1)<<d

(其中d为最大数的二进制总位数)

FFT开始前预处理一遍数组,若i<rev[i],则swap(x[i],x[rev[i]])即可,这样可以做到不重不漏地将每个数翻转一次,然后直接按照下标分治即可

Code

#include<bits/stdc++.h>
using namespace std;
const int N = 3e6 + 5;
const double PI = acos(-1.0);
int n,m,rev[N],tt = 1,tw = 0;
struct Complex{
    double r,c;
}a[N],b[N];
Complex operator +(Complex x,Complex y)
{
    Complex z;
    z.r = x.r + y.r;
    z.c = x.c + y.c;
    return z;
}
Complex operator -(Complex x,Complex y)
{
    Complex z;
    z.r = x.r - y.r;
    z.c = x.c - y.c;
    return z;
}
Complex operator *(Complex x,Complex y)
{
    Complex z;
    z.r = x.r * y.r - x.c * y.c;
    z.c = x.c * y.r + x.r * y.c;
    return z;
}
inline int read()
{
    int s = 0,w = 1;
    char k = getchar();
    while(k > '9' || k < '0')
    {
        if(k == '-') w = -w;
        k = getchar();
    }
    while(k <= '9' && k >= '0')
    {
        s = s * 10 + k - '0';
        k = getchar();
    }
    return s * w;
}
inline void FFT(Complex *x,int len,int type)
{
    for(int i = 0;i < tt;i++) if(i < rev[i]) swap(x[i],x[rev[i]]);
    for(int mid = 1;mid < tt;mid <<= 1)
    {
        Complex omega;omega.r = cos(PI / mid);omega.c = type * sin(PI / mid);
        for(int j = 0,R = mid << 1;j < tt;j += R)
        {
            Complex now;now.c = 0;now.r = 1;
            for(int k = j;k < j + mid;k++,now = now * omega)
            {
                Complex X = x[k],Y = x[k + mid];
                x[k] = X + now * Y;
                x[k + mid] = X - now * Y;
            }
        }
    }
}
int main()
{
    n = read();m = read();
    for(int i = 0;i <= n;i++) a[i].r = read();
    for(int i = 0;i <= m;i++) b[i].r = read();
    while(tt <= n + m) tt <<= 1,tw++;
    rev[0] = 0;
    for(int i = 1;i < tt;i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (tw - 1));
    FFT(a,n,1);FFT(b,m,1);
    for(int i = 0;i <= tt;i++) a[i] = a[i] * b[i];
    FFT(a,n,-1);
    for(int i = 0;i <= n + m;i++)
        printf("%d ",(int)(a[i].r / tt + 0.5));
    return 0;
}

买一送一环节

我们知道,FFT中暴力计算ω的值,虽然使用了double,但是精度仍然会丢失很多,所以我们就要对其进行魔改 NTT 快速数论变换

考虑到在FFT中,每一层的单位乘积是ωk,考虑用原根替换这个东西

考虑到答案中的每个数不会很大,我们将答案模一个素数,就可以利用它的原根

(在int范围内,我们一般取998244353,其最小原根是3,原根逆元为332748118,并且

998244353=119223+1,足够我们完成项数在1e6以内的运算

于是我们每一步将ωk换作g(p1)k即可(k表示当前枚举的部分长度),作为单位乘积每次乘上。

(这就是为什么需要P1有很多个2因子)

Code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 3e6 + 5,MOD = 998244353,g = 3,gi = 332748118;
inline ll ksm(ll base,ll pts)
{
    ll ret = 1;
    for(;pts > 0;pts >>= 1,base = base * base % MOD)
        if(pts & 1)
            ret = ret * base % MOD;
    return ret;
}
int rev[N];
ll a[N],b[N],n,m,tt = 1,tw = 0;
inline void NTT(ll *x,ll type)
{
    for(int i = 0;i < tt;i++) if(i < rev[i]) swap(x[i],x[rev[i]]);
    for(int mid = 1,t = 0;mid < tt;mid <<= 1,t++)
    {
        ll dom = ksm((type == 1) ? g : gi,(MOD - 1) >> (t + 1));
        for(int j = 0,R = mid << 1;j < tt;j += R)
        {
            ll w = 1;
            for(int k = j;k < j + mid;k++,w = w * dom % MOD)
            {
                ll X = x[k],Y = x[k + mid] * w % MOD;
                x[k] = (X + Y) % MOD;
                x[k + mid] = (X - Y + MOD) % MOD;
            }
        }
    }
}
int main()
{
    cin>>n>>m;
    for(int i = 0;i <= n;i++) cin>>a[i];
    for(int i = 0;i <= m;i++) cin>>b[i];
    while(tt <= n + m) tt <<= 1,tw++;
    for(int i = 0;i < tt;i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (tw - 1));
    NTT(a,1);NTT(b,1);
    for(int i = 0;i <= tt;i++) a[i] = a[i] * b[i] % MOD;
    NTT(a,-1);
    ll inv = ksm(tt,MOD - 2);
    for(int i = 0;i <= n + m;i++) cout<<a[i] * inv % MOD<<" ";
    return 0;
}
posted @   The_Last_Candy  阅读(96)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示