[多项式] FFT小计

引入

给出两个多项式 A,B ,计算它们相乘的结果。

我们能轻易写出 code:

for(int i=0;i<=n;i++)
	for(int j=0;j<=n;j++)
		C[i+j]+=A[i]*B[j];

然后超时了。
FFT 是一种将多项式乘法优化成 O(nlogn) 的神仙算法。
接下来会介绍傅里叶的四个神仙 trick。

分析

上面的式子没有任何优化空间。
什么意思呢?就是怎么乱搞都优化不了。于是我们看看天才算法是怎么搞的。

点值表示法

我们知道 n 个点确定一条 n1 次多项式。
所以我们把多项式转到平面上。
我们随便取 2n+1 个点,先在 A 中计算出这些点,表示成 (x,yA),然后在 B 中计算出这些点:(x,yB) ,然后我们就能很容易计算出 yC=yA×yB 。然后我们用 n 个点确定一条多项式。

天才思路!但是是不是有什么不对的地方?
计算一个点是 O(n) 的,然后有 n 个点要计算...时间复杂度 O(n2)

如果只是随便取点,就是低估了傅里叶的上限了。

递归计算

我们想想我们有没有知道一个点 x1 的值,就能马上另一点的值呢?
我们先来看二次函数 f(x)=x2
如果知道 f(5)=25,马上就能知道 f(5)=25
再来看看三次函数 f(x)=x3,和二次函数一样,但是加个负号就行,说人话就是奇偶函数。

我们推广到多项式这一般形式:
A(x)=16x5+22x4+7x3+5x2+13x+8
我们把奇偶次数分开一下:

p=16x5+7x3+13x

q=22x4+5x2+8

假设我们算出 x 点的取值为 p+q ,那么 x 点的取值不就是 qp 吗!

也就是说,我们现在只需要取 n2 这么多点就行了。

然后能不能继续拓展?
这个看起来能递归的样子:
我们定义函数 D(A) 表示给这个函数一个 n 次的多项式,它会返回 n+1 个点和它们的取值。
我们看看这个式子:

A(x)=x(16x4+7x2+13)+(22x4+5x2+8)

我们看看括号的式子,我们把 x2 看成 x ,这不就是子问题吗!
所以递归求解就行了!
举个例子,假设我们计算 x1,x2,x3,x4,x1,x2,x3,x4,我们计算子问题 x12,x22,x32,x42 的取值就行了。

等等,是不是有些问题?
新要计算的数全是平方项,都是正数,不存在相反数,所以递归不成立(悲)。所以难道就只能结束了吗?

取点

这里是 FFT 算法最天才的部分。
我们想想刚才的递归。
假设第一轮取的四个数是 x1,x2,x1,x2
那么第二轮的两个数是 x12,x12
最后一轮取的数就是 x4,好消息是,最后一轮这个 x 我们可以随便取,所以我们令 x=1
我们往下推,x4=x12,所以我们取 x1=1,所以我们已经确定了两项:x1=1,x1=1
我们看另一部分, x22=x12=1,所以 x22=1 这不显然无解吗。

所以傅里叶最天才的部分就到了,实数部分没有,取虚数不就行了!
所以 x2=i

我们继续看上面的例子。
我们计算子问题 x12,x22,x32,x42 的取值。
假设我们满足 x22=x12,x42=x32

这个时候我们计算 x14x34 就行。
最后,我们发现,其实我们就是解一个方程: x8=1

image

整个流程是一棵二叉树。
我们想要对于每个节点,它的两个叶子节点成相反数,当前节点的值等于叶子节点值的平方。
看这个图,也可以说,FFT 一次性就是在处理每一层的信息,我们也能感受到 FFT 的时间复杂度:O(nlogn)
问题来了:这些数怎么取呢?
这部分不是 FFT 重点,请看 这个

知道单位根以后,这部分就很简单了。
我们看看这个图:
image

A:ω20(=1),B:ω21
D:ω40(=1),C:ω42,F:ω41,E:ω43
所以,对于任意一个点,若是 ωnk,那么它的儿子分别是 ω2nk,ω2nk+n
于是,我们在 O(nlogn) 的优秀时间复杂度内完成了取点。

点值转多项式

这个怎么办呢?
拉插的 O(n2) 明显无法接受。
我们知道最终的点值表示:G(k)=i=0n1F(i)(ωnk)i
emm这个正推很搞,直接给出结论: F(k)×n=i=0n1(ωnk)iG(i)

我们反证。
我们把 G(i) 给带入一下。
i=0n1(ωnk)iG(i)
=i=0n1(ωnk)ij=0n1F(j)(ωni)j
=i=0n1j=0n1ωnikF(j)ωnij
=i=0n1j=0n1F(j)ωni(jk)

我们分类讨论一下。

  • 1.j=k
    此时原式子等价于 nF(k)

  • 2.jk
    我们定义 p=jk
    原式等价于
    =i=0n1j=0n1F(p+k)ωnip
    我们把 j 枚举提前,相当于枚举 p
    =j=0n1i=0n1F(p+k)ωnip

我们看这个部分。
i=0n1F(p+k)ωnip
=F(p+k)i=0n1ωnip

玄学的地方就在这里:

i=0n1ωnip=0(p0)

S=i=0n1(ωnp)i
ωnpSS=ωnnωn0=0
由于 p0,所以 ωnp1,所以 S=0。所以这一部分没有贡献。

所以我们看到这个公式:
F(k)×n=i=0n1(ωnk)iG(i)
×n 那一项我们最后消掉即可。然后我们发现这个式子怎么长得和得到的式子这么像啊,所以我们直接对 G 做转点值,但是系数是 ωn0,ωn1,ωn2...ωn(n1)。这与 ωn0,ωnn1,ωnn2...ωn1 等价。所以可以进行 FFT 加速。

还是详细写一下这里:
举个例子:
ω40,ω41,ω42,ω43
那么根据 DFT 能得到:
ω80,ω81,ω82,ω83,ω84,ω83,ω82,ω81
改造一下就是:
ω80,ω81,ω82,ω83,ω84,ω85,ω86,ω87

所以可以进行迭代。
至此,经过了四个神仙优化的锤炼后,理论部分终于完工。

代码部分

我们知道原理后,代码变得异常好写。

#include<bits/stdc++.h>
#define ld double
#define N 2000005
using namespace std;
const ld Pi=acos(-1);
int n,m;
struct CP{
	ld x,y;
}f[N*2],g[N*2],tmp[N*2];
CP operator +(CP a,CP b){return (CP){a.x+b.x,a.y+b.y};}
CP operator -(CP a,CP b){return (CP){a.x-b.x,a.y-b.y};}
CP operator *(CP a,CP b){return (CP){a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x};}
void FFT(CP *f,int len,bool flag)
{
	if(len==1) return;
	CP *fl=f,*fr=f+len/2;
	for(int i=0;i<len;i++) tmp[i]=f[i];
	for(int i=0;i<len/2;i++)
		fl[i]=tmp[(i<<1)],fr[i]=tmp[(i<<1)|1];
	FFT(fl,len/2,flag),FFT(fr,len/2,flag);
	CP cur=(CP){1,0},w=(CP){cos(2*Pi/len),(flag?1:-1)*sin(2*Pi/len)};
	for(int i=0;i<len/2;i++)
	{
		tmp[i]=fl[i]+cur*fr[i];
		tmp[i+len/2]=fl[i]-cur*fr[i];
		cur=cur*w;
	}
	for(int i=0;i<len;i++) f[i]=tmp[i];
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=0;i<=n;i++) scanf("%lf",&f[i].x);
	for(int i=0;i<=m;i++) scanf("%lf",&g[i].x);
	for(m=n+m+1,n=1;n<m;n<<=1);
	FFT(f,n,1);FFT(g,n,1);
	for(int i=0;i<n;i++) f[i]=f[i]*g[i];
	FFT(f,n,0);
	for(int i=0;i<m;i++)
		printf("%d ",(int)(f[i].x/n+0.5));
	return 0;
}

虽然没超时,但是很慢。
其中 f 是指向 f 数组的指针。f+x 可以取到 f 往后 x 位的指针。

接下来介绍迭代实现。

迭代实现

我们回到这个性质:

对于一个操作 ωnk,它的左节点是 ω2nk,右节点是 ω2nn+k

image

画的太屎了

我们可以发现,在第 x 层往右走,那么它会加上 2x,往左则不变。
我们再仔细想想这个操作:

  • 如果往左走,不变。
  • 如果往右走,在最高位补 1

我们分析最后一层第 6 个数(不算 0)。
6 进行二进制拆分,显然是 110
那么我们能分析它的行走规律:第 1 层右走,第 2 层右走,第 3 层左走。
所以它的值就是 20+21=3
我们可以发现,每个数本质上就是当前的数翻转
这个东西叫蝴蝶变换。

我们把最后一层的搞出来以后,往后的就直接搞就行了。
这样就把递归去了。

所以现在问题就是……
怎么快速求出一个数 x 的二进制翻转呢?
nlogn 固然可做,但是太慢了。

假设我们已经处理出 0x1,怎么求 x 的翻转?

举个例子:
我们求 (00110)1 的翻转。
我们已经知道了 0(00110) 的翻转。
所以我们先得到 0(00110) 的翻转:(01100)0
我们知道,括号部分是我们想要的,我们右移一位就得到了。
最后判断第一位是不是 1,是的话在最高位补就 ok 了。

数列求完后,我们想想怎么改变原数组,我们知道开一个新数组是很浪费时间的。我们想要原地搞。

比如说,x 的翻转是 !x!x 的翻转就是 x。所以我们交换一下这两个位置的数就行了。

我们的迭代 FFT 火热出炉!

#include<bits/stdc++.h>
#define ld double
#define N 2000005
using namespace std;
const ld Pi=acos(-1);
int n,m;
int tr[N*2];
struct CP{
	ld x,y;
}f[N*2],g[N*2];
CP operator +(CP a,CP b){return (CP){a.x+b.x,a.y+b.y};}
CP operator -(CP a,CP b){return (CP){a.x-b.x,a.y-b.y};}
CP operator *(CP a,CP b){return (CP){a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x};}
void FFT(CP *f,int n,bool flag)
{
	for(int i=0;i<n;i++)
		if(i<tr[i]) swap(f[i],f[tr[i]]);
	for(int k=2;k<=n;k<<=1)
	{
		int len=(k>>1);
		CP cur=(CP){1,0},w=(CP){cos(2*Pi/k),(flag?1:-1)*sin(2*Pi/k)};
		for(int i=0;i<n;i+=k)
		{
			cur=(CP){1,0};
			for(int j=i;j<i+len;j++)
			{
				CP tmp=cur*f[j+len];
				f[j+len]=f[j]-tmp;
				f[j]=f[j]+tmp;
				cur=cur*w;
			}
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=0;i<=n;i++) scanf("%lf",&f[i].x);
	for(int i=0;i<=m;i++) scanf("%lf",&g[i].x);
	for(m=n+m+1,n=1;n<m;n<<=1);
	for(int i=0;i<n;i++) tr[i]=(tr[i>>1]>>1)|((i&1)?(n>>1):0);
	FFT(f,n,1),FFT(g,n,1);
	for(int i=0;i<n;i++) f[i]=f[i]*g[i];
	FFT(f,n,0);
	for(int i=0;i<m;i++) printf("%d ",(int)(f[i].x/n+0.5));
	return 0;
}

所以差不多就结束了,这已经是最经典的 FFT 了。

附加:三次变两次 FFT

我们定义一个函数 T(x)T(x)=F(x)+G(x)i
然后给它平方一下。T(x)=F(x)2G(x)2+2F(x)G(x)i
所以虚部就是我们想要的,这样两次就能搞了。

所以一般数满足的,多项式一般都有,这个时候多项式就产出来全家桶了。

posted @   g1ove  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示