前言

\(\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)\)

证明如下:

\[\begin{align}{}
&(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_k=\sum_{j=0}^{n-1}a_j\omega^{kj}
\]

然后我们可以矩阵乘积 \(y=V_na\) 的形式表示向量 \(a\) 到向量 \(y\) 的变换。\(V_n\) 为由 \(\omega_n\) 各项指数构成的范德蒙德矩阵。

\[\begin{pmatrix}
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\)

证明如下

\[\begin{array}{}
[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\)。

那么显然有

\[A(x)=A^{[0]}(x^2)+xA^{[1]}(x^2)
\]

我们把 \(\omega_n^k\) 代入得到

\[A(\omega_n^k)=A^{[0]}(\omega_n^{2k})+\omega_n^kA^{[1]}(\omega_n^{2k})
\]

利用消去引理得到

\[A(\omega_n^k)=A^{[0]}(\omega_{n/2}^k)+\omega_n^kA^{[1]}(\omega_{n/2}^k)
\]

那么将 \(A^{[0]},A^{[1]}\) 的系数向量 \(a^{[0]},a^{[1]}\) 进行一次 \(\text{DFT}\) ,分别得到 \(y^{[0]},y^{[1]}\) 。

\[y^{[0]}_k=A^{[0]}(\omega_{n/2}^k)\\
y^{[1]}_k=A^{[1]}(\omega_{n/2}^k)
\]

只要令 \(k<n/2\) ,将 \(k\geq n/2\) 的部分用折半引理即可。

\[A(\omega_n^k)=A^{[0]}(\omega_{n/2}^k)+\omega_n^kA^{[1]}(\omega_{n/2}^k)\\
A(\omega_n^{k+n/2})=A^{[0]}(\omega_{n/2}^k)-\omega_n^kA^{[1]}(\omega_{n/2}^k)
\]

推导不难,注意将在单位圆上的旋转借用平面向量来理解。

用 \(y\) 代入,最终的表达式为

\[y_k=y^{[0]}_k+\omega_n^ky_k^{[1]}\\
y_{k+n/2}=y_k^{[0]}-\omega_n^ky_k^{[1]}
\]

这样就可以分治求解了。

更高效的FFT

事实上 \(\text{FFT}\) 可以迭代求解。先观察一下递归求解的过程,如图所示。

然后用人类智慧观察,发现 \(a_i\) 在底层是在的位置为 \(i\) 的二进制位翻转。

发现只需要枚举区间长度,扫整个序列,就可以进行对区间进行合并。观察递归求解的式子

\[y_k=y^{[0]}_k+\omega_n^ky_k^{[1]}\\
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的更多相关文章

  1. 浅谈FFT&NTT

    复数及单位根 复数的定义大概就是:\(i^2=-1\),其中\(i\)就是虚数单位. 那么,在复数意义下,对于方程: \[ x^n=1 \] 就必定有\(n\)个解,这\(n\)个解的分布一定是在复平 ...

  2. 浅谈FFT(快速傅里叶变换)

    前言 啊摸鱼真爽哈哈哈哈哈哈 这个假期努力多更几篇( 理解本算法需对一些< 常 用 >数学概念比较清楚,如复数.虚数.三角函数等(不会的自己查去(其实就是懒得写了(¬︿̫̿¬☆) 整理了一 ...

  3. 浅谈FFT(快速傅里叶变换)

    本文主要简单写写自己在算法竞赛中学习FFT的经历以及一些自己的理解和想法. FFT的介绍以及入门就不赘述了,网上有许多相关的资料,入门的话推荐这篇博客:FFT(最详细最通俗的入门手册),里面介绍得很详 ...

  4. 浅谈FFT(快速博立叶变换)&学习笔记

    0XFF---FFT是啥? FFT是一种DFT的高效算法,称为快速傅立叶变换(fast Fourier transform),它根据离散傅氏变换的奇.偶.虚.实等 特性,对离散傅立叶变换的算法进行改进 ...

  5. 浅谈范德蒙德(Vandermonde)方阵的逆矩阵的求法以及快速傅里叶变换(FFT)中IDFT的原理

    浅谈范德蒙德(Vandermonde)方阵的逆矩阵与拉格朗日(Lagrange)插值的关系以及快速傅里叶变换(FFT)中IDFT的原理 标签: 行列式 矩阵 线性代数 FFT 拉格朗日插值 只要稍微看 ...

  6. FFT/NTT/MTT学习笔记

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

  7. FFT&NTT总结

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

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

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

  9. 浅谈 Fragment 生命周期

    版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Fragment 文中如有纰漏,欢迎大家留言指出. Fragment 是在 Android 3.0 中 ...

随机推荐

  1. sitecore系列教程之目标功能有什么新意?

    由于SItecore 8中有很多令人兴奋的东西,我选择专注于体验平台的特定领域,这篇文章的主题是目标. 1.客户智能选项  目标项目(/ sitecore / system / Marketing C ...

  2. sitecore系统教程之体验编辑器

    体验编辑器是一个WYSIWYG编辑器,允许您直接在页面上轻松更改项目.您可以编辑页面上可见的所有项目 - 文本,图形,徽标,链接等. 在体验编辑器中,您可以编辑当前项目的字段以及页面上呈现的任何项目的 ...

  3. request.getServletPath(),request.getContextPath()

    2018-11-24  16:34:33 1. getServletPath():获取能够与“url-pattern”中匹配的路径,注意是完全匹配的部分,*的部分不包括. 2. getPageInfo ...

  4. 【CDH学习之三】CDH安装

    登录CM 1.版本选择 免费版本的CM5已经去除50个节点数量的限制. 各个Agent节点正常启动后,可以在当前管理的主机列表中看到对应的节点. 选择要安装的节点,点继续. 接下来,出现以下包名,说明 ...

  5. python selenium设置chrome的下载路径

    python可以通过ChromeOptions设置chrome参数,如下载路径等,代码如下(python 3.6.7): #-*-coding=utf-8-*- from selenium impor ...

  6. LDA模型了解及相关知识

    什么是LDA? LDA是基于贝叶斯模型的,涉及到贝叶斯模型离不开“先验分布”,“数据(似然)”和"后验分布"三块.贝叶斯相关知识:先验分布 + 数据(似然)= 后验分布. 贝叶斯模 ...

  7. Git HEAD detached from XXX (git HEAD 游离) 解决办法

    本文 Git 图片主要来自:图解 Git,非常感谢! 读完本文你将了解: 什么是 HEAD HEAD 游离状态的利与弊 具体解决操作 Thanks 什么是 HEAD Git 中的 HEAD 可以理解为 ...

  8. SQL知识点、SQL语句学习

    一. 数据库简介和创建1. 系统数据库在安装好SQL SERVER后,系统会自动安装5个用于维护系统正常运行的系统数据库: (1)master:记录了SQL SERVER实例的所有系统级消息,包括实例 ...

  9. mysql命令(三)

    创建一个名字为Student库: create database Student; 用以下地命令来查看创建的数据库是否成功: show databases; 进入数据库: use Student; 用 ...

  10. 前端框架VUE----补充

    修饰符 .lazy 在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 .你可以添加 lazy 修饰符,从而转变为使用 change 事件进行同步: <!-- ...