前言

如果我们能用一种时间上比 \(O(n^2)\) 更优秀的方法来计算大整数(函数)的乘法,那就好了。快速傅里叶变换(FFT) 可以帮我们在 \(O(n\log n)\) 的时间内解决问题。

函数乘积

计算两个大整数之积时,我们发现

\[(2x+3)(4x+5)=8x^2+22x+15\quad...(*)\\
23\times45=1035\]

而如果我们把 \((*)\) 式右边的每一位的系数看做一个数每位上的数码,正好得到了 \(1035\)。事实上,对于所有的多项式乘法,以上规律同样成立。

证明: (提示)考虑竖式乘法的过程,和多项式乘法的过程,它们的本质都是一样的。

这样,我们就把问题转换为:计算两个已知函数之积的函数的解析式

复平面、单位圆

考虑 \(\sqrt{-9}\) 的值。

\[\begin{aligned}\sqrt{-9}&=\sqrt{-1}\times\sqrt9=3\sqrt{-1}.\end{aligned}
\]

类似地,\(\forall N\in \Z_-\) 我们都可以用类似的方法得到 $$\sqrt{N}=\sqrt{-N}\times\sqrt{-1}$$

引入虚数单位 \(\text{i}\),使 \(\text{i}^2=-1.\) 这样我们就重新认识了数的范围,从实数扩充到复数。

一复数 \(a+b\text{i}\) 中的 \(a,b\in\R\),\(a\) 是它的实数部分,\(b\text{i}\) 是虚数部分。若 \(b=0\),则它是实数。复数服从实数的大部分运算法则。

若两个复数,它们的实数部分相等,虚数部分之和为 \(0\),我们称它们互为 共轭复数

我们知道,数轴上的每个点与每个实数一一对应。类似地,我们可以使用 复平面 上的点表示复数。复平面与平面直角坐标系类似,它的 \(x\) 轴单位长度为 \(1\),\(y\) 轴单位长度为 \(\text{i}\)。复平面上的点之横纵坐标表示的数之和即为该点表示的数。比如,\((1,2)\) 表示 \(1+2\text{i}\)。

以圆点为圆心,\(1\) 为半径,在复平面上作圆,如图所示。这个圆叫 单位圆



在圆上任取一点 \(A\),过此点作 \(x\) 轴的垂线段,垂足为 \(B\)。设 \(∠AOB=\theta\)。易知$$OB=OA\times\cos\theta=\cos\theta\

AB=OB\times\sin\theta=\sin\theta$$\(\therefore A(\cos\theta,\sin\theta).\) 这个 \(\theta\) 叫做 \(A\) 的 辐角

单位根及其性质

  1. \(\forall k,n\in\N^*\) 有 \(\omega_n^k\times\omega_n^1=\omega_n^{k+1}\);

    证明:



    如图所示,过点 \(A(1,0)\) 作圆内接 \(n\) 边形。设 \(∠AOA'=\theta.\) 则

\[A'(\cos\theta,\sin\theta)\\A''(\cos2\theta,\sin2\theta)\\\ \\\begin{aligned}&\quad(\cos\theta+\sin\theta\times\text{i})\times(\cos2\theta+\sin2\theta\times\text{i})\\
&=\cos\theta\cos2\theta+(\cos\theta\sin2\theta+\sin\theta\cos2\theta)\times\text{i}-\sin\theta\sin2\theta\\
&=(\sin\theta\sin2\theta+\cos\theta\cos2\theta)+(\sin\theta\cos2\theta+\cos\theta\cos2\theta)\times\text{i}\end{aligned}\]

由三角函数恒等变换式

\[\sin(\alpha+\beta)=\sin\alpha\sin\beta-\cos\alpha\cos\beta,\\
\cos(\alpha+\beta)=\sin\alpha\cos\beta+\sin\beta\cos\alpha\]

\[\begin{aligned}原式&=\sin(\theta+2\theta)+\cos(\theta+2\theta)\times\text{i}\\
&=\sin3\theta+\cos3\theta\times\text{i}\end{aligned}\]

换句话说,\(A'\) 和 \(A''\) 表示的数相乘后得到了对应的 \(A'''\)。

设这个多边形的顶点为 \(\{\omega_i^0\},\ i\in[0,n-1]\)。那么我们一定有$$\omega_nk\times\omega_n1=\omega_n^{k+1},\ k\in[0,n-1]$$

特别地,我们有 \(\omega_n^n=\omega_n^0\)。它们叫做 \(n\) 次 单位根

  1. \(\omega_n^0=\omega_n^n=1\);
  2. \(\omega_n^0,\omega_n^1,...,\omega_n^{n-1}\) 互不相同;
  3. \(\forall k,n\in\N^*\) 有 \(\omega_n^k=\omega_{2n}^{2k}\);

证明一: 感性理解。一个 \(n\) 边形把单位圆分成了 \(n\) 个部分,取这 \(n\) 个圆弧的中点,顺次连接这 \(2n\) 个点,得到一个新多边形。则 \(\omega_n^k\) 即表示 \(n\) 变形的第 \(k\) 个单位根,也表示 \(2n\) 变形的第 \(2k\) 个点。

证明二:

\(\begin{aligned}\omega_{2n}^{2k}&=\cos2k\times\frac{2\pi}{2n}+\text{i}\times\sin2k\times\frac{2\pi}{2n}\\
&=\cos k\times\frac{2\pi}n+\text{i}\times\sin k\times\frac{2\pi}n\\
&=\omega_n^k.\end{aligned}\)

  1. \(\forall k,n\in\N^*\) 有 \(\omega_n^{k+\frac2n}=-\omega_n^k\);

证明一: 感性理解。乘以 \(\omega_n^2\) 的意思其实就是在单位圆逆时针转半圈。单位圆上的一个点,逆时针转了半圈后到达的点,与原来的点关于原点对称。

证明二: $$\begin{aligned}\omega_n{k+\frac2n}&=\omega_nk\times\omega_n^{\frac2n}\

&=\omega_n^k\times(\cos\pi+\text{i}\times\sin\pi)\

&=\omega_n^k\times(-1+0)\

&=-\omega_n^k.\end{aligned}$$

  1. \(\forall k,n\in\N^*\) 有 $$\sum_{i=0}{n-1}{(\omega_nk)^i}=\begin{cases}0,k\neq0\n,k=0\end{cases}$$

    证明: \((\text{I})\) 若 \(k\neq0\),设 $$S=\sum_{i=0}{n-1}{(\omega_nk)^i}\quad...(1)$$ 则 $$\omega_n^k\times S=\sum_{i=1}{n}{(\omega_nk)^i}\quad...(2)$$

    \((2)-(1)\) 得$$\begin{aligned}(\omega_n^k-1)\times S&=(\omega_nk)n-1\

    S&=\frac{(\omega_nn)k-1}{\omega_n^k-1}\

    &=\frac{1-1}{\omega_n^k-1}\

    &=0.\end{aligned}$$

\((\text{II})\) 当 \(k=0\) 时,

\[\begin{aligned}原式&=\sum_{i=0}^{n-1}{1}\\&=n.\end{aligned}
\]

## 快速傅里叶变换
显然地,**一个 $n$ 次多项式可以被 $n+1$ 个点唯一确定**。

那么,我们可以在单位圆上取 \(n+1\) 个单位根,代入已知的两个函数,得到 \(n+1\) 对点,再把每对点相乘,得到结果函数上的 \(n+1\) 个点,再求出结果函数。

设已知函数(合并后)为 $$\begin{aligned}f(x)&=\sum_{i=0}{n-1}{a_ixi}\

&=(a_0+a_2x+...+a_{n-2}x^{n-2})\

&+(a_1x+a_3x3+...+a_{n-1}x{n-1})\end{aligned}$$

设 $$\begin{aligned}g(x)&=a_0+a_2x+...+a_{n-2}x^{\frac{n}2-1}\

h(x)&=a_1+a_3x+...+a_{n-1}x^{\frac{n}2-1}\end{aligned}$$

则 $$f(x^2)=g(x)+x\times h(x^2)$$

令 \(x=\omega_n^k\),由 单位根的性质1 得 $$\begin{aligned}f(x2)&=g(\omega_n{2k})+\omega_n^k\times h(\omega_n^{2k})\

&=g(\omega_{\frac{n}2}k)+\omega_nk\times h(\omega_{\frac{n}2}^k)\quad...(1)\end{aligned}$$

令 \(x=-\omega_n^k=\omega_n^{k+\frac{n}2}\) 得 $$\begin{aligned}f(x2)&=g(\omega_n{2k+n})+\omega_n^{k+\frac{n}2}\times h(\omega_n^{2k+n})\

&=g(\omega_n{2k})+\omega_n{k+\frac{n}2}\times h(\omega_n^{2k})\

&=g(\omega_{\frac{n}2}k)-\omega_nk\times h(\omega_{\frac{n}2}^k)\quad...(2)\end{aligned}$$

不难发现,\((1)\) 式与 \((2)\) 式 互轭!换句话说,当你求出 \((1)\) 式的值时,只需将虚数部分取相反数(时间复杂度为 \(O(1)\))即得到了 \((2)\) 式。

这样,我们就将问题的规模减为一半。同理,剩下的一半也可以使用类似方法,分为更小的两半……没错,这就是 分治,这样就将时间复杂度降为了 \(O(n\log n)\)。

快速傅里叶逆变换

设 \(\{y_i\}\) 是 \(\{a_i\}\) 的傅里叶变换,即在 \(\{\omega_n^i\}\) 处的值;

设 \(\{c_i\}\) 是 \(\{y_i\}\) 在 \(\{\omega_n^{-i}\}\) 的值。

则有 $$\begin{aligned}c_k&=\sum_{i=0}{n-1}{y_i(\omega_n{-k})^i}\

&=\sum_{i=0}{n-1}{(\sum_{j=0}{n-1}{a_j(\omega_nk)j})·(\omega_n{-k})i}\

&=\sum_{i=0}{n-1}{a_j\times\sum_{j=0}{n-1}{(\omega_n{j-k})i}}\end{aligned}$$

根据 单位根的性质6 ,有且仅有一个 \(j\in[0,n-1]\) 使得 \(j=k\)。

\(\therefore \forall j\) 有且仅有一个$${(\omega_n{j-k})i}=n$$有且仅有 \(n-1\) 个 $${(\omega_n{j-k})i}=0$$

\[\begin{aligned}\therefore\sum_{j=0}^{n-1}{(\omega_n^{j-k})^i}&=n,\\c_k&=\sum_{i=0}^{n-1}{a_j\times\sum_{j=0}^{n-1}{(\omega_n^{j-k})^i}}\\
&=a_k\times n,\\
a_k&=\frac{c_k}n.\end{aligned}\]

换句话说,经过快速傅里叶逆变换后的数组 \(\{c_i\}\),除以 \(n\) 后就得到了结果 \(\{a_i\}\)。(这里的 \(n\) 是指 \(c\) 数组的长度)

小结

使用快速傅里叶变换(FFT)计算两个函数之积的步骤如下:

  1. 分别对两个函数进行快速傅里叶变换;
  2. 将两组结果相乘;
  3. 对结果进行快速傅里叶逆变换,并将结果除以 \(n\)。

迭代快速傅里叶变换

经过上文的学习,容易写出以下代码(感谢 linjiayang2016 大佬的代码)

void fast_fast_tle(int limit,complex *a,int type){
if(limit==1)
return ;
complex a1[limit>>1],a2[limit>>1]; //*
for(int i=0;i<=limit;i+=2)
a1[i>>1]=a[i],a2[i>>1]=a[i+1];
fast_fast_tle(limit>>1,a1,type);
fast_fast_tle(limit>>1,a2,type);
complex Wn=complex(std::cos(2.0*Pi/limit),type*std::sin(2.0*Pi/limit)),w=complex(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];
}

发现 \(*\) 行定义了两个不小的数组,而 FFT 函数是被递归调用的,所以会造成爆栈。

原序列 1 2 3 4 5 6 7 8
快速傅里叶变换后序列 1 5 3 7 2 6 4 8
原下标的二进制 000 100 010 110 001 101 011 111
新下标的二进制 000 001 010 011 100 101 110 111

不难发现,快速傅里叶变换后的序列中,每个数的新下标的二进制,在数值上等于原下标二进制的反转。

设原下标为 \(x\) 的数的新下标为 \(r[x]\)。

以十进制数 \(184\) 的二进制表示为例。设这个二进制数长度为 \(l\)。

\[\begin{array}{ccc}1&\ &0&\ &1&\ &1&\ &1&\ &0&\ &0&|&0\end{array}
\]

我们把这个数分为两部分,第一部分是前 \(l-1\) 位,第二部分是最后一位。不难得到,它的翻转就是 在第二部分后面接上第一部分的翻转形成的数

	r[i]=((r[i>>1]>>1)|((i&1)<<(l-1)));

至此,快速傅里叶变换的相关内容已全部结束。

\(\text{luogu P3803}\)

给你两个正整数 \(n,m\leq10^6\),和 \(n+1\) 次多项式 \(f(x)\)、\(m+1\) 次多项式 \(g(x)\)。求 \(f(x)\) 和 \(g(x)\) 的卷积。

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm> using namespace std;
#define reg register
const int MAXN=400010;
const double pi=acos(-1.0); struct compex{
double x,y;
friend compex operator +(const compex a,const compex b){
compex c;
c.x=a.x+b.x;c.y=a.y+b.y;
return c;
}
friend compex operator -(const compex a,const compex b){
compex c;
c.x=a.x-b.x;c.y=a.y-b.y;
return c;
}
friend compex operator *(const compex a,const compex b){
compex c;
c.x=a.x*b.x-a.y*b.y;
c.y=a.x*b.y+a.y*b.x;
return c;
}
}a[MAXN],b[MAXN];
int r[MAXN];
int n,m,l=0;
int limit=1; void FFT(compex*t,int type){
for(reg int i=0;i<limit;++i)
if(i<r[i]) swap(t[i],t[r[i]]);
for(reg int mid=1;mid<limit;mid<<=1){
compex wn=(compex){cos(pi/mid),type*sin(pi/mid)};
for(reg int j=0,R=(mid<<1);j<limit;j+=R){
compex w={1.0,0};
for(reg int k=0;k<mid;++k,w=w*wn){
compex x=t[j+k],y=w*t[j+k+mid];
t[j+k]=x+y;
t[j+k+mid]=x-y;
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(reg int i=0;i<=n;++i)
scanf("%lf",&a[i].x);
for(reg int i=0;i<=m;++i)
scanf("%lf",&b[i].x);
while(limit<=n+m) limit<<=1,++l;
for(reg int i=1;i<limit;++i)
r[i]=((r[i>>1]>>1)|((i&1)<<(l-1)));
FFT(a,1);FFT(b,1);
for(reg int i=0;i<limit;++i)
a[i]=a[i]*b[i];
FFT(a,-1);
for(reg int i=0;i<=n+m;++i)
printf("%d ",(int)(a[i].x/limit+0.5));
}

快速傅里叶变换(FFT)略解的更多相关文章

  1. 快速傅里叶变换(FFT)详解

    本文只讨论FFT在信息学奥赛中的应用 文中内容均为个人理解,如有错误请指出,不胜感激 前言 先解释几个比较容易混淆的缩写吧 DFT:离散傅里叶变换—>$O(n^2)$计算多项式乘法 FFT:快速 ...

  2. 快速傅里叶变换(FFT)_转载

    FFTFFT·Fast  Fourier  TransformationFast  Fourier  Transformation快速傅立叶变换 P3803 [模板]多项式乘法(FFT) 参考上文 首 ...

  3. 快速傅里叶变换FFT / NTT

    目录 FFT 系数表示法 点值表示法 复数 DFT(离散傅里叶变换) 单位根的性质 FFT(快速傅里叶变换) IFFT(快速傅里叶逆变换) NTT 阶 原根 扩展知识 FFT 参考blog: 十分简明 ...

  4. 快速傅里叶变换(FFT)学习笔记

    定义 多项式 系数表示法 设\(A(x)\)表示一个\(n-1\)次多项式,则所有项的系数组成的\(n\)维向量\((a_0,a_1,a_2,\dots,a_{n-1})\)唯一确定了这个多项式. 即 ...

  5. Algorithm: 多项式乘法 Polynomial Multiplication: 快速傅里叶变换 FFT / 快速数论变换 NTT

    Intro: 本篇博客将会从朴素乘法讲起,经过分治乘法,到达FFT和NTT 旨在能够让读者(也让自己)充分理解其思想 模板题入口:洛谷 P3803 [模板]多项式乘法(FFT) 朴素乘法 约定:两个多 ...

  6. 快速傅里叶变换FFT& 数论变换NTT

    相关知识 时间域上的函数f(t)经过傅里叶变换(Fourier Transform)变成频率域上的F(w),也就是用一些不同频率正弦曲线的加 权叠加得到时间域上的信号. \[ F(\omega)=\m ...

  7. 多项式 之 快速傅里叶变换(FFT)/数论变换(NTT)/常用套路【入门】

    原文链接https://www.cnblogs.com/zhouzhendong/p/Fast-Fourier-Transform.html 多项式 之 快速傅里叶变换(FFT)/数论变换(NTT)/ ...

  8. 【学习笔记】快速傅里叶变换(FFT)

    [学习笔记]快速傅里叶变换 学习之前先看懂这个 浅谈范德蒙德(Vandermonde)方阵的逆矩阵的求法以及快速傅里叶变换(FFT)中IDFT的原理--gzy hhh开个玩笑. 讲一下\(FFT\) ...

  9. 再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Bluestein算法+分治FFT+FFT的优化+任意模数NTT)

    再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Bluestein算法+分治FFT+FFT的优化+任意模数NTT) 目录 再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Blueste ...

  10. 快速傅里叶变换FFT

    多项式乘法 #include <cstdio> #include <cmath> #include <algorithm> #include <cstdlib ...

随机推荐

  1. DevExpress的TextEdit限制输入内容的格式,比如只能输入数字

    场景 Winform控件-DevExpress18下载安装注册以及在VS中使用: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/1 ...

  2. Linux遇到的问题-记录

    Linux遇到的问题 2019-04-09以前: Linux&Win双系统下时间显示不正常的问题 一般安装了双系统(Linux+Windows)就很容易出现问题,Windows是直接取硬件时间 ...

  3. 词义消除歧义NLP项目实验

    词义消除歧义NLP项目实验 本项目主要使用https://github.com/alvations/pywsd 中的pywsd库来实现词义消除歧义 目前,该库一部分已经移植到了nltk中,为了获得更好 ...

  4. 小白专场-FileTransfer-c语言实现

    目录 一.集合的简化表示 二.题意理解 三.程序框架搭建 3.1 Input_connection 3.2 Check_connection 3.3 Check_network 四.pta测试 五.按 ...

  5. FPGA 开发详细流程你了解吗?

    FPGA 的详细开发流程就是利用 EDA 开发工具对 FPGA 芯片进行开发的过程. FPGA 的详细开发流程如下所示,主要包括电路设计.设计输入.综合(优化).布局布线(实现与优化).编程配置五大步 ...

  6. Linux 文件/目录操作详解

    目录 Linux 文件/目录操作详解 初识Linux 一.文件/目录显示命令 ls 二.目录创建命令 mkdir 三.目录转移命令 cd 四.当前目录显示命令 pwd 五.文件处理命令 rmdir 六 ...

  7. 如何更规范化编写Java 代码

    如何更规范化编写Java 代码 Many of the happiest people are those who own the least. But are we really so happy ...

  8. 阿里云ESC服务器centos6.9使用及注意事项

    阿里云ESC服务器,配置低,但是足够新手尝试操作练习. 使用之前,注意事项: 选择操作系统 设置实例快照 安装终端工具 一,选择操作系统. 可以在购买服务器的时候进行选择系统盘,也可以在购买之后在实例 ...

  9. 算法题解:最大或最小的K个数(海量数据Top K问题)

    题目 输入 n 个整数,找出其中最小的 k 个数.例如输入4.5.1.6.2.7.3.8 这8个数字,则最小的4个数字是1.2.3.4. 初窥 这道题最简单的思路莫过于把输入的 n 个整数排序,排序之 ...

  10. Golang:线程 和 协程 的区别

    作者:林冠宏 / 指尖下的幽灵 博客:http://www.cnblogs.com/linguanh/ GitHub : https://github.com/af913337456/ 掘金:http ...