FFT

FFT 是一种高效实现 DFT 和 IDFT 的方式,可以在 \(O(n \log n)\) 的时间内求多项式的乘法。

多项式的点值表示

不同于用每项的系数来表示一个多项式,我们知道对于给定的 \(n+1\) 个点值,可以确定唯一的 \(n\) 次多项式。这种用点值表示多项式的方法叫点值表示法。

如果知道 \(F(x)\) 和 \(G(x)\) 的点值表示,求出 \(F(x)\times G(x)\) 的点值表示是 \(O(n+m)\) 的。复杂度瓶颈变成了如何快速转换多项式的两种表示形式。

把系数转成点值的算法叫 离散傅里叶变换(DFT),相应地,把点值转成系数的算法叫 逆离散傅里叶变换(IDFT)

DFT

前置知识:单位根

OIWiki-复数-单位根

已经说过,要选取 \(n\) 个点并求出它们对应 \(F(x)\) 的值。先观察 \(F(x)\) 的性质:

\[F(x)=a_0+a_1x+a_2x^2+a_3x^3+a_4x^4+a_5x^5+a_6x^6+a_7x^7
\]

把奇偶项分开,得

\[F(x)=(a_0+a_2x^2+a_4x^4+a_6x^6)+(a_1x+a_3x^3+a_5x^5+a_7x^7)\\
F(x)=(a_0+a_2x^2+a_4x^4+a_6x^6)+x(a_1+a_3x^2+a_5x^4+a_7x^6)\\
F(x)=G(x^2)+x\times H(x^2)
\]

似乎看起来对每个 \(x\) 求 \(G\) 和 \(H\),要求的还是 \(n\) 个值。但这时单位根的性质就能用上了:

令 \(x\) 分别为 \(\omega_n^1,\omega_n^2,...,\omega_n^n\),那么有:

\[\begin{aligned}
F(\omega_n^k)&=G(\omega_n^{2k})+x\times H(\omega_n^{2k})\\
&=G(\omega_{n/2}^k)+x\times H(\omega_{n/2}^k)

\end{aligned}
\]

根据 \(\omega_n^{k}=-\omega_n^{k+n/2}\),且 \(G,H\) 均为偶函数,可以得到:

\[F(\omega_n^{n/2+k})=G(\omega_{n/2}^{k})-x\times H(\omega_{n/2}^{k})
\]

发现这是好的,因为只用求前一半的 \(G\) 和 \(H\) 的值就可以了,把它们两个分别递归处理即可。

但是 FFT 只能对次数为 \(2^m\) 的多项式这么做,所以如果多项式不是恰好 \(2^m\) 项,需要在后面用一些系数为 \(0\) 的项把它补满。

IDFT

想完成多项式乘法,还要把点值表示重新转换回系数表示,这个过程叫 IDFT。

构造方法实在过于神秘,以至于菜猫猫并不能理解这个思路的由来,所以这里直接给出做法:

设对 \(F(x)\) 进行 DFT 后得到点值表示 \(y_0,y_1,...y_{n-1}\),构造 \(G(x)=y_0+y_1x+...+y_{n-1}x^{n-1}\)。

将 \(x=\omega_n^0,\omega_n^{-1}...\omega_n^{-(n-1)}\) 代入,得:

\[\begin{aligned}
G(\omega_n^{-k})&=\sum\limits_{i=0}^{n-1} y_i \omega_n^{-ki}\\
&=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1} a_j\times (\omega_n^i)^j\times \omega_n^{-ki}\\
&=\sum_{j=0}^{n-1}a_j \sum_{i=0}^{n-1}(\omega_n^{j-k})^i
\end{aligned}
\]

那么当且仅当 \(j=k\),\(G=a_k\times n\)。因此对 \(G(x)\) 代入 \(x=\omega_n^0,\omega_n^{-1}...\omega_n^{-(n-1)}\) 求 DFT,结果除以 \(n\) 即为多项式的各项系数。

实现上可以设定一个参数为 \(1\) 或 \(-1\) 传入 DFT 函数,减小码量。

蝶形运算

一个很诈骗的东西。不要被网上这种图吓到了:



通俗地讲,其实就是,我们通过 \(G(\omega_{n/2}^k)\) 和 \(H(\omega_{n/2}^k)\) 求 \(F(\omega_{n}^k)\)、\(F(\omega_{n}^{k+n/2})\) 时,本来需要额外开一个数组;

但你发现 \(G,H\) 存储的位置和你要把 \(F\) 求完存储的位置是同一个,且求其他位置 \(F\) 的值时也不再用到这两个数据,一边求一边覆盖是不影响正确性的。

讲完了,跟蝶形确实没什么关系对吧。

实现-递归版

众所周知 c++ 自带的 complex 类常数极大,建议手写。

至此,我们可以写出朴素的递归版本 FFT 了:

递归 FFT
void FFT(int limit,Complex a[],int op)
{
if(limit==1) return;
Complex a1[(limit>>1)+5],a2[(limit>>1)+5];
for(int i=0;i<=limit;i+=2) a1[i>>1]=a[i],a2[i>>1]=a[i+1];
FFT(limit>>1,a1,op),FFT(limit>>1,a2,op);
Complex Wn={cos(2.0*pai/limit),sin(2.0*pai/limit)*op},w={1,0};
for(int i=0;i<(limit>>1);i++,w=w*Wn)
{
a[i]=a1[i]+w*a2[i];
a[i+(limit>>1)]=a1[i]-w*a2[i];
}
}
int main()
{
n=read(),m=read();
for(int i=0;i<=n;i++) a[i].x=read();
for(int i=0;i<=m;i++) b[i].x=read();
int w=1;while(w<=m+n) w<<=1;
FFT(w,a,1),FFT(w,b,1);
for(int i=0;i<=w;i++) a[i]=a[i]*b[i];
FFT(w,a,-1);
for(int i=0;i<=n+m;i++) printf("%d ",(int)(a[i].x/w+0.5));
return 0;
}

位逆序置换

递归的常数太大了,接下来讲讲怎么不递归。

考虑每次把系数按奇偶分开的过程,即把下标以二进制的最后一位为关键字排序。也就是说,把二进制倒过来看,如果 \(i\) 按位翻转后为 \(j\),那么下标为 \(i\) 的数操作到最后就会跑到位置 \(j\) 上。

这启发我们使用迭代实现,预处理出每个系数最后所在的位置,直接从底层倒推即可。

const int N=4e6+5;
const double pi=acos(-1);
int n,m,limit=1;
struct Complex {double x,y;}a[N],b[N];
il Complex operator +(Complex a,Complex b) {return {a.x+b.x,a.y+b.y};}
il Complex operator -(Complex a,Complex b) {return {a.x-b.x,a.y-b.y};}
il Complex operator *(Complex a,Complex b) {return {a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x};}
int to[N];
void FFT(Complex *a,int tp)
{
for(int i=0;i<limit;i++) if(i<to[i]) swap(a[i],a[to[i]]);
for(int len=1;len<limit;len<<=1)
{
Complex Wn={cos(pi/len),sin(pi/len)*tp};
for(int i=0;i<limit;i+=(len<<1))
{
Complex w={1,0};
for(int j=0;j<len;j++,w=w*Wn)
{
Complex x=a[i+j],y=w*a[i+len+j];
a[i+j]=x+y,a[i+len+j]=x-y;
}
}
}
}
int main()
{
n=read(),m=read();
for(int i=0;i<=n;i++) a[i].x=read();
for(int i=0;i<=m;i++) b[i].x=read();
int k=0; while(limit<=n+m) limit<<=1,k++;
for(int i=0;i<limit;i++) to[i]=(to[i>>1]>>1)|((i&1)<<(k-1));
FFT(a,1),FFT(b,1);
for(int i=0;i<limit;i++) a[i]=a[i]*b[i];
FFT(a,-1);
for(int i=0;i<=n+m;i++) printf("%d ",(int)(a[i].x/limit+0.5));
return 0;
}

三次变两次优化

上文中,进行两个多项式相乘时,我们需要调用三次 FFT 函数:对 \(F\) 和 \(G\) 分别 DFT,对 \(F\times G\) IDFT。这么做是不够优的。

这里就可以用到三次变两次优化的技巧,使常数减小三分之一。

具体地,我们在计算前直接把 \(G\) 放在 \(F\) 的虚部上,乘法操作变为计算 \(F(x)\) 平方。

答案就是 \(F^2(x)\) 的虚部除以 \(2\)。

正确性证明是容易的:\((a+b\text{i})^2=(a^2-b^2)+2ab\text{i}\)。

在主函数上稍作改动即可:

int main()
{
n=read(),m=read();
for(int i=0;i<=n;i++) a[i].x=read();
for(int i=0;i<=m;i++) a[i].y=read();//here
int k=0; while(limit<=n+m) limit<<=1,k++;
for(int i=0;i<limit;i++) to[i]=(to[i>>1]>>1)|((i&1)<<(k-1));
FFT(a,1);
for(int i=0;i<limit;i++) a[i]=a[i]*a[i];//and here
FFT(a,-1);
for(int i=0;i<=n+m;i++) printf("%d ",(int)(a[i].y/2/limit+0.5));
return 0;
}

NTT

前置知识-原根

:使得 \(a^n\equiv1\pmod{m}\) 的最小正整数 \(n\) 称为 \(a\) 模 \(m\) 的阶。

原根:若 \(\gcd(g,m)=1\),且 \(\delta_m(g)=\varphi(m)\),则 \(g\) 是 \(m\) 的原根。

存在定理 一个数 \(m\) 存在原根当且仅当 \(m=2,4,p^{\alpha},2p^{\alpha}\),其中 \(p\) 为奇素数,\(\alpha \in \mathbb{N}^{*}\)。

判定定理 设 \(m\ge 3,\gcd(a,m)=1\),则 \(a\) 是模 \(m\) 的原根的充要条件是,对于 \(\varphi (m)\) 的每个质因数 \(p\),都有 \(a^{\frac{\varphi(m)}{p}}\not\equiv 1\pmod m\)。

原根个数 若一个数有原根,则它原根的个数为 \(\varphi(\varphi(m))\)。

以上结论的证明:link

NTT 用来解决模意义下的多项式乘法问题,其运算均为整数,在常数和精度方面均优于 FFT。(划掉,实测还是 FFT 跑得快。

原根的性质与单位根类似,因此我们把 FFT 中所有 \(\omega_n\) 替换成 \(g^{\frac{p-1}{n}}\),乘 \(-1\) 换成乘逆元即可。

NTT 的常见模数是 \(998244353\),它的原根是 \(3\)。

为什么我的 NTT 跑得比 FFT 慢啊 /dk

#define int long long
const int N=4e6+5,mod=998244353;
il int qpow(int n,int k=mod-2)
{
int res=1;
for(;k;n=n*n%mod,k>>=1) if(k&1) res=res*n%mod;
return res;
}
int n,m,a[N],b[N],inv=qpow(3),limit=1,to[N];
il void NTT(int *a,int tp)
{
for(int i=0;i<limit;i++) if(i<to[i]) swap(a[i],a[to[i]]);
for(int len=1;len<limit;len<<=1)
{
int Wn=qpow(tp>0?3:inv,(mod-1)/(len<<1));
for(int i=0;i<limit;i+=(len<<1))
for(int j=0,w=1;j<len;j++,w=w*Wn%mod)
{
int x=a[i+j],y=w*a[i+len+j]%mod;
a[i+j]=(x+y)%mod,a[i+len+j]=(x-y+mod)%mod;
}
}
}
signed main()
{
n=read(),m=read();
for(int i=0;i<=n;i++) a[i]=read();
for(int i=0;i<=m;i++) b[i]=read();
int k=0; while(limit<=m+n) limit<<=1,k++;
for(int i=0;i<limit;i++) to[i]=(to[i>>1]>>1)|((i&1)<<(k-1));
NTT(a,1),NTT(b,1);
for(int i=0;i<limit;i++) a[i]=a[i]*b[i]%mod;
NTT(a,-1);
for(int i=0;i<=n+m;i++) printf("%lld ",a[i]*qpow(limit)%mod);
return 0;
}

FFT & NTT 及其简单优化的更多相关文章

  1. [学习笔记&教程] 信号, 集合, 多项式, 以及各种卷积性变换 (FFT,NTT,FWT,FMT)

    目录 信号, 集合, 多项式, 以及卷积性变换 卷积 卷积性变换 傅里叶变换与信号 引入: 信号分析 变换的基础: 复数 傅里叶变换 离散傅里叶变换 FFT 与多项式 \(n\) 次单位复根 消去引理 ...

  2. FFT&NTT数学解释

    FFT和NTT真是噩梦呢 既然被FFT和NTT坑够了,坑一下其他的人也未尝不可呢 前置知识 多项式基础知识 矩阵基础知识(之后会一直用矩阵表达) FFT:复数基础知识 NTT:模运算基础知识 单位根介 ...

  3. FFT/NTT复习笔记&多项式&生成函数学习笔记Ⅰ

    众所周知,tzc 在 2019 年(12 月 31 日)就第一次开始接触多项式相关算法,可到 2021 年(1 月 1 日)才开始写这篇 blog. 感觉自己开了个大坑( 多项式 多项式乘法 好吧这个 ...

  4. FFT/NTT/MTT学习笔记

    FFT/NTT/MTT Tags:数学 作业部落 评论地址 前言 这是网上的优秀博客 并不建议初学者看我的博客,因为我也不是很了解FFT的具体原理 一.概述 两个多项式相乘,不用\(N^2\),通过\ ...

  5. FFT&NTT总结

    FFT&NTT总结 一些概念 \(DFT:\)离散傅里叶变换\(\rightarrow O(n^2)\)计算多项式卷积 \(FFT:\)快速傅里叶变换\(\rightarrow O(nlogn ...

  6. [拉格朗日反演][FFT][NTT][多项式大全]详解

    1.多项式的两种表示法 1.系数表示法 我们最常用的多项式表示法就是系数表示法,一个次数界为\(n\)的多项式\(S(x)\)可以用一个向量\(s=(s_0,s_1,s_2,\cdots,s_n-1) ...

  7. 快速构造FFT/NTT

    @(学习笔记)[FFT, NTT] 问题概述 给出两个次数为\(n\)的多项式\(A\)和\(B\), 要求在\(O(n \log n)\)内求出它们的卷积, 即对于结果\(C\)的每一项, 都有\[ ...

  8. $FFT/NTT/FWT$题单&简要题解

    打算写一个多项式总结. 虽然自己菜得太真实了. 好像四级标题太小了,下次写博客的时候再考虑一下. 模板 \(FFT\)模板 #include <iostream> #include < ...

  9. HDU-4609(FFT/NTT)

    HDU-4609(FFT/NTT) 题意: 给出n个木棒,现从中不重复地选出3根来,求能拼出三角形的概率. 计算合法概率容易出现重复,所以建议计算不合法方案数 枚举选出的最大边是哪条,然后考虑剩下两条 ...

  10. FFT/NTT复习笔记&多项式&生成函数学习笔记Ⅲ

    第三波,走起~~ FFT/NTT复习笔记&多项式&生成函数学习笔记Ⅰ FFT/NTT复习笔记&多项式&生成函数学习笔记Ⅱ 单位根反演 今天打多校时 1002 被卡科技了 ...

随机推荐

  1. 【HDC.Cloud 2023】新鲜速递:从多元生态、开源到人才培养,让开发者成为决定性力量

    摘要:华为云开发者联盟邀您一起回顾大会精彩时刻. 本文分享自华为云社区<[HDC.Cloud 2023]新鲜速递:从多元生态.开源到人才培养,让开发者成为决定性力量>,作者: 华为云社区精 ...

  2. day-3 路由底层源码

    1. 定义路由本质 比如在url.py定义以下路由,浏览器中输入http://192.168.0.1:8000/user/2003-04-21可以访问 意味着此url http://192.168.0 ...

  3. 最为常用的Laravel操作(2)-路由

    基本路由 // 接收一个 URI 和一个闭包 Route::get('hello', function () { return 'Hello, Laravel'; }); // 支持的路由方法 Rou ...

  4. 玩转 PI 系列-如何在 Rockchip Arm 开发板上安装 Docker Tailscale K3s Cilium?

    概述 618 买了几个便宜的 Purple PI OH 开发板 (500 块多一点买了 3 个), 这个开发板类似树莓派,是基于 Rockchip(瑞芯微) 的 rx3566 arm64 芯片.如下: ...

  5. 我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱

    我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱 文档地址 https://xuejm.gitee.io/easy-query-doc/ GITHUB地址 https://github. ...

  6. Cilium系列-6-从地址伪装从IPtables切换为eBPF

    系列文章 Cilium 系列文章 前言 将 Kubernetes 的 CNI 从其他组件切换为 Cilium, 已经可以有效地提升网络的性能. 但是通过对 Cilium 不同模式的切换/功能的启用, ...

  7. 使用Docker安装Apollo并使用SpringBoot连接配置中心

    上篇文章我们学习了Apollo的本地安装,如果还不会本地安装的朋友可以查看之前的文章进行了解和学习链接地址(https://www.cnblogs.com/eternality/p/17583023. ...

  8. 好用工具: Carbon--代码美化工具

    问题背景 当进行PPT展示时,如果只是简单的复制粘贴代码.会看起来很难看,因此产生美化的想法. 使用方式 官网链接 https://carbon.now.sh

  9. SQL-去除最大值与最小值求均值的问题

    背景 今天有同事问我一道关于数据库SQL的面试题,我刚开始随便给了一个思路,后来思索发现这个思路有漏洞,于是总结下来,仅供参考. 问题: 薪水表中是员工薪水的基本信息,包括雇员编号,和薪水,查询除去最 ...

  10. Tcpdump 使用指南

    论网络数据包的分析,我首选wireshark,因为图形化界面直观明了.但如果遇到没有图形化显示的Linux环境,那么此时会使用tcpdump该是一件多么美好的事情. 网上关于tcpdump的介绍很多, ...