FFT学习笔记

FFT

FFT 常用于加速多项式乘法。

点值表示法#

先考虑如何表示一个多项式。

最常见的是给定长度为 n+1 的系数序列 a 来表示多项式 F(x)=i=0naixi,做多项式乘法时直接乘法分配律,时间复杂度是 O(n2) 的。

另一种方法叫做点值表示法,也就是用平面上 n+1 个点(横坐标两两不同)来确定一个 n 次的多项式。

假设有两个多项式 F(x)G(x),最高次分别为 nm。那么计算 H(x)=F(x)G(x) 时,任取 x1,x2,,xn+m+1,求出所有 F(xi)G(xi),那么 H(xi)=F(xi)+G(xi)。这样子时间复杂度是 O(n) 的。

FFT 的作用就是:将系数表示法在 O(nlogn) 转化成点值表示法。然后在将点值表示法用 IFFT 转化回系数表示法。

快速傅里叶变换#

对于一个多项式 F(x)=i=0naixi,目的是快速求出在一些 x 上的取值。

先考虑一种特殊情况:F(x)=x2 时,显然求出 F(x) 时也知道 F(x) 的值,因为它是偶函数;类似的 F(x)=x3 时,知道 F(x) 也能知道 F(x),因为它是奇函数。

幸运的是 x 可以任意取,所以可以取正负成对的 x,减少了一半的计算量。

对于任意一个 F(x),可以将其分解:

F(x)=a0+a1x1+a2x2+a3x3++anxn=(a0+a2x2+a4x4+)+x(a1+a3x2+a5x4+)

Fe(x)=a0+a2x+a4x2+

Fo(x)=a1+a3x+a5x2+

显然 Fe(x2)Fo(x2) 都是偶函数,那么

F(x)=Fe(x2)+xFo(x2)

F(x)=Fe(x2)xFo(x2)

FeFo 都是 n2 次的,而求 Fe(x2)Fo(x2) 是原问题的子问题,可以考虑递归解决。

这里有个问题:x 是正负成对的,但是 x2 不一定是正负成对的。上述优化只有在 x 正负成对时有效,所以考虑构造一组 x,使得 x2 也是正负成对的。

显然实数是绝对不行的,考虑在复数域上取 x

单位根#

n=2k 以下默认 F(x)n1 次的多项式。如果不是,补上系数为 0 的高次项即可。

每个复数 a+bi 都在复平面上对应一个向量 (a,b),而两个复数相乘相当于模长相乘,幅角相加

复数 a+biabi 共轭,记为 a+bi=abi。共轭在复平面上的表现是关于实轴对称。

在复平面上作单位圆,然后将单位圆 n 等分,显然所有 n 等分点对应的复数都是 xn=1 的解(模长乘积为 1,幅角相加为 2kπ)。定义其中幅角为正且最小的复数为 ωn,即 n 次单位根。根据定义有

ωn=cos2πn+isin2πn

ωnk=cos2kπn+isin2kπn

ωnk+n2=ωnk

因为 n=2k,那么 (a,b)(a,b) 同时在圆上,满足正负成对;同时 (ωn0)2,(ωn1)2,,(ωnn21)2 是单位圆的 n2 等分点,所以也满足正负成对。

回到 FFT,我们找到了平方之后也能正负成对的 x=ωnk,要求所有 0kn2F(ωnk)。那么:

F(ωnk)=Fe(ωn2k)+ωnkFo(ωn2k)=Fe(ωn2k)+ωnkFo(ωn2k)

F(ωnk)=F(ωnk+n2)=Fe(ωn2k+n)+ωnk+n2Fo(ωn2k+n)=Fe(ωn2k)ωnkFo(ωn2k)

然后 Fe(ωn2k)Fo(ωn2k) 就真的是子问题了,递归解决即可。时间复杂度 O(nlogn)

快速傅里叶逆变换#

已经完成系数表示法到点值表示法的转换了,现在要转回去。

设系数为 a0,a1,,an1,那么,

[11111(ωn1)1(ωn1)2(ωn1)n11(ωn2)1(ωn2)2(ωn2)n11(ωnn1)1(ωnn1)2(ωnn1)n1][a0a1a2an1]=[F(1)F(ωn1)F(ωn2)F(ωnn1)]

而从 F(ωnk) 转回 ak,只需要将左边的矩阵求逆即可。求逆的结果为:

C=1n[11111(ωn1)1(ωn1)2(ωn1)n11(ωn2)1(ωn2)2(ωn2)n11(ωnn1)1(ωnn1)2(ωnn1)n1]

直接矩阵乘向量是 O(n2) 的。但是共轭在复平面上表现为关于实轴对称,单位根的性质不变。所以可以套用 FFT 的过程,但是 ωn=cos2πnisin2πn

Code
#include <bits/stdc++.h>
const int M = 5e6 + 10;
const double pi = acos(-1);

struct Complex{
	double real, imag;
	Complex(double _r = 0, double _i = 0){
		real = _r, imag = _i;
	}
};
inline Complex operator + (Complex A, Complex B){
	return Complex(A.real + B.real, A.imag + B.imag);
}
inline Complex operator - (Complex A, Complex B){
	return Complex(A.real - B.real, A.imag - B.imag);
}
inline Complex operator * (Complex A, Complex B){
	return Complex(A.real * B.real - A.imag * B.imag, A.real * B.imag + B.real * A.imag);
}

int n, m, len;
Complex a[M], b[M];

inline void FFT(Complex* a, int len, int v) {
	if (len == 1) return;
	Complex e[len >> 1], o[len >> 1];
	for (int i = 0; i < len; i += 2)
		e[i >> 1] = a[i], o[i >> 1] = a[i + 1];
	FFT(o, len >> 1, v), FFT(e, len >> 1, v);
	Complex Wn(cos(2 * pi / len), v * sin(2 * pi / len)), w(1, 0);
	for (int i = 0; i < (len >> 1); i++, w = w * Wn)
		a[i] = e[i] + w * o[i], a[i + (len >> 1)] = e[i] - w * o[i];
}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 0; i <= n; i++) scanf("%lf", &a[i].real);
	for (int i = 0; i <= m; i++) scanf("%lf", &b[i].real);
	
	len = 1;
	while (len < n + m + 1) len <<= 1;
	FFT(a, len, 1), FFT(b, len, 1);
	for (int i = 0; i <= len; i++) a[i] = a[i] * b[i];
	FFT(a, len, -1);
	for (int i = 0; i <= n + m; i++)
		printf("%d ", (int)(a[i].real / len + 0.5));
	return 0;
}

蝴蝶变换#

递归的开销太大了,考虑迭代实现 FFT。

假设一个八阶 FFT,在递归的过程中,考虑每个元素的位置:

第一层:[a0,a1,a2,a3,a4,a5,a6,a7]

第二层:[a0,a2,a4,a6][a1,a3,a5,a7]

第三层:[a0,a4][a2,a6][a1,a5][a3,a7]

第四层:[a0][a4][a2][a6][a1][a5][a3][a7]

数字 二进制
0 000
4 100
2 010
6 110
1 001
5 101
3 011
7 111

容易发现,如果将二进制表示倒过来,就是 07。所以可以预处理出每个元素最后的位置,然后从底层做到顶层,就可以规避递归。

Code
inline void FFT(Complex* a, int v) {
	for (int i = 0; i < n; i++)
        rev[i] = (rev[i >> 1] >> 1) | ((i & 1) ? (n >> 1) : 0);
	for (int i = 0; i < n; i++)
		if (rev[i] > i) std::swap(a[i], a[rev[i]]);
	for (int k = 2, l = 1; k <= n; k <<= 1, l <<= 1) {
		Complex Wn = Complex(cos(2 * pii / k), v * sin(2 * pii / k));
		for (int i = 0; i < n; i += k) {
			Complex w = Complex(1, 0);
			for (int j = 0; j < l; j++, w = w * Wn) {
				Complex tmp = w * a[i + j + l];
				a[i + j + l] = a[i + j] - tmp;
				a[i + j] = a[i + j] + tmp;
			}
		}
	}
}

作者:zzxLLL

出处:https://www.cnblogs.com/zzxLLL/p/17893319.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   zzxLLL  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示