Loading

快速傅里叶变换

FFT#

问题:

A(x)=i=0naixiB(x)=i=0mbixi。求 A(x)B(x) 的卷积。

有一个结论:坐标系中 n 个点确定一个 n1 次函数。

可以这样理解:n1 次函数有 n 个系数,而 n 个点相当于 n 个方程。

于是我们可以换一种思路求卷积:将 n+m+1 个不同的 x 代入多项式,那么其卷积的结果的函数 C(x) 必然经过 (xi,A(xi)B(xi)),于是再通过插值就可以得到卷积结果了。

但是,这样子复杂度仍然不变,那该怎么办呢?

于是我们考虑用复数解决问题,在一个单位圆上,圆上的点表示 z=cosθ+isinθ0θ<2π)。然后我们把圆分成 n 等份,便有了单位根:wnk=cos2πkn+isin2πkn,它有很好的性质:

1.wna×wnb=wna+b,其实就是积化和差直接推式子就可以证了,并且也可以通过这个简单推出 (wna)b=wnab。不过这个式子其实是最最最重要的,也是唯一用到虚数的性质。

2.wnk+n2=wnk,学过三角函数就会发现很显然。

3.wn2k=wn2k,显然。

4.wnk=wk+n,显然。

然后,考虑如何快速计算多项式的值,通过思考可以得到:

A(x)=a0+a1x1++an1xn1+anxn=(a0+a2x2++an2xn2)+(a1+a3x2++an1xn2)x=A1(x2)+A2(x2)x

注意:A1(x)=a0+a2x+a4x2++an2xn21A2 同理可推,然后在 FFT 中我们规定 n=2iZ,并且我们在运算中是不会管第 n 项的,故 n 必须大于多项式的次数。

但是这并不能优化我们的复杂度,于是我认为 FFT 一个很厉害的地方来了,我们将单位根 wn0,wn1,,wnn1 分带入多项式并求值,那么:

0k<n2 时:

A(wnk)=A1(wn2k)+wnkA2(wn2k)

n2k<n 时:

A(wnk)=A1(wn2k)wnkA2(wn2k)

这样,我们就有了较大的突破,因为这样子,我们要求的东西就是可转移的了,它就相当于一个 f(n)=f1(n)+k×f2(n),于是,要求它每一项的值就可以通过递归实现了,为了更好的理解这里给出代码:

void FFT (int lim, comp F[])
{
	if (lim == 1) return ;
	comp F1[lim >> 1], F2[lim >> 1];
	rep (i, 0, (lim >> 1) - 1) F1[i] = F[i << 1], F2[i] = F[i << 1 | 1];
	FFT (lim >> 1, F1), FFT (lim >> 1, F2);
	comp w = (comp) {1, 0};
	comp wk = (comp) {cos (2.0 * PI / lim), sin (2.0 * PI / lim)};
	for (int i = 0; i < (lim >> 1); ++ i, w = w * wk)
	{
		F[i] = F1[i] + F2[i] * w;
		F[i + (lim >> 1)] = F1[i] - F2[i] * w;
	}
}

我认为仔细看是可以看懂的,首先我们把系数带进自己的左右节点的数组,然后我们发现每一层完成以后自己的数组变成了每一个 F(wnk) 的答案,然后再转移即可。

于是每个需要的 (xi,A(xi)B(xi)) 便求出来了,然后我们需要考虑如何通过已有坐标推出多项式系数了。

我们设 yi=A(xi)B(xi)C(x)=i=0n1yixi,然后我们分别将 wn0,wn1,,wn1n 带入进这个多项式,设其结果为 z0,,z1n,我们开始求 z

zk=i=0n1yi(wnk)i=i=0n1j=0n1aj(wni)j(wnk)i=j=0n1aji=0n1(wnjk)i

此时,当 j=k 时,内部求和的值显然为 n,而当 jk 时,我们相当于是在求一个等比数列求和,内部求和的值便是 (wnjk)n1wnjk1,其中可以发现 (wnjk)n 就是 wn0=1,于是内部求和的总和就是零,那么可以得出 zk=nakak=zkn。于是我们求出 wnk 就可以求出 zk 了,现在考虑怎么求:

wnk=cosθ+isinθ, 而 wnk=wnk,所以 wnk=cosθisinθ,这个东西也可以通过 FFT 求,于是我们就学会了 FFT 了!接下来上的代码:

void FFT (int lim, comp F[], int op)
{
	if (lim == 1) return ;
	comp F1[lim >> 1], F2[lim >> 1];
	rep (i, 0, (lim >> 1) - 1) F1[i] = F[i << 1], F2[i] = F[i << 1 | 1];
	FFT (lim >> 1, F1, op), FFT (lim >> 1, F2, op);
	comp w = (comp) {1, 0};
	comp wk = (comp) {cos (2.0 * PI / lim), sin (2.0 * PI / lim) * op};
	for (int i = 0; i < (lim >> 1); ++ i, w = w * wk)
	{
		F[i] = F1[i] + F2[i] * w;
		F[i + (lim >> 1)] = F1[i] - F2[i] * w;
	}
}
signed main ()
{
	// freopen ("1.in", "r", stdin);
	// freopen ("1.out", "w", stdout);
	n = rd (), m = rd ();
	int l = 0;
	for (lim = 1; lim <= n + m; lim <<= 1, ++ l) ;
	rep (i, 1, lim) R[i] = (R[i >> 1] >> 1) | (i & 1) << (l - 1);
	rep (i, 0, n) A[i].x = rd ();
	rep (i, 0, m) B[i].x = rd ();
	FFT (lim, A, 1), FFT (lim, B, 1);
	rep (i, 0, lim) A[i] = A[i] * B[i];
	FFT (lim, A, -1);
	rep (i, 0, n + m) printf ("%lld ", (int) (A[i].x / lim + 0.5));
}

这里的 op 就是表示带进去的是 wnk 还是 wnk,然后还有要注意的就是 lim 其实就是重置成 2 的次幂的新的 n,显然对于答案不会产生任何影响,并且复杂度跟线段树一样,O(nlogn)

这个写法效率很慢,有一个叫蝴蝶变换的更优秀的规律可以优化它,这里不多赘述。

作者:lalaouye

出处:https://www.cnblogs.com/lalaouyehome/p/18063511

版权:本作品采用「114514」许可协议进行许可。

posted @   lalaouye  阅读(34)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示