【笔记】快速傅里叶变换

0 约定

  1. exp(x)=ex
  2. 有些地方标注有 ?,系本人不太能保证严谨性的部分。

1 复数 (Complex)

1.1 三种形式

  1. 代数形式:z=a+bi,其中 a,bR

  2. 三角形式:z=r(cosθ+isinθ),其中 r=a2+b2a,b 是在代数形式中定义的)。

  3. 指数形式:z=reiθ

    根据 欧拉公式

    eix=cosx+isinx,xC

    x=θ,再带入三角形式中,即可得指数形式。


1.2 单位根 (unit root)

对于方程 xn=1,在复数意义下,她有 n 个解,我们称这 n 个解都是 单位根 (unit root)

ωn=exp2πin,则这 n 个单位根可以表示为 ωnkk=0,1,n1

在复平面中,n 次单位根把单位圆 n 等分。

1.2.1 性质

  1. ωnn=1

    证:ωnn=exp(2πinn)=(expπi)2=(1)2=1

  2. ωnk=ωpnpk

    ω 的上下标同乘 p,相当于 exp 后面的分子分母同乘 p

  3. ω2nk+n=ω2nk

    几何理解:这实际上相当于将单位园 2n 等分,次数 +n 相当于在复平面中,这两个点关于原点对称,故而她们互为相反数。(?

    证:ω2nk+n=exp2πi(k+n)2n=exp(2πik2n+2πin2n)=exp(2πik2n)=ω2nk


2 快速傅里叶变换 (Fast Fourier Transform)

FFT 可以在 O(nlogn) 的时间复杂度内,计算多项式乘法,优于暴力的 O(n2)

2.1 定义

对于一个多项式 f(x),定义傅里叶变换为:

F(f(x))=(f(0),f(ωn),,f(ωnn1))

傅里叶变化本质上可以理解为一种「从一个多项式到一个 n 维向量的映射关系」,将多项式的系数表示转化为点值表示。(?


2.2 求解

2.2.1 引入(不太重要

考虑如何计算 i=0naixi

一般的方法是,枚举 i,然后维护 xi 的值 k,再将 aik 相乘并累加。精细统计的话,这样应该需要 2n 次乘法。

另一种办法是:

i=0naixi=a0+a1x++anxn=a0+x(a1+x(a2+xan))【每次都把后面一部分提一个 x 出来】

如果是这样计算,总共其实只需要 n 次计算。(似乎没什么用


2.2.2 思想

不失一般性的,我们令 n=2p(pN)(如果 n 不足 2p,可将更高位的系数视作 0

借用上述思想,对于一个多项式 f(x),我们可以做如下的变换,即将 f(x)x 次数的奇偶分开:

f(x)=a0+a1x++an1xn1=a0+a2x2++an2xn2+x(a1+a3x2++an1xn2)

f0(x)=a0+a2x1++an2xn/21f1(x)=a1+a3x1++an1xn/21

代回原式:

f(x)=f0(x2)+xf1(x2)

所以我们有:

f(ωnk)=f0(ωn2k)+ωnk×f1(ωn2k)=f0(ωn/2k)+ωnk×f1(ωn/2k)【单位根性质 2】

以及:

f(ωnk+n/2)=f0((ωnk)2)ωnk×f1((ωnk)2)【单位根性质 3】=f0(ωn2k)ωnk×f1(ωn2k)=f0(ωn/2k)ωnk×f1(ωn/2k)【单位根性质 2】

然后我们惊奇的发现,这两个式子只有系数差别,而且 n 的规模每次减半。这就意味着我们可以在 O(nlogn) 的时间复杂度内递归求解出 f(ωnk) 其中 k=0,1,,n1,即求出 F(f(x))​​。

而上面最后推出的式子被称做:蝴蝶操作 (Butterfly Operation)。下面这张图形象地展示了「蝴蝶」的含义:


2.2.3 实现

通过递归我们可以轻易的模拟出上述过程,为减少常数下介绍一种非递归版本。

画出递归树,如下图,并按照下图的方式给每层的 f 编号:

我们发现,最底层的 f 的二进制下标反转过来并转换成十进制,就等于她对应 a 的下标。

所以我们可以直接预处理出最底层的 f,然后自下而上递推回来。(具体实现可以参考 4.1 Code 中的 r​。


3 快速傅里叶逆变换 (Inverse Fast Fourier Transform)

3.1 定义

f 为进行了快速傅里叶变化后的点值,即 F(f(x))

现在考虑还原为原来的系数数列。这样的操作被称之为 快速傅里叶逆变换 (Inverse Fast Fourier Transform)

推导过程比较复杂,这里直接给出结论:

f(x)=1nf(ωnx)


4 优化多项式乘法

那么傅里叶变换的优势在哪呢?这就要问点值表达了。

由于是用点值表示多项式,两个多项式相乘,显然就是每个点的虚数相乘。

最后再做一次逆变换,相当于插值,就可以还原回最终结果了。

4.1 Code

#include <bits/stdc++.h>
using namespace std;
const int N = 4e6 + 5, logN = 20;
const double PI = acos(-1);
int n, m, lim, len, r[N];
struct Complex {
	double a, b;
	Complex operator + (Complex x) { return {a+x.a, b+x.b}; }
	Complex operator - (Complex x) { return {a-x.a, b-x.b}; }
	Complex operator * (Complex x) { return {a*x.a-b*x.b, a*x.b+b*x.a}; }
} a[N], b[N], c[N];
void fft (Complex *f, int sign) {
	for (int i = 0; i < lim; i++)
		if (i < r[i]) swap(f[i], f[r[i]]);
	for (int mid = 1; mid < lim; mid<<=1) {
		Complex o = {cos(PI/mid), sign*sin(PI/mid)};
		for (int R = mid<<1, j=0; j < lim; j += R) {
			Complex p = {1, 0};
			for (int k = 0; k < mid; k++) {
				Complex f0 = f[j+k], f1 = f[j+k+mid];
				f[j+k] = f0 + p*f1;
				f[j+k+mid] = f0 - p*f1;
				p = p*o;
			}
		}
	}
}
int main () {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for (int i = 0, x; i <= n; i++) cin >> x, a[i] = {x*1.0, 0};
	for (int i = 0, x; i <= m; i++) cin >> x, b[i] = {x*1.0, 0};
	for (lim=1, len=0; lim <= n+m; lim<<=1, len++);
	for (int i = 0; i <= lim; i++)
		r[i] = (r[i>>1]>>1)|((i&1)<<(len-1));
	fft(a, 1), fft(b, 1);
	for (int i = 0; i <= lim; i++) c[i] = a[i]*b[i];
	fft(c, -1);
	for (int i = 0; i <= n+m; i++)
		cout << ((int)(c[i].a/lim+0.5)) << ' ';
	return 0;
}

5 例题

「BZOJ4503」两个串

n=|S|,m=|T|

首先将 {?,a,b,,z}{0,1,2,,26}​。然后将 T reverse 下。

s 的第 i 匹配成功,可以转换为:

c[i]=j=0min(m1,i)(s[ij]t[j])2t[j]=0

平方是为了防止负数可能的抵消,如果是绝对值不能化简。

然后暴力拆开看下:

c[i]=j=0min(m1,i)(s[ij]2t[j])+(t[j]3)(2s[ij]t[j]2)

然后发现,这就是两个卷积和一个普通累加相加,fft 加速算即可。


「ZJOI2014」力

Warning: 不同于题面,下面的数组均从 0 开始标号。

令:

Ai=j=0i1qj(ij)2Bi=j=i+1n1qj(ij)2

于是:

Ei=AiBi

考虑分开计算 A,B

再令:

pi=1i2,特别地 p0=0

由于 p0=0,于是 i=j​ 可以被纳入运算,而不造成影响。

再把 q reverse 下,令作 q

于是:

A[i]=j=0iq[j]p[ij]B[i]=j=in1q[j]p[ji]=j=0ni1q[i+j]p[j]=j=0ni1q[n1(i+j)]p[j]

又令:t=ni1

有:

B[i]=j=0tq[tj]p[j]

fft 加速算即可。


万径人踪灭

假定 ai=0(in)

令:t0=2i,t1=2i+1

设:

  • fi :以 i 为对称中心,两边最多能匹配的对数(包括自己);
  • g 是枚举分割线。

则有

{a,b}{1,1}fi=j=0min(i,ni1)a[ij]×a[i+j]+12=14((j=0t0a[t0j]×a[j])+(a[i]×a[i]+1)+min(t0,n1)max(0,t0n+1)+1)gi=j=1min(i,ni)a[ij]×a[i+j1]+12=14((j=0t1a[t1j]×a[j])+min(t1n1)max(0,t0n+1)+1)

然后答案就是:

Ans=i=0n1(2f[i]1)+i=1n1(2g[i]1)res

其中 res 为字符串中回文串个数,这个可以用 Manacher 算。

f,g 卷积算完之后加上后面的那部分即可。


「BZOJ 3771」Triple


「Luogu5488」差分与前缀和

算一次的前缀:

a1[i]=j=0ia[j]a2[i]=j=0ia1[j]=j=0ik=0ja[j]

考虑每个 a[i]Ans 的贡献。

posted @   CloudWings  阅读(165)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示