多项式乘法
我们知道,多项式可以表示成:
对于两个多项式和,我们可以计算乘积:
但是,这样算是的,太慢了,怎么办?
我们需要换一条思路。
首先,我们得知道一个东西:多项式的点值表示法。
我们把上面的称为多项式的系数表示法,而点值表示法就是:
若多项式的次数为,则任取个不相同的,求出多项式的。记为:
对于一个点值表示法下多项式
等等,我们好像找到了一个突破口!
为啥不把原来的系数表示法下的多项式转化成点值表示法呢?
仔细想一想:系数表示法与点值表示法互相转换,这个步骤好像是的。
而FFT(快速傅里叶变换)就是为了优化这个。
PS:对于的点值表示法转化成系数表示法可以看百度百科中关于插值法的介绍。
FFT(快速傅里叶变换)
如果未特别说明,那么下面的多项式次数将是的形式。
如果不是关键部分的公式或定理,不提供证明,自己出门右转百度。
首先介绍两个概念:
DFT(离散傅里叶变换)是将多项式由系数表示法转化成点值表示法;
IDFT(离散傅里叶逆变换)是将多项式由点值表示法转化成系数表示法;
而FFT就是上述两种变换的优化。
DFT部分
前置技能:
下面的内容将会提到复数,不会的可以参考百度百科中关于复数的介绍;
为了介绍FFT中的DFT部分,首先要介绍的是一个概念:单位根。
单位根:若有
若有,显然,可以等于,如果是偶数,则还可以等于。
我们把范围扩大到,那么,我们可以得到个复数,它们将复平面上的单位圆等分成份。
为了表示次单位根,我们引入一个公式。
欧拉公式:
如果我们令:
下面是关于的两条性质:(都是在为偶数的情况下)
下面,我们进入正题:DFT的求法。
在这里,我们令多项式次数为,那么我们可以用点值表示成
额……这时间复杂度好像并没有减少……
别急,我们来看能够表示成什么。
我们来分别看一看这神奇的步骤。
步骤就是将带入原来的多项式。
步骤就是将原多项式拆成两个部分,按奇偶分类。
步骤用到了上面提到的性质。
步骤就是上面式子的后半部分提出公因数。
有了这个等式,我们就可以分治+递归解决DFT了。
算法步骤:
- 对当前的多项式(一个数组)系数进行奇偶分类;
- 递归算出偶数部分的数组的和奇数部分的数组的;
- 这个多项式的
但是这个的常数好像很大啊?能不能减少一点呢?
上面的性质给了我们提示:
在算时,可以顺便把的情况也算出来。
常数减小了一半!但是还是很大啊!
递归版的程序一般比非递归版慢,为啥不用非递归版呢?
算法核心就是奇偶分类,分来分去最后分到了哪里?我们来研究研究。
显然,一个序列原来是,最终变成。
把它们的二进制列出来:
其中,上面是位置,下面是这个位置对应的数。
把上面的数翻转,好像就是下面的数!
没错,只需要计算一下每个数的二进制翻转后的结果,就能得到一个数最终对应的位置,也就能实现非递归版了。
代码:
int dft_fast(complex* ar,int len)
{
for(register int i=0; i<len; ++i)
{
if(rev[i]<i)
{
std::swap(ar[rev[i]],ar[i]);//交换一个位置和它的翻转后位置
}
}
for(register int i=2; i<=len; i<<=1)//i代表当前序列的长度
{
complex wn(cos(2*pi/i),sin(2*pi/i));//omega_n
for(register int j=0; j<len; j+=i)//j代表序列的起始位置
{
complex w(1,0);//下面代表omega_n^k
for(register int k=0; k<(i>>1); ++k)//枚举i次单位根的每一种取值
{
complex x=ar[j+k],y=w*ar[j+k+(i>>1)];
ar[j+k]=x+y;//合并操作,将两边合并成一个点值表示法下的多项式
ar[j+k+(i>>1)]=x-y;
w=w*wn;
}
}
}
return 0;
}
IDFT部分
回顾上面的DFT部分,仔细思考一下,它本质就是在求:
其中,给定了以及,求的值。
用矩阵表示如下:
我们令:
那么IDFT的本质就是求矩阵的逆矩阵。
考虑下面这个矩阵:
那么我们令,则:
显然:
因此,
式两边同时左乘,可得
这就相当于把DFT中都换成。
FFT总代码
int fft(complex* ar,int len,int op)
{
for(register int i=0; i<len; ++i)
{
if(rev[i]<i)
{
std::swap(ar[rev[i]],ar[i]);
}
}
for(register int i=2; i<=len; i<<=1)
{
complex wn(cos(2*pi/i),sin(2*pi*op/i));//只有这里较DFT代码有变动
for(register int j=0; j<len; j+=i)
{
complex w(1,0);
for(register int k=0; k<(i>>1); ++k)
{
complex x=ar[j+k],y=w*ar[j+k+(i>>1)];
ar[j+k]=x+y;
ar[j+k+(i>>1)]=x-y;
w=w*wn;
}
}
}
if(op==-1)
{
for(register int i=0; i<len; ++i)
{
ar[i].r/=len;
}
}
return 0;
}
多项式乘法模板
#include <cstdio>
#include <cmath>
#include <algorithm>
const int maxn=100000;
const double pi=acos(-1);
struct complex
{
double r,i;
complex(double r_=0,double i_=0)
{
r=r_;
i=i_;
}
complex operator +(const complex &other)
{
return complex(r+other.r,i+other.i);
}
complex operator -(const complex &other)
{
return complex(r-other.r,i-other.i);
}
complex operator *(const complex &other)
{
return complex(r*other.r-i*other.i,r*other.i+i*other.r);
}
};
complex a[maxn<<2],b[maxn<<2],c[maxn<<2];
int rev[maxn<<2],n,m;
int fft(complex* ar,int len,int op)
{
for(register int i=0; i<len; ++i)
{
if(rev[i]<i)
{
std::swap(ar[rev[i]],ar[i]);
}
}
for(register int i=2; i<=len; i<<=1)
{
complex wn(cos(2*pi/i),sin(2*pi*op/i));
for(register int j=0; j<len; j+=i)
{
complex w(1,0);
for(register int k=0; k<(i>>1); ++k)
{
complex x=ar[j+k],y=w*ar[j+k+(i>>1)];
ar[j+k]=x+y;
ar[j+k+(i>>1)]=x-y;
w=w*wn;
}
}
}
if(op==-1)
{
for(register int i=0; i<len; ++i)
{
ar[i].r/=len;
}
}
return 0;
}
int main()
{
scanf("%d%d",&n,&m);
for(register int i=0; i<=n; ++i)
{
scanf("%lf",&a[i].r);
}
for(register int i=0; i<=m; ++i)
{
scanf("%lf",&b[i].r);
}
n=n+m;
int l=0;
m=1;
while(m<=n)
{
++l;
m<<=1;
}
for(register int i=0; i<m; ++i)
{
rev[i]=(rev[i>>1]>>1)|((i&1)<<(l-1));
}
fft(a,m,1);
fft(b,m,1);
for(register int i=0; i<m; ++i)
{
c[i]=a[i]*b[i];
}
fft(c,m,-1);
for(register int i=0; i<n; ++i)
{
printf("%d ",(int)(c[i].r+0.5));
}
printf("%d\n",(int)(c[n].r+0.5));
return 0;
}