终于学会了FFT,水一篇随笔记录一下

前置知识网上一大堆,这里就不多赘述了,直接切入正题

01 介绍FFT

这里仅指出FFT在竞赛中的一般应用,即优化多项式乘法

一般情况下,计算两个规模为$n$的多项式相乘的结果,复杂度为$O(n^2)$,但是神奇的FFT可以将其优化至$O(nlogn)$

FFT的过程一般为:

多项式的系数表示$\longrightarrow$多项式的点值表示$\longrightarrow$多项式的系数表示

网上对每一步的叫法都有一定出入,这里称第一步变换为快速傅里叶变换,第二步为快速傅里叶逆变换

02快速傅里叶变换

先指出,接下来的每个$n$都是$2$的整数次幂

首先我们有一个已知系数表达的$n$项的多项式

$A(x)=a_0+a_1x+a_2x^2+\dots+a_{n-1}x^{n-1}$

要确定其的点值表达$(y_0,y_1,y_2,\dots,y_{n-1})$,朴素的做法就是取$n$个不同值代进去,这么做显然是$O(n^2)$

下面介绍快速傅里叶变换的做法

首先将多项式按照奇偶分类

$A(x)=(a_0+a_2x^2+\dots+a_{n-2}x^{n-2})+(a_1x+a_3x^3+\dots+a_{n-1}x^{n-1})$

$A(x)=(a_0+a_2x^2+\dots+a_{n-2}x^{n-2})+x\cdot(a_1+a_3x^2+\dots+a_{n-1}x^{n-2})$

$A_1(x)=a_0+a_2x+\dots+a_{n-2}x^{\tfrac{n-2}{2}}$

$A_2(x)=a_1+a_3x+\dots+a_{n-1}x^{\tfrac{n-2}{2}}$

不难发现

$A(x)=A_1(x^2)+xA_2(x^2)$

令$k<\frac{n}{2}$

将$\omega_{n}^k$代入得

$A(\omega_{n}^k)=A_1(\omega_{n}^{2k})+\omega_{n}^{k}A_2(\omega_{n}^{2k})$

$A(\omega_{n}^k)=A_1(\omega_{\tfrac{n}{2}}^{k})+\omega_{n}^{k}A_2(\omega_{\tfrac{n}{2}}^{k})$

将$\omega_{n}^{k+\frac{n}{2}}$代入得

$A(\omega_{n}^{k}+\tfrac{n}{2})=A_1(\omega_{n}^{2k+n})+\omega_{n}^{k+\tfrac{n}{2}}A_2(\omega_{n}^{2k+n})$

$A(\omega_{n}^{k}+\tfrac{n}{2})=A_1(\omega_{n}^{2k}\cdot\omega_{n}^{n})-\omega_{n}^{k}A_2(\omega_{n}^{2k}\cdot\omega_{n}^{n})$

$A(\omega_{n}^{k}+\tfrac{n}{2})=A_1(\omega_{n}^{2k})-\omega_{n}^{k}A_2(\omega_{n}^{2k})$

$A(\omega_{n}^k)=A_1(\omega_{\tfrac{n}{2}}^{k})-\omega_{n}^{k}A_2(\omega_{\tfrac{n}{2}}^{k})$

显然的,这两个式子只有常数项不同

当$k$取遍$[0,\frac{n}{2}-1]$中所有值时$k+\dfrac{n}{2}$也取遍$[\dfrac{n}{2},n-1]$中所有值

因此,问题的规模缩小了一半,我们只需要在$[0,\dfrac{n}{2}-1]$中枚举$k$,这样就可以算出$A(\omega_{n}^i)\quad(i\in[0,n-1])$的所有值

如果我们已知$A_1(x),A_2(x)$在$\omega_{\tfrac{n}{2}}^0,\omega_{\tfrac{n}{2}}^1,\dots,\omega_{\tfrac{n}{2}}^{\tfrac{n}{2}-1}$的值,通过上面的两个式子就可以在$O(n)$的时间内求出$A(x)$

而求$A_1(x),A_2(x)$正好是求$A(x)$的子问题,并且可以递归求解

03快速傅里叶逆变换

在上面我们将一个多项式的系数表示转换成了点值表示,这里我们要研究将一个多项式的点值表示转换成系数表示

记$(a_0,a_1,\dots,a_{n-1})$是$A(x)$的系数向量,而我们已知$A(x)$的点值表达为$(A(x_0),A(x_1),\dots,A(x_{n-1}))$

设向量$(d_0,d_1,\dots,d_{n-1})$是以$(a_0,a_1,\dots,a_{n-1})$为系数向量,快速傅里叶变换求得的点值表示

构造一个多项式$F(x)=d_0+d_1x+d_2x^2+\dots+d_{n-1}x^{n-1}$

设$(c_0,c_1,\dots,c_{n-1})$是$F(x)$在$x=\omega_n^{-k}$时的点值表示,即$c_k=F(\omega_n^k)$,也就是$c_k=\sum_{i=0}^{n-1}d_i(\omega_n^{-k})^i$

我们知道$d_k=A(\omega_n^k)$,也就是$d_k=\sum_{j=0}^{n-1}a_j(\omega_n^k)^j$

联立上面两个和式得

$c_k=\sum_{i=0}^{n-1} [\sum_{j=0}^{n-1}a_j(\omega_n^i)^j] (\omega_n^{-k})^i$

$\quad \:=\sum_{i=0}^{n-1} \sum_{j=0}^{n-1}a_j(\omega_n^j)^i (\omega_n^{-k})^i$

$\quad \:=\sum_{j=0}^{n-1} a_j \sum_{i=0}^{n-1} (\omega_n^j \omega_n^{-k})^i$

$\quad \:=\sum_{j=0}^{n-1} a_j \sum_{i=0}^{n-1} (\omega_n^{j-k})^i$

我们分情况讨论后面的一个和式$\sum_{i=0}^{n-1} (\omega_n^{j-k})^i$

$j \neq\ k$

那么后面的一个和式就转换为一个等比求和

$\sum_{i=0}^{n-1} (\omega_n^{j-k})^i=\frac{\omega_n^0 [1-(\omega_n^{j-k})^n]}{1-\omega_n^{j-k}}$

$\qquad \qquad \quad \: \: \:=\frac{1-(\omega_n^{j-k})^n}{1-\omega_n^{j-k}}$

$\qquad \qquad \quad \: \: \:=\frac{1-(\omega_n^n)^{j-k}}{1-\omega_n^{j-k}}$

$\qquad \qquad \quad \: \: \:=\frac{1-1^{j-k}}{1-\omega_n^{j-k}}$

$\qquad \qquad \quad \: \: \:=\frac{0}{1-\omega_n^{j-k}}$

$\qquad \qquad \quad \: \: \:=0$

$j = k$

那么$\omega_n^{j-k} = 1$

$\sum_{i=0}^{n-1} (\omega_n^{j-k})^i = n$

由上面两种情况,我们知道当且仅当$j = k$时,整个式子才有值,其余情况都为$0$

所以有

$c_j=a_jn$

$a_j = \frac{c_j}{n}$

到这里,我们就求出了$A(x)$的系数表达

从整个分析过程看,我们是将$A(x)$的点值表示$(A(x_0),A(x_1),\dots,A(x_{n-1}))$当作一个新的多项式$F(x)$的系数表示,再对$F(x)$做快速傅里叶变换得到$(c_0,c_1,\dots,c_{n-1})$,然后再除以$n$就得到$A(x)$的系数表示了。需要指出的是,快速傅里叶变换中$x=\omega_n^k$但是在逆变换中代入的是$\omega_n^{-k}$

04实现

学会了前面的方法,具体实现就不难了

对于求$C(x)=A(x) \cdot B(x)$

将$A(x)$和$B(x)$都转化成点值表达,即$(a_0,a_1,\dots,a_{n-1})$和$(b_0,b_1,\dots,b_{n-1})$

对应相乘$(a_0b_0,a_1b_1,\dots,a_{n-1}b_{n-1})$,再将这一结果变换成$C(x)$的系数表达就完成了

贴一份C++的代码,这是洛谷上的FFT板子题P3803

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define MAXN 4000006
using namespace std;
class complex
{
public:
complex(){}
complex(double a,double b)
{
this->a=a;
this->b=b;
}
double a,b;
}a[MAXN],b[MAXN];
complex operator+ (complex x,complex y)
{
return complex(x.a+y.a,x.b+y.b);
}
complex operator- (complex x,complex y)
{
return complex(x.a-y.a,x.b-y.b);
}
complex operator* (complex x,complex y)
{
return complex(x.a*y.a-x.b*y.b,x.a*y.b+x.b*y.a);
}
const double pi=acos(-1.0);
void FFT(int l,complex *arr,int f)
{
if(l==1) return;
int dl=l>>1;
complex a1[dl],a2[dl];
for(int i=0;i<l;i+=2)
{
a1[i>>1]=arr[i];
a2[i>>1]=arr[i+1];
}
FFT(dl,a1,f);
FFT(dl,a2,f);
complex wn=complex(cos(2.0*pi/l),sin(2.0*pi/l)*f),w=complex(1.0,0.0);
for(int i=0;i<dl;i++,w=w*wn)
{
arr[i]=a1[i]+w*a2[i];
arr[i+dl]=a1[i]-w*a2[i];
}
}
int n,m,N;
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<=n;i++)
scanf("%lf",&a[i].a);
for(int i=0;i<=m;i++)
scanf("%lf",&b[i].a);
N=1;
while(N<n+m+1) N<<=1;
FFT(N,a,1);
FFT(N,b,1);
for(int i=0;i<N;i++)
a[i]=a[i]*b[i];
FFT(N,a,-1);
for(int i=0;i<n+m+1;i++)
printf("%d ",(int)(a[i].a/N+0.5));
puts("");
return 0;
}

闲着没事干,再贴一份Python的

import numpy as np

pi = np.arccos(-1.0)

def read():
def get_numbers():
try:
read.s = input().split()
read.s_len = len(read.s)
if read.s_len == 0:
get_numbers()
read.cnt = 0
return 1
except:
return 0 if not hasattr(read, 'cnt'):
if not get_numbers():
return 0
if read.cnt == read.s_len:
if not get_numbers():
return 0
read.cnt += 1
return eval(read.s[read.cnt - 1]) n = int(read())
m = int(read()) class Complex:
# 复数类 def __init__(self, a=0.0, b=0.0):
self.a = a
self.b = b def __add__(self, other):
return Complex(self.a + other.a, self.b + other.b) def __sub__(self, other):
return Complex(self.a - other.a, self.b - other.b) def __mul__(self, other):
return Complex(self.a * other.a - self.b * other.b, self.a * other.b + self.b * other.a) def fft(num, f, args):
if num == 1:
return
div_num = num >> 1
a1 = []
a2 = []
for i in range(0, num, 2):
a1.append(args[i])
a2.append(args[i + 1])
fft(div_num, f, a1)
fft(div_num, f, a2)
wn = Complex(np.cos(2.0 * pi / num), np.sin(2.0 * pi / num) * f)
w = Complex(1.0, 0.0) for i in range(0, div_num):
args[i] = a1[i] + w * a2[i]
args[i + div_num] = a1[i] - w * a2[i]
w = w * wn aa = []
bb = []
for j in range(0, n + 1):
aa.append(Complex(float(read()), 0.0))
for j in range(0, m + 1):
bb.append(Complex(float(read()), 0.0)) nn = 1
while nn < n + m + 1:
nn <<= 1 for j in range(n + 1, nn):
aa.append(Complex(0.0, 0.0))
for j in range(m + 1, nn):
bb.append(Complex(0.0, 0.0)) fft(nn, 1, aa)
fft(nn, 1, bb) for j in range(0, nn):
aa[j] = aa[j] * bb[j]
fft(nn, -1, aa) for j in range(0, n + m + 1):
print(int(aa[j].a / nn + 0.5), end=' ')

无奈Python实在是太慢了……

05结语

总算是学会了快速傅里叶变换,某种程度上说是弥补了过去的某些遗憾吧。

这里贴一张大佬的图,解释了FFT的思路

这里也推荐一下大佬的博客,以供参考

快速傅里叶变换(FFT)详解 - 自为风月马前卒 - 博客园 (cnblogs.com)

一小时学会快速傅里叶变换(Fast Fourier Transform) - 知乎 (zhihu.com)

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

  1. 快速傅里叶变换FFT

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

  2. [学习笔记] 多项式与快速傅里叶变换(FFT)基础

    引入 可能有不少OIer都知道FFT这个神奇的算法, 通过一系列玄学的变化就可以在 $O(nlog(n))$ 的总时间复杂度内计算出两个向量的卷积, 而代码量却非常小. 博主一年半前曾经因COGS的一 ...

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

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

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

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

  5. 快速傅里叶变换(FFT)

    扯 去北京学习的时候才系统的学习了一下卷积,当时整理了这个笔记的大部分.后来就一直放着忘了写完.直到今天都腊月二十八了,才想起来还有个FFT的笔记没整完呢.整理完这个我就假装今年的任务全都over了吧 ...

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

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

  7. 基于python的快速傅里叶变换FFT(二)

    基于python的快速傅里叶变换FFT(二)本文在上一篇博客的基础上进一步探究正弦函数及其FFT变换. 知识点  FFT变换,其实就是快速离散傅里叶变换,傅立叶变换是数字信号处理领域一种很重要的算法. ...

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

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

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

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

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

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

随机推荐

  1. Less-(38~41) 堆叠注入

    首先申明,Less-(38~41)可以采取和Less-(1~4)相同的解法:(一一对应) 然而,他们的漏洞其实更大,我们可以做更多具有破坏性的事情. 代码审计: Less-(38~41): 41的$s ...

  2. [对对子队]会议记录5.18(Scrum Meeting5)

    今天已完成的工作 何瑞 ​ 工作内容:搭建第8关 ​ 相关issue:搭建关卡7.8.9 ​ 相关签入:feat:初步搭建了Lv8 吴昭邦 ​ 工作内容:搭建第8关 ​ 相关issue:搭建关卡7.8 ...

  3. AtCoder Beginner Contest 210题解

    A B 过水,略... C 统计长度为k的区间的最多本质不同的数.用尺取法维护下左右指针就可以了.调了许久的原因是更新答案时出现了问题. 当我移动指针时,我们应该移动一个就更新一个,而不是将移动与更新 ...

  4. hdu 2086 A1 = ? (公式推导)

    有如下方程:Ai = (Ai-1 + Ai+1)/2 - Ci (i = 1, 2, 3, .... n).若给出A0, An+1, 和 C1, C2, .....Cn.请编程计算A1 = ? Inp ...

  5. 中文NER的那些事儿4. 数据增强在NER的尝试

    这一章我们不聊模型来聊聊数据,解决实际问题时90%的时间其实都是在和数据作斗争,于是无标注,弱标注,少标注,半标注对应的各类解决方案可谓是百花齐放.在第二章我们也尝试通过多目标对抗学习的方式引入额外的 ...

  6. php linux yaml 的安装和使用

    安装: 1 下载yaml包 wget http://pyyaml.org/download/libyaml/yaml-0.2.2.tar.gz tar -zxvf yaml-0.2.2.tar.gz ...

  7. git push超过100M文件处理方法

    git push超过100M文件处理方法 github 会在你上传文件大于50M的时候,给予警告 ; 大于100M的时候给出 server reject(拒绝上传) 解决方法 保持单个文件在 100 ...

  8. 浅谈Vue中计算属性(computed)和方法(methods)的差别

    浅谈Vue中计算属性(computed)和方法(methods)的差别 源码地址 methods方法和computed计算属性,两种方式的最终结果确实是完全相同 计算属性是基于它们的响应式依赖进行缓存 ...

  9. RDD的详解、创建及其操作

    RDD的详解 RDD:弹性分布式数据集,是Spark中最基本的数据抽象,用来表示分布式集合,支持分布式操作! RDD的创建 RDD中的数据可以来源于2个地方:本地集合或外部数据源 RDD操作 分类 转 ...

  10. Unity——技能系统(三)

    Unity技能系统(三) Unity技能系统(一) Unity技能系统(二) Demo展示 六.Buff系统 buff分为增益和减益buff,应该区分开来: /// <summary> / ...