FFT——快速傅里叶变换(附NTT——快速数论变换)

FFT是一种能在O(nlogn)时空复杂度内求解两个函数卷积的优秀算法。

算法思想(DFT):

对于函数 \(f(x)=\Sigma_{i=0}^{n-1}c_ix^{ik}\) 构造一个函数,即k次离散傅里叶级数 \(h(w^k)\) ,其中 \(w\) 是单位元

\[h(w^k)=\Sigma_{j=0}^{n-1}c_jw^{jk}(w=e^{\frac{2i\pi}{n}})
\]

由于单位元 \(w\) 具有性质

\[\Sigma_{j=0}^{n-1}w^{j}=0
\]

所以我们求卷积时,可以直接将 \(f(x1)\) 的 \(h(x1)\) 与 \(f(x2)\) 的 \(h(x2)\) 相乘得到 \(h(x3)\),即为 \(f(x1)\) 与 \(f(x2)\) 的卷积 \(f(x3)\) 的k次离散傅里叶级数。

单位元的求法也很简单,可借用实复坐标系帮助求出:

\[w=cos(\frac{\pi}{n})+sin(\frac{\pi}{n})*i;
\]

现在我们需要将 \(h(x3)\) 转换回 \(f(x3)\) ,相当于对 \(h(x3)\) 进行一次逆傅里叶变换,类似的有结论:

\[c_k=\frac{1}{n}g(w^{-k})=\frac{1}{n}\Sigma_{j=0}^{n-1}h(w^{j})*w^{-jk}
\]

证明:由 \(h(x)\) 的定义得,

\[c_k=\frac{1}{n}\Sigma_{j=0}^{n-1}h(w^{j})*w^{-jk}=\frac{1}{n}\Sigma_{j=0}^{n-1}w^{-jk}\Sigma_{p=0}^{n-1}c_pw^{jp}=\frac{1}{n}\Sigma_{j=0}^{n-1}\Sigma_{p=0}^{n-1}c_pw^{j(p-k)}
\]

1、\(p=k\) 时,\(c_kw^{j(k-k)}=c_k\to\Sigma_{j=0}^{n-1}c_kw^{j(k-k)}=nc_k\)

2、\(p\ne k\) 时,\(\Sigma_{j=0}^{n-1}c_pw^{j(p-k)}=0\to\Sigma_{j=0}^{n-1}\Sigma_{p=0}^{n-1}c_pw^{j(p-k)}=0 (p\ne k)\)

综上,\(\Sigma_{j=0}^{n-1}\Sigma_{p=0}^{n-1}c_pw^{j(p-k)}=nc_k\),证毕。

算法优化(FFT):

显然,如果我们直接枚举 \(j\) 和 \(p\) ,时间复杂度为 \(O(n^2)\) ,与暴力复杂度相同,且常数更大(那我们为什么要学这个?),所以需要优化。

我们再次仔细观察公式

\[h(w^k)=\Sigma_{j=0}^{n-1}c_jw^{jk}(w=e^{\frac{i\pi}{n}})
\]

我们可以发现

\[h(w^k)=\Sigma_{j=0}^{(n-1)/2}c_{j*2}w^{2*j*k}+\Sigma_{j=0}^{n/2-1}c_{j*2+1}w^{(2*j+1)*k}
\]

所以可以构造两个函数

\[he(w^k)=\Sigma_{j=0}^{(n-1)/2}c_{j*2}w^{j*k},ho(w^k)=\Sigma_{j=0}^{n/2-1}c_{2*j+1}w^{j*k};
\]

原式改写为

\[h(w^k)=he(w^{2*k})+w*ho(w^{2*k})
\]

由于 \(he(x^{2*k})\) 与 \(ho(x^{2*k})\) 与 \(h(x^k)\) 同构,所以可以一起求解。

但是我们发现,如果每次都对两边求解,那么总时间复杂度 \(O(n^2)\) ,依然达不到要求。

继续观察

\[h(w^k)=he(w^{2*k})+w*ho(w^{2*k}),h(w^{k+n/2})=he(w^{2*k})+w^{n/2+1}*ho(w^{2*k});
\]

由欧拉公式

\[e^{i\pi}=1
\]

再由单位元定义

\[w^{n/2}=e^{\frac{i\pi}{2}}=-1
\]

所以

\[h(w^k)=he(w^{2*k})+w*ho(w^{2*k}),h(w^{k+n/2})=he(w^{2*k})-w*ho(w^{2*k});
\]

由于 \(h(w^k)\) 与 \(h(w^{k+n/2})\) 可以一起求出,所以可以只对一边求解,时间复杂度降为O(nlogn)

所以有如下递归写法

const double pi=acos(-1.0);//π
struct node//复数
{
double x,y;//x是实部,y是虚部
node (double xx=0,double yy=0)
{
x=xx;
y=yy;
}
};
node operator + (node a,node b) {return node(a.x+b.x,a.y+b.y);}
node operator - (node a,node b) {return node(a.x-b.x,a.y-b.y);}
node operator * (node a,node b) {return node(a.x*b.x-a.y*b.y,a.x*b.y+b.x*a.y);}//复数加,减,乘法
void dfs(node *a,int len)
{
if(len==0)
return;
node a1[(len>>1)+1],a2[(len>>1)+1];
for(int i=0;i<=len;i+=2)
{
a1[i>>1]=a[i];
a2[i>>1]=a[i+1];
}
dfs(a1,len>>1);
dfs(a2,len>>1);
node w(cos(pi/len),sin(pi/len)),wn(1,0);
for(int i=0;i<=(len>>1);i++)
{
a[i]=a1[i]+wn*a2[i];
a[i+(len>>1)]=a1[i]-wn*a2[i];
wn=wn*w;
}
}

此时,该算法的时空复杂度已经达到 \(O(nlogn)\) ,已经可以通过大部分题目,但是观察到递归写法多开临时数组且常数较大,遇到某些(毒瘤)数据很强的题,有爆栈或超时的风险,所以继续优化。

考虑观察最后序列序号与原序号。

0 1 2 3 4 5 6 7
0 4 2 6 1 5 3 7

我们再把他们写成二进制

000 001 010 011 100 101 110 111
000 100 010 110 001 101 011 111

不难发现,两组序号互为二进制反转之后的数。所以我们可以直接跳过划分下标奇偶的步骤,直接先将序列排成最后的样式,再用非递归方式计算。

计算二进制反转数的式子:

\[rev[i]=(rev[i>>1]>>1)|((i\&1)\&\&(1<<len))
\]

解释:1、如果 \(x\) 是偶数,那么 \(x\) 可以写成 \((x>>1)<<1\) 的形式,对应到反转数上就是 \(rev[i>>1]>>1\)

​ 2、如果 \(x\) 是奇数,那么 \(x\) 可以写成 \(((x>>1)<<1)|1\) 的形式,对应到反转数上就是 \((rev[i>>1]>>1)|(1<<len)\)

\((len是所有数二进制最高位的下一位的位数)\)

直接贴出代码:

#include<bits/stdc++.h>
using namespace std;
const double pi=acos(-1.0);//π
struct node
{
double x,y;
node (double xx=0,double yy=0)
{
x=xx;
y=yy;
}
};
node a[4000005],b[4000005];
node operator + (node a,node b) {return node(a.x+b.x,a.y+b.y);}
node operator - (node a,node b) {return node(a.x-b.x,a.y-b.y);}
node operator * (node a,node b) {return node(a.x*b.x-a.y*b.y,a.x*b.y+b.x*a.y);}
int n,m,limit=1,rev[4000005];
void FFT(node *a,int type)//快速傅里叶变换
{
for(int i=0;i<limit;i++)
if(i<rev[i])//防止变回去
swap(a[i],a[rev[i]]);//将序列变成最终样式
for(int k=2;k<=limit;k<<=1)
{
node wn(cos(pi/(k>>1)),type*sin(pi/(k>>1)));
for(int i=0;i<limit;i+=k)
{
node w(1.0,0.0);
for(int j=i;j<i+(k>>1);j++)
{
node x=a[j],y=a[j+(k>>1)];
a[j]=x+w*y;
a[j+(k>>1)]=x-w*y;
w=w*wn;
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<=n;i++)
scanf("%lf",&a[i].x);
for(int i=0;i<=m;i++)
scanf("%lf",&b[i].x);
while(limit<=n+m)
limit<<=1;//最高位的下一位
for(int i=0;i<limit;i++)
{
if((i&1)==0)
rev[i]=rev[i>>1]>>1;
else
rev[i]=(rev[i>>1]>>1)|(limit>>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("%lf ",a[i].x/limit);//千万不要忘了1/n这个系数
}

最后,我们来证明为什么到最后所有数虚部都变成0

证明:

​ 不需要证明。

​ 考虑两个原函数的系数皆为实数,它们卷积的系数也必为实数。

​ 证毕。

题目模板:

P3803 [模板] 多项式乘法 (FFT)

NTT——FFT的进阶版

考虑在FFT中,我们选用 \(e^{\frac{i\pi}{n}}\) 这个复数来作为单位元,这种方法固然巧妙,但缺点就是容易丢失精度,对于取余类问题也很不好处理,复数运算的常数也较大,所以我们想到在整数中寻找这个 \(w\)

考虑在整数域中,有什么具有与 \(w\) 相同的性质呢?

(快速翻找中。。。。。。)(掏出一本积灰的笔记本。。。。。。)(打开。。。)

显然我们可以想到原根(对于原根的资料请自行查找,本文重点不在于此)

考虑原根的几条性质,都和 \(w\) 要求的完全相同。

所以我们直接用原根替换掉上述代码中的 \(w\) 就OK了。

如果是取余类问题,直接取模数的原根即可;对于非取余类且数字上界较小的问题(如1e9),可以直接用一个比较大的质数当做模数(实际上根本没有取模)(推荐998244353,原根是3)

但是由于单位元的指数是 \((mod-1)/((mod-1)/2^k)\) 有可能有一些(毒瘤) 质数,由于 \(mod-1\) 的 2 的质因子个数不够多,出现指数是小数的情况,就会出错(依然推荐998244353,998244352的2的质因子个数足足23个,支持2e6左右的数据)(差评1e9+7,1e9+6的2的质因子个数只有1,分分钟WA掉)。如果有些 核癌氪氢 的题目,规定这样的数作模数,就会很难办。(不过因为本人是一名蒟蒻,还没遇见过这种题目,评论大佬知道的戳我一下)

FFT及NTT的更多相关文章

  1. 多项式乘法,FFT与NTT

    多项式: 多项式?不会 多项式加法: 同类项系数相加: 多项式乘法: A*B=C $A=a_0x^0+a_1x^1+a_2x^2+...+a_ix^i+...+a_{n-1}x^{n-1}$ $B=b ...

  2. FFT和NTT学习笔记_基础

    FFT和NTT学习笔记 算法导论 参考(贺) http://picks.logdown.com/posts/177631-fast-fourier-transform https://blog.csd ...

  3. fft,ntt总结

    一个套路:把式子推成卷积形式,然后用fft或ntt优化求解过程. fft的扩展性不强,不可以在fft函数里多加骚操作--DeepinC T1:多项式乘法 板子题 T2:快速傅立叶之二 另一个板子,小技 ...

  4. 多项式fft、ntt、fwt 总结

    做了四五天的专题,但是并没有刷下多少题.可能一开始就对多项式这块十分困扰,很多细节理解不深. 最简单的形式就是直接两个多项式相乘,也就是多项式卷积,式子是$N^2$的.多项式算法的过程就是把卷积做一种 ...

  5. FFT与NTT专题

    先不管旋转操作,考虑化简这个差异值 $$begin{aligned}sum_{i=1}^n(x_i-y_i-c)^2&=sum_{i=1}^n(x_i-y_i)^2+nc^2-2csum_{i ...

  6. 卷积FFT、NTT、FWT

    先简短几句话说说FFT.... 多项式可用系数和点值表示,n个点可确定一个次数小于n的多项式. 多项式乘积为 f(x)*g(x),显然若已知f(x), g(x)的点值,O(n)可求得多项式乘积的点值. ...

  7. 多项式的基本运算(FFT和NTT)总结

    设参与运算的多项式最高次数是n,那么多项式的加法,减法显然可以在O(n)时间内计算. 所以我们关心的是两个多项式的乘积.朴素的方法需要O(n^2)时间,并不够优秀. 考虑优化. 多项式乘积 方案一:分 ...

  8. 浅谈FFT、NTT和MTT

    前言 \(\text{FFT}\)(快速傅里叶变换)是 \(O(n\log n)\) 解决多项式乘法的一个算法,\(\text{NTT}\)(快速数论变换)则是在模域下的,而 \(\text{MTT} ...

  9. hdu 1402(FFT乘法 || NTT乘法)

    A * B Problem Plus Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Other ...

  10. FFT与NTT

    讲解:http://www.cnblogs.com/poorpool/p/8760748.html 递归版FFT #include <iostream> #include <cstd ...

随机推荐

  1. iview table添加input框,且校验

    方法一 render渲染 { title: "用户名", key: "stockPrice", render: (h, params) => { retu ...

  2. Codeforces Round #769 (Div. 2) - D. New Year Concert

    GCD + st表 + 二分 Problem - 1632D - Codeforces 题意 给出一个长度为 \(n\;(1<=n<=2*10^5)\) 的数组 \(a[i]\;(1< ...

  3. this.$refs 获取的值是undefined

    以下是父组件内的代码截图 如果想取子组件内的方法,参数,等可以试以下两种方法 1.在mounted内使用this.$nextTick(()=>{   }) 2.直接再undated() {} 内 ...

  4. Vmware 虚拟机Ubuntu系统,解决忘记用户名和密码解决办法

    1.在开机界面按住shift,会加载grub的启动界面,找到Advaced options for Ubuntu选项.按"e" 进入编辑模式. 2.光标移动至ro,改为rw,(Li ...

  5. react module.scss文件中弹窗中 keyframes动画不生效,

    以下修改,亲测有效非弹窗内动画写法 .submit_btn{   animation: submit_btn 1.5s infinite;     -webkit-animation: submit_ ...

  6. 数据驱动DDT(Data-Driven Tests):测试数据的参数化

    准备第三方库: 首先安装ddt库,其次在脚本中引入ddt 打开官网 https://pypi.org/project/ddt/ from ddt import ddt,data,unpack @ddt ...

  7. MAC使用Graphviz包报错 failed to execute PosixPath('dot')

    在使用LightGBM进行可视化时,用到了Graphviz包,在安装Graphviz包时遇到了以下问题. 错误描述: ExecutableNotFound: failed to execute Pos ...

  8. 梯度下降算法VS正规方程算法

    梯度下降算法的大家族: ①批量梯度下降:有N个样本,求梯度的时候就用了N个样本的梯度数据 优点:准确 缺点:速度慢 ②随机梯度下降:和批量梯度下降算法原理相似,区别在于求梯度时没有用所有的N歌样本数据 ...

  9. Unity Editor 扩展入门1

    教程来源:https://www.youtube.com/watch?v=491TSNwXTIg&t=204s 一个点击物体修改材质颜色的简单editor扩展工具 using UnityEng ...

  10. 安装robotframwork 报错Requirement already satisfied

    pip install 的时候报错信息为,在这里插入图片描述对于这样的问题需要指定安装路径pip install --target=d:\python\python37\lib\site-packag ...