[多项式] FFT小计
引入
给出两个多项式 ,计算它们相乘的结果。
我们能轻易写出 code:
for(int i=0;i<=n;i++)
for(int j=0;j<=n;j++)
C[i+j]+=A[i]*B[j];
然后超时了。
FFT 是一种将多项式乘法优化成 的神仙算法。
接下来会介绍傅里叶的四个神仙 trick。
分析
上面的式子没有任何优化空间。
什么意思呢?就是怎么乱搞都优化不了。于是我们看看天才算法是怎么搞的。
点值表示法
我们知道 个点确定一条 次多项式。
所以我们把多项式转到平面上。
我们随便取 个点,先在 中计算出这些点,表示成 ,然后在 中计算出这些点: ,然后我们就能很容易计算出 。然后我们用 个点确定一条多项式。
天才思路!但是是不是有什么不对的地方?
计算一个点是 的,然后有 个点要计算...时间复杂度 。
如果只是随便取点,就是低估了傅里叶的上限了。
递归计算
我们想想我们有没有知道一个点 的值,就能马上另一点的值呢?
我们先来看二次函数 :
如果知道 ,马上就能知道 。
再来看看三次函数 ,和二次函数一样,但是加个负号就行,说人话就是奇偶函数。
我们推广到多项式这一般形式:
我们把奇偶次数分开一下:
假设我们算出 点的取值为 ,那么 点的取值不就是 吗!
也就是说,我们现在只需要取 这么多点就行了。
然后能不能继续拓展?
这个看起来能递归的样子:
我们定义函数 表示给这个函数一个 次的多项式,它会返回 个点和它们的取值。
我们看看这个式子:
我们看看括号的式子,我们把 看成 ,这不就是子问题吗!
所以递归求解就行了!
举个例子,假设我们计算 ,我们计算子问题 的取值就行了。
等等,是不是有些问题?
新要计算的数全是平方项,都是正数,不存在相反数,所以递归不成立(悲)。所以难道就只能结束了吗?
取点
这里是 FFT 算法最天才的部分。
我们想想刚才的递归。
假设第一轮取的四个数是
那么第二轮的两个数是
最后一轮取的数就是 ,好消息是,最后一轮这个 我们可以随便取,所以我们令 。
我们往下推,,所以我们取 ,所以我们已经确定了两项:。
我们看另一部分, ,所以 这不显然无解吗。
所以傅里叶最天才的部分就到了,实数部分没有,取虚数不就行了!
所以 。
我们继续看上面的例子。
我们计算子问题 的取值。
假设我们满足
这个时候我们计算 就行。
最后,我们发现,其实我们就是解一个方程:
整个流程是一棵二叉树。
我们想要对于每个节点,它的两个叶子节点成相反数,当前节点的值等于叶子节点值的平方。
看这个图,也可以说,FFT 一次性就是在处理每一层的信息,我们也能感受到 FFT 的时间复杂度: 。
问题来了:这些数怎么取呢?
这部分不是 FFT 重点,请看 这个。
知道单位根以后,这部分就很简单了。
我们看看这个图:
所以,对于任意一个点,若是 ,那么它的儿子分别是 。
于是,我们在 的优秀时间复杂度内完成了取点。
点值转多项式
这个怎么办呢?
拉插的 明显无法接受。
我们知道最终的点值表示:
emm这个正推很搞,直接给出结论:
我们反证。
我们把 给带入一下。
我们分类讨论一下。
-
此时原式子等价于 。 -
我们定义 。
原式等价于
我们把 枚举提前,相当于枚举 。
我们看这个部分。
玄学的地方就在这里:
设
则
由于 ,所以 ,所以 。所以这一部分没有贡献。
所以我们看到这个公式:
那一项我们最后消掉即可。然后我们发现这个式子怎么长得和得到的式子这么像啊,所以我们直接对 做转点值,但是系数是 。这与 等价。所以可以进行 加速。
还是详细写一下这里:
举个例子:
那么根据 DFT 能得到:
改造一下就是:
所以可以进行迭代。
至此,经过了四个神仙优化的锤炼后,理论部分终于完工。
代码部分
我们知道原理后,代码变得异常好写。
#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;
}
虽然没超时,但是很慢。
其中 是指向 数组的指针。 可以取到 往后 位的指针。
接下来介绍迭代实现。
迭代实现
我们回到这个性质:
对于一个操作 ,它的左节点是 ,右节点是
画的太屎了
我们可以发现,在第 层往右走,那么它会加上 ,往左则不变。
我们再仔细想想这个操作:
- 如果往左走,不变。
- 如果往右走,在最高位补 。
我们分析最后一层第 个数(不算 )。
将 进行二进制拆分,显然是 。
那么我们能分析它的行走规律:第 层右走,第 层右走,第 层左走。
所以它的值就是 。
我们可以发现,每个数本质上就是当前的数翻转。
这个东西叫蝴蝶变换。
我们把最后一层的搞出来以后,往后的就直接搞就行了。
这样就把递归去了。
所以现在问题就是……
怎么快速求出一个数 的二进制翻转呢?
固然可做,但是太慢了。
假设我们已经处理出 ,怎么求 的翻转?
举个例子:
我们求 的翻转。
我们已经知道了 的翻转。
所以我们先得到 的翻转:
我们知道,括号部分是我们想要的,我们右移一位就得到了。
最后判断第一位是不是 ,是的话在最高位补就 ok 了。
数列求完后,我们想想怎么改变原数组,我们知道开一个新数组是很浪费时间的。我们想要原地搞。
比如说, 的翻转是 , 的翻转就是 。所以我们交换一下这两个位置的数就行了。
我们的迭代 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
我们定义一个函数 ,。
然后给它平方一下。
所以虚部就是我们想要的,这样两次就能搞了。
所以一般数满足的,多项式一般都有,这个时候多项式就产出来全家桶了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】