浅谈FFT、NTT和MTT
前言
\(\text{FFT}\)(快速傅里叶变换)是 \(O(n\log n)\) 解决多项式乘法的一个算法,\(\text{NTT}\)(快速数论变换)则是在模域下的,而 \(\text{MTT}\)(毛神仙对\(\text{FFT}\)的精度优化算法)可以针对任意模数。本文主要讲解这三种算法,具体的应用还请参考我博客内的题解。
正文
FFT-快速傅里叶变换
学习这个算法可以借助《算法导论》,当然算导上的东西需要耐心才能啃下来。这里只是概括一下算导上的介绍,并加入一些个人的见解。下面逐步介绍这个算法。
复数
如果学过的话可以跳过。实数可以一一对应数轴上的点,那么复数就可以一一对应平面直角坐标系上的点。对应 \(x\) 轴上的点的就是我们熟悉的实数,而外面的就是虚数。其中 \((0,1)\) 这个点对应的数记作 \(i\) ,即 \(\sqrt{-1}\),它表示虚数单位。复数可以表示成 \(a+ib\) 的形式,其中 \(a,b\) 为实数。
极坐标表示法下的乘法
\((a,\alpha)\cdot(b,\beta)=(ab,\alpha+\beta)\)
证明如下:
&(a,\alpha)\cdot(b,\beta) \notag\\
=&ab(\cos\alpha+i\sin\alpha)(\cos\beta+i\sin\beta)\notag\\
=&ab[\cos\alpha\cos\beta+i(\sin\alpha\cos\beta+\cos\alpha\sin\beta)-\sin\alpha\sin\beta]\notag\\
=&ab[\cos(\alpha+\beta))+i\sin(\alpha+\beta)]\notag\\
=&(ab,\alpha+\beta)\notag
\end{align}
\]
代数表示法下的乘法
\((a+ib)\cdot(c+id)=ac-bd+i(ad+bc)\)
无需证明,肉眼化简。
单位复数根
在单位圆上,我们用 \(\omega_{n}^k\) 表示将单位圆 \(n\) 等分,取其第 \(k\) 条线对应的单位复数。其中 \(\omega_n^0=1\) ,逆时针方向编号,如图所示:
单位复根有一些重要的性质。
消去引理
\(\omega_{dn}^{dk}=\omega_{n}^k\) 其中 \(n,k\geq 0,d>0\)
折半引理
\((\omega_n^{k+n/2})^2=(\omega_n^k)^2\) 其中\(n\geq0,k\geq 0\)
如果借助向量去理解的话,理解起来非常方便。
多项式
一个形如 \(\displaystyle A(x)=\sum_{i=0}^{n-1}a_ix^i\) 的式子。
系数表示
直接列出 \(A(x)\) 的各项系数。这种表示方法可以 \(O(n)\) 的实现多项式加法,但多项式乘法却需要 \(O(n^2)\)
点值表示
通过带入若干个特值确定,显然,一个最高次为 \(n-1\) 的多项式需要 \(n\) 的特殊值便唯一确定。这种表示方法可以 \(O(n)\) 的加和乘,但是要转化成系数表示才能体现出它作为多项式的价值。
DFT
对于一个列向量 \(a=(a_0,a_1,\cdots,a_{n-1})\) ,以它为系数的多项式 \(A(x)=\displaystyle\sum_{j=0}^{n-1}a_jx^j\)
若有一个列向量 \(y=(y_0,y_1,\cdots,y_{n-1})\) 满足 \(y_k=A(\omega_n^k)\) ,则\(y=\text{DFT}_n(a)\)
\(\text{DFT}\) 的全称为离散傅里叶变换,是将多项式的系数表达化作点值表达的一个变换。
同理 \(a=\text{DFT}_n^{-1}(y)\) ,\(\text{DFT}^{-1}\) 就是逆离散傅里叶变换,也称 \(\text{IDFT}\),我们尝试写出 \(\text{DFT}^{-1}\) 的表达式。
写出 \(y\) 与 \(a\) 的关系
\]
然后我们可以矩阵乘积 \(y=V_na\) 的形式表示向量 \(a\) 到向量 \(y\) 的变换。\(V_n\) 为由 \(\omega_n\) 各项指数构成的范德蒙德矩阵。
y_0\\
y_1\\
y_2\\
y_3\\
\vdots\\
y_{n-1}\\
\end{pmatrix}
=
\begin{pmatrix}
\omega^0 & \omega^0 &\omega^0 & \omega^0 & \cdots & \omega^0 \\
\omega^0 & \omega^1 &\omega^2 & \omega^3 & \cdots & \omega^{(n-1)}\\
\omega^0 & \omega^2 &\omega^4 & \omega^6 & \cdots & \omega^{2(n-1)} \\
\omega^0 & \omega^3 &\omega^6 & \omega^9 & \cdots & \omega^{3(n-1)} \\
\vdots & \vdots & \vdots & \vdots & \ddots &\vdots\\
\omega^{0} & \omega^{1(n-1)} &\omega^{2(n-1)} & \omega^{3(n-1)} & \cdots & \omega^{(n-1)(n-1)} \\
\end{pmatrix}
\begin{pmatrix}
a_0\\
a_1\\
a_2\\
a_3\\
\vdots\\
a_{n-1}\\
\end{pmatrix}
\]
那我们现在要求的就是 \(V_n^{-1}\) 的矩阵,即 \(V_n\) 的逆矩阵。
有如下定理:
对于 \(j,k\in[0,n)\) ,\(V_n^{-1}\) \((j,k)\) 处的元素为 \(\omega_n^{-jk}/n\)
证明如下
[V_nV_n^{-1}]_{jj'}&=\displaystyle\sum_{k=0}\omega_n^{jk}\omega^{-kj'}/n\\
&=\displaystyle{1\over n}\sum_{k=0}\omega_n^{k(j-j')}
\end{array}
\]
显然,当 \(j=j'\) 时,\([V_nV_n^{-1}]_{jj'}\) 的值为 \(1\) ,否则为 \(0\) ,那么 \([V_nV_n^{-1}]\) 是一个行列数为 \(n\) 的单位矩阵,即得证 \(V^{-1}\) 为 \(V\) 的逆矩阵。
那么在作 \(\text{IDFT}\) 的时候,只需将单位根换成 \({\omega_n^{-1}}\) ,最后系数再除以 \(n\) 即可。
当然,直接变换是 \(O(n^2)\) 的。我们考虑用分治的思想进行变换。
FFT
首先观察多项式 \(A(x)\) ,我们将指数分奇偶两类。偶数项以 \(\{a_0,a_2,...,a_{n-2}\}\) 构造一个新的多项式 \(\displaystyle A^{[0]}(x)=\sum_{j=0}^{n/2-1}a_{2j}x^j\),奇数项同理为 \(\displaystyle A^{[1]}(x)=\sum_{j=0}^{n/2-1}a_{2j+1}x^j\)。
那么显然有
\]
我们把 \(\omega_n^k\) 代入得到
\]
利用消去引理得到
\]
那么将 \(A^{[0]},A^{[1]}\) 的系数向量 \(a^{[0]},a^{[1]}\) 进行一次 \(\text{DFT}\) ,分别得到 \(y^{[0]},y^{[1]}\) 。
有
y^{[1]}_k=A^{[1]}(\omega_{n/2}^k)
\]
只要令 \(k<n/2\) ,将 \(k\geq n/2\) 的部分用折半引理即可。
A(\omega_n^{k+n/2})=A^{[0]}(\omega_{n/2}^k)-\omega_n^kA^{[1]}(\omega_{n/2}^k)
\]
推导不难,注意将在单位圆上的旋转借用平面向量来理解。
用 \(y\) 代入,最终的表达式为
y_{k+n/2}=y_k^{[0]}-\omega_n^ky_k^{[1]}
\]
这样就可以分治求解了。
更高效的FFT
事实上 \(\text{FFT}\) 可以迭代求解。先观察一下递归求解的过程,如图所示。
然后用人类智慧观察,发现 \(a_i\) 在底层是在的位置为 \(i\) 的二进制位翻转。
发现只需要枚举区间长度,扫整个序列,就可以进行对区间进行合并。观察递归求解的式子
y_{k+n/2}=y_k^{[0]}-\omega_n^ky_k^{[1]}
\]
它的流程可以用上图来表示,上面操作叫作蝴蝶操作,其实和递归求解的流程相似。具体还是看代码,码风还是清晰的。
struct Complex
{
double x,y;
Complex operator +(const Complex &_){return (Complex){x+_.x,y+_.y};}
Complex operator -(const Complex &_){return (Complex){x-_.x,y-_.y};}
Complex operator *(const Complex &_){return (Complex){x*_.x-y*_.y,x*_.y+y*_.x};}
Complex operator /(const int &_){return (Complex){x/_,y/_};}
};
namespace _Polynomial
{
Complex A[N<<1],B[N<<1];
Complex w[N<<1];int r[N<<1];
void DFT(Complex *a,int op,int n)
{
FOR(i,0,n-1)if(i<r[i])swap(a[i],a[r[i]]); //位翻转
for(int i=2;i<=n;i<<=1) //合并出一个长i的区间
for(int j=0;j<n;j+=i) //区间开头的位置
for(int k=0;k<i/2;k++) //蝴蝶操作
{
Complex u=a[j+k],t=w[op==1?n/i*k:n-n/i*k]*a[j+k+i/2];
a[j+k]=u+t,a[j+k+i/2]=u-t;
}
if(op==-1)FOR(i,0,n-1)a[i]=a[i]/n;
}
void multiply(const int *a,const int *b,int *c,int n1,int n2)
{
int n=1;
while(n<n1+n2-1)n<<=1;
FOR(i,0,n1-1)A[i].x=a[i],A[i].y=0;
FOR(i,0,n2-1)B[i].x=b[i],B[i].y=0;
FOR(i,n1,n-1)A[i].x=A[i].y=0;
FOR(i,n2,n-1)B[i].x=B[i].y=0;
FOR(i,0,n-1)r[i]=(r[i>>1]>>1)|((i&1)*(n>>1));
FOR(i,0,n)w[i]=(Complex){cos(2*PI*i/n),sin(2*PI*i/n)};
DFT(A,1,n),DFT(B,1,n);
FOR(i,0,n-1)A[i]=A[i]*B[i];
DFT(A,-1,n);
FOR(i,0,n1+n2-2)c[i]=A[i].x+0.5;
}
};
显而易见,由于 \(\text{double}\) 的存在,精度多多少少会被卡一点。而具体的题目经常往往会给一个特殊的模数,这种时候就要用到接下来介绍的算法了。
NTT-快速数论变换
待补充。。。
浅谈FFT、NTT和MTT的更多相关文章
- 浅谈FFT&NTT
复数及单位根 复数的定义大概就是:\(i^2=-1\),其中\(i\)就是虚数单位. 那么,在复数意义下,对于方程: \[ x^n=1 \] 就必定有\(n\)个解,这\(n\)个解的分布一定是在复平 ...
- 浅谈FFT(快速傅里叶变换)
前言 啊摸鱼真爽哈哈哈哈哈哈 这个假期努力多更几篇( 理解本算法需对一些< 常 用 >数学概念比较清楚,如复数.虚数.三角函数等(不会的自己查去(其实就是懒得写了(¬︿̫̿¬☆) 整理了一 ...
- 浅谈FFT(快速傅里叶变换)
本文主要简单写写自己在算法竞赛中学习FFT的经历以及一些自己的理解和想法. FFT的介绍以及入门就不赘述了,网上有许多相关的资料,入门的话推荐这篇博客:FFT(最详细最通俗的入门手册),里面介绍得很详 ...
- 浅谈FFT(快速博立叶变换)&学习笔记
0XFF---FFT是啥? FFT是一种DFT的高效算法,称为快速傅立叶变换(fast Fourier transform),它根据离散傅氏变换的奇.偶.虚.实等 特性,对离散傅立叶变换的算法进行改进 ...
- 浅谈范德蒙德(Vandermonde)方阵的逆矩阵的求法以及快速傅里叶变换(FFT)中IDFT的原理
浅谈范德蒙德(Vandermonde)方阵的逆矩阵与拉格朗日(Lagrange)插值的关系以及快速傅里叶变换(FFT)中IDFT的原理 标签: 行列式 矩阵 线性代数 FFT 拉格朗日插值 只要稍微看 ...
- FFT/NTT/MTT学习笔记
FFT/NTT/MTT Tags:数学 作业部落 评论地址 前言 这是网上的优秀博客 并不建议初学者看我的博客,因为我也不是很了解FFT的具体原理 一.概述 两个多项式相乘,不用\(N^2\),通过\ ...
- FFT&NTT总结
FFT&NTT总结 一些概念 \(DFT:\)离散傅里叶变换\(\rightarrow O(n^2)\)计算多项式卷积 \(FFT:\)快速傅里叶变换\(\rightarrow O(nlogn ...
- FFT/NTT复习笔记&多项式&生成函数学习笔记Ⅰ
众所周知,tzc 在 2019 年(12 月 31 日)就第一次开始接触多项式相关算法,可到 2021 年(1 月 1 日)才开始写这篇 blog. 感觉自己开了个大坑( 多项式 多项式乘法 好吧这个 ...
- 浅谈 Fragment 生命周期
版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Fragment 文中如有纰漏,欢迎大家留言指出. Fragment 是在 Android 3.0 中 ...
随机推荐
- Django admin模块无法调用css样式文件
在使用Django Admin开发时,发现admin模块css样式文件丢失,无法调用,使火狐浏览器提示: 此 URL 的资源不是文本: http://127.0.0.1:8000/statics/ad ...
- Axis2之异步调用
本章主要介绍axis2接口的异步调用方式. 一般情况下,我们使用同步方法(invokeBlocking)调用axis2接口,如果被调用的WebService方法长时间不返回,客户端将一直被阻塞,直到该 ...
- Linux服务器---流量监控ntop
Ntop Ntop 是一款类似于sniffer的流量监控工具,它显示出的流量信息比mrtg更加详细. 1 .安装一些依赖软件 [root@localhost bandwidthd]# yum ins ...
- The Little Prince-12/07
The Little Prince-12/07 "My little man, where do you come from? What is this ‘where I live,‘ of ...
- Struts2 的 配置
三.Struts2配置 Struts2的核心配置文件 1.名称和位置是固定的 在src下struts.xml 2.Struts根标签 Package Action Result Action Pa ...
- mysql03
查询的列不在同一表中必须使用连接内连接,外链接 -- 输出学生姓名以及对应的年级名称 内连接 select studentName,gradeName from student inner join ...
- 双屏互动h5
情侣H5:https://www.25xt.com/allcode/10837.html 双屏互动:https://www.digitaling.com/articles/18180.html
- Django中Session
Django中默认支持Session,其内部提供了5种类型的Session供开发者使用: ·数据库(默认) ·缓存 ·文件 ·缓存+数据库 ·加密cookie (1)数据库中的Session Djan ...
- Spring学习笔记2:Spring HelloWorld
1:IntelliJ新建Maven工程 2:pom文件加入Spring依赖 <project xmlns="http://maven.apache.org/POM/4.0.0" ...
- daemon进程fork一次和fork两次的区别?
守护进程也称为精灵进程(Daemon),是运行在后台的一种特殊的进程.它独立于控制终端并且周期性的执行某种任务负等待处理某些发生的事件.因为他们没有控制终端,所以说他们是在后台运行的. 守护进程的特点 ...