使用复数(单位根)和分治(蝶形运算)加速多项式乘法。
问题#
计算两个次数为 和 的多项式 的乘积,结果为一个 次多项式 .
朴素#
朴素算法使用系数对系数乘法,要算出结果多项式中的每一项的系数都需要 ,整体复杂度 .
优化#
考虑优化。众所周知,分治能够优化很多问题,而其适用条件是:父问题可以拆成相似的几个子问题,且当子问题的答案已经求出时,能够快速地求解(或称“合并”)出父问题的答案。
乍一看,多项式乘法中似乎没有什么方法,能够施展“先拆分后合并”的思想。但是下面这种方式证明了分治是可行的:
Karatsuba 分治#
可以达到 的复杂度。
流程:
- 先拆分:将 次多项式 按照次数从低到高,以 为界拆成两半: ,而两个子多项式分别是 和 次的. 这个问题中,我们将待乘的两个多项式均如是拆分,得到 .
- 再合并:朴素的算法流程是 ,需要计算 次子多项式乘法。使用 Master Theorem 分析得知其复杂度仍为 . 但是,我们可以利用一个数学技巧:,把中间的两次多项式乘法变成两次多项式加法、一次多项式乘法、两次多项式减法,复杂度来到 .
注意到 的选取可能影响到算法的优劣,当 次数相同时,取 是个不错的选择;但当二者长度不相同时,应当取 ,但此时对于次数较小的一侧会有计算上的浪费。
更多、更快的类似分治优化也存在,但是实现复杂度较高,且其优化效果不如即将介绍的这个全新思路。
FFT#
上述优化是对系数域进行的,离不开系数之间的卷积。但如果利用拉格朗日插值法的思想,唯一确定一个 多项式只需要 个点即可,而每个点只需要在原来的两个多项式中采样后相乘,复杂度为 .
这启发我们将系数乘法转化为点值乘法,完成运算后再逆转换回去,或许能够大幅加快算法速度。
然而,要获得每个点的点值,我们会考虑直接代入 个点,但是每个点的函数值计算复杂度仍然是 ,因此由多项式得到点值这一步本身就是 的。但是,这一步通过一些技巧能够大幅优化。
单位根#
在复平面上,单位圆上的点有很多特殊的性质。
而所谓 次单位根 是指满足 的点。不难发现有 个这样的点且他们均在单位圆上,其表达式是 . 而由于所有的 都能用 表示,我们又将 简记为 ,进而所有 次单位根都可以记为 .
回到上文,我们希望取(至少) 个不同的点并计算多项式在这些点处的值。如果我们令 ,然后取 次单位根,那么总共可以得到 个点(这里 ,即 和 是同规模的)。而我们将看到,计算多项式在这些点的值的时候,由于单位根特殊的数学性质,很多数值可以复用,因此能够大幅简化计算。
具体地,计算一个具有 项的多项式 在 处的值时,我们可以将原式按次数的奇偶性拆分:
然后令 ,,得到 .
此时,我们发现 和 这两个需要计算的部分竟然是偶函数,也就意味着,一旦计算出了 和 ,不但可以得到 ,还可以把 一并算出。 而 正是我们需要计算的另一个单位根。因此,我们利用了单位根的性质,成功做到了算一遍得两个取值。
注意到 仍然是单位根,因此 和 的计算可以很方便地递归进行下去,但这要求 的项数是 的幂。一个常见的做法是给 的高次项补 到最低的 次。
上述做法相当于,将「计算 项多项式在 个点处的函数值」这个任务,转化为了 个「计算 项多项式在 个点处的函数值」的任务,然后进行复杂度为 的合并。总复杂度 .
刚刚所描述的,就是快速离散傅里叶变换(FFT)。
逐点相乘#
通过上述方法, 地得到了 个点处两个多项式的函数值之后,可以进行逐点对应相乘,复杂度 .
从点值还原为系数#
我们刚刚利用 FFT 将多项式从系数形式变换为点值形式,随后逐点相乘得到了
而我们真正想要的,是 的系数形式。
由于 ,并且 的次数小于等于 ,因此 可以被这 个点唯一确定。换言之,我们只需做一次插值即可还原出 的系数。
但朴素的拉格朗日插值仍然是 的,因此我们必须进一步利用单位根的性质。
事实上,FFT 所做的事情等价于离散傅里叶变换(DFT):
而逆变换(IDFT)为:
注意到二者的形式几乎完全一致,只是指数取了负号,并多了一个系数 .
因此,我们可以用几乎同样的分治结构完成逆变换:只需在递归合并时,将旋转因子 换成其逆元 ,最后将所有系数统一除以 即可。
这个过程被称为逆 FFT(IFFT),其复杂度同样为 .
至此,多项式乘法的完整流程为:
- 选取 并对两多项式补零至 项;
- 对 分别进行 FFT 得到点值表示;
- 逐点相乘得到 的点值表示;
- 对 进行 IFFT 得到系数表示(并对结果除以 );
整体复杂度为 ,相比朴素的 有数量级的提升。
最后需要注意,FFT 使用浮点数复数运算,会引入误差,因此输出系数通常需要进行四舍五入处理。对于系数较大的情况,有时还需将系数拆分为高低位分别卷积以避免精度爆炸。
蝶形优化#
可以省去递归分治时使用的 swap 数组,减小空间复杂度,具体可以参考模板代码。
模板代码#
//
// main.cpp
// P3803
//
// Created by Leo Xia on 2026/3/29.
//
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010<<3;//空间复杂度可能达到 (n+m)*4 ≈ 8n
const double Pi = acos(-1.0);
struct C//Complex number
{
double x,y;
C()=default;
C(double x_,double y_):x(x_),y(y_){}
C operator+(const C&b)const
{return {x+b.x,y+b.y};}
C operator-(const C&b)const
{return {x-b.x,y-b.y};}
C operator*(const C&b)const
{return {x*b.x-y*b.y,x*b.y+y*b.x};}
}a[N],b[N];
int n,m;
int lim=1,l;
int r[N];
void fft(C p[], int sgn)
{
for(int i=0;i<lim;i++)
if(i<r[i])swap(p[i],p[r[i]]);
for(int mid=1;mid<lim;mid<<=1)
{
C wn(cos(Pi/mid),sgn*sin(Pi/mid));
for(int len=mid<<1,j=0;j<lim;j+=len)
{
C w(1,0);
for(int k=0;k<mid;k++,w=w*wn)
{
C x=p[j+k],y=w*p[j+k+mid];
p[j+k]=x+y;
p[j+k+mid]=x-y;
}
}
}
}
int main()
{
cin>>n>>m;
for(int i=0;i<=n;i++)cin>>a[i].x;
for(int i=0;i<=m;i++)cin>>b[i].x;
while(lim<=n+m)
{
lim<<=1;
l++;
}
for(int i=0;i<lim;i++)
{
r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
}
fft(a,1);
fft(b,1);
for(int i=0;i<lim;i++)
a[i]=a[i]*b[i];
fft(a,-1);
for(int i=0;i<=n+m;i++)
cout<<(int)(a[i].x / lim + 0.5)<<' ';//误差处理
return 0;
}
cpp