本来很久以前就学了,但是我一直没有写博客,直到最近要用的时候才发现忘了。于是就有了这篇博客.
我们知道,如果直接暴力将两个多项式比如
A(x)=a0+a1x+a2x2+...+anxnA(x)=a_0+a_1x+a_2x^2+...+a_nx^nA(x)=a0+a1x+a2x2+...+anxn
B(x)=b0+b1x+b2x2+...+bnxnB(x)=b_0+b_1x+b_2x^2+...+b_nx^nB(x)=b0+b1x+b2x2+...+bnxn
相乘将会花费O(n2)O(n^2)O(n2)的时间,有没有更快的方法呢?
我们注意到,耗时的原因是每一项都要花费O(n)O(n)O(n)的时间来乘,合起来乘的难度较大,说不定换一种表示方法就可以不一样了。
我们要确定一个有nnn次项的方程,不仅可以用上面的方法,我们也可以用nnn个点(向量)来表示,而且这样一来方程就可以唯一确定了。
很明显如果是用nnn个点来表示A(x)A(x)A(x)的话,A(x),B(x)A(x),B(x)A(x),B(x)可以表示为:
(x1,A(x1)),(x2,A(x2)),...,(xn,A(xn))(x_1,A(x_1)),(x_2,A(x_2)),...,(x_n,A(x_n))(x1,A(x1)),(x2,A(x2)),...,(xn,A(xn))
(x1,B(x1)),(x2,B(x2)),...,(xn,B(xn))(x_1,B(x_1)),(x_2,B(x_2)),...,(x_n,B(x_n))(x1,B(x1)),(x2,B(x2)),...,(xn,B(xn))
由于我们要求的C(x)=A(x)∗B(x)C(x)=A(x)*B(x)C(x)=A(x)∗B(x)因此C(x)C(x)C(x)可以表示为:
(x1,A(x1)∗B(x1)),(x2,A(x2)∗B(x2)),...,(xn,A(xn)∗B(xn))(x_1,A(x_1)*B(x_1)),(x_2,A(x_2)*B(x_2)),...,(x_n,A(x_n)*B(x_n))(x1,A(x1)∗B(x1)),(x2,A(x2)∗B(x2)),...,(xn,A(xn)∗B(xn))
在这种表示方法下我们只需要O(n)O(n)O(n)时间就可以将两个方程相乘,但是我们已有的是A(x)A(x)A(x)和B(x)B(x)B(x)的系数表示法的式子。
但如果暴力转成点值表示法,需要O(n2)O(n^2)O(n2)的时间,时间复杂度并没有丝毫的减少,而且常数反而变大了。我们需要找到更快的能在系数表示法和点值表示法之间转换的方法。
很明显,我们在带入n个值求点是在做操作重复的运算,而且由于这n个值的取值我们现在还没有确定,这里应该可以通过巧妙的取值来达到减少运算的目的,要是我们选的数的次方之间可以相互轮回,那么应该可以简化运算。一维的实数相乘的话只能在数轴上面走,二维的虚数就比较方便了,因为(cos(a)+isin(a))n=(cos(na)+isin(na))(cos(a)+isin(a))^n=(cos(na)+isin(na))(cos(a)+isin(a))n=(cos(na)+isin(na))我们如果用这样的数,那么就它的n次方就是相当于在平面上转圈,如下图:
因此如上图(w81)n=w8n(w_8^1)^n=w_8^n(w81)n=w8n.
我们可以发现它有这些性质:
w2n2m=wnmw_{2n}^{2m}=w_n^mw2n2m=wnm
wnm+n2=−wnmw_n^{m+\frac{n}{2}}=-w_n^mwnm+2n=−wnm
因此各次方的关系就有了,我们可以把最后的式子排在一起看看
比如现在只有4个:
a0+a1∗w40+a2∗(w40)2+a3∗(w40)3a_0+a_1*w_4^0+a_2*(w_4^0)^2+a_3*(w_4^0)^3a0+a1∗w40+a2∗(w40)2+a3∗(w40)3
a0+a1∗w41+a2∗(w41)2+a3∗(w41)3a_0+a_1*w_4^1+a_2*(w_4^1)^2+a_3*(w_4^1)^3a0+a1∗w41+a2∗(w41)2+a3∗(w41)3
a0+a1∗w42+a2∗(w42)2+a3∗(w42)3a_0+a_1*w_4^2+a_2*(w_4^2)^2+a_3*(w_4^2)^3a0+a1∗w42+a2∗(w42)2+a3∗(w42)3
a0+a1∗w43+a2∗(w43)2+a3∗(w43)3a_0+a_1*w_4^3+a_2*(w_4^3)^2+a_3*(w_4^3)^3a0+a1∗w43+a2∗(w43)2+a3∗(w43)3
即:
a0+a1∗w40+a2∗w40+a3∗w40a_0+a_1*w_4^0+a_2*w_4^0+a_3*w_4^0a0+a1∗w40+a2∗w40+a3∗w40
a0+a1∗w41+a2∗w42+a3∗w43a_0+a_1*w_4^1+a_2*w_4^2+a_3*w_4^3a0+a1∗w41+a2∗w42+a3∗w43
a0+a1∗w42+a2∗w44+a3∗w46a_0+a_1*w_4^2+a_2*w_4^4+a_3*w_4^6a0+a1∗w42+a2∗w44+a3∗w46
a0+a1∗w43+a2∗w46+a3∗w49a_0+a_1*w_4^3+a_2*w_4^6+a_3*w_4^9a0+a1∗w43+a2∗w46+a3∗w49
由于wnm+n=wnmw_n^{m+n}=w_n^mwnm+n=wnm,我们可以把次数模n加强联系即:
a0+a1∗w40+a2∗w40+a3∗w40a_0+a_1*w_4^0+a_2*w_4^0+a_3*w_4^0a0+a1∗w40+a2∗w40+a3∗w40
a0+a1∗w41+a2∗w42+a3∗w43a_0+a_1*w_4^1+a_2*w_4^2+a_3*w_4^3a0+a1∗w41+a2∗w42+a3∗w43
a0+a1∗w42+a2∗w40+a3∗w42a_0+a_1*w_4^2+a_2*w_4^0+a_3*w_4^2a0+a1∗w42+a2∗w40+a3∗w42
a0+a1∗w43+a2∗w42+a3∗w41a_0+a_1*w_4^3+a_2*w_4^2+a_3*w_4^1a0+a1∗w43+a2∗w42+a3∗w41
由于w42=−w40,w43=−w41w_4^2=-w_4^0,w_4^3=-w_4^1w42=−w40,w43=−w41我们可以进一步把次数降低加强联系即:
a0+a1∗w40+a2∗w40+a3∗w40a_0+a_1*w_4^0+a_2*w_4^0+a_3*w_4^0a0+a1∗w40+a2∗w40+a3∗w40
a0+a1∗w41−a2∗w40−a3∗w41a_0+a_1*w_4^1-a_2*w_4^0-a_3*w_4^1a0+a1∗w41−a2∗w40−a3∗w41
a0−a1∗w40+a2∗w40−a3∗w40a_0-a_1*w_4^0+a_2*w_4^0-a_3*w_4^0a0−a1∗w40+a2∗w40−a3∗w40
a0−a1∗w41−a2∗w40+a3∗w41a_0-a_1*w_4^1-a_2*w_4^0+a_3*w_4^1a0−a1∗w41−a2∗w40+a3∗w41
可以发现其中的(0,2)(1,3)式的次数是完全一样的,系数的符号有一半一样,另一半互为相反数,其实这很好证明第mmm式和第m+n2m+\frac {n}{2}m+2n式的关系。
fm=a0+a1∗wnm+a2∗wn2m+...+an−1∗wn(n−1)∗mf_m=a_0+a_1*w_n^m+a_2*w_n^{2m}+...+a_{n-1}*w_n^{(n-1)*m}fm=a0+a1∗wnm+a2∗wn2m+...+an−1∗wn(n−1)∗m
fm+n2=a0+a1∗wnm+n2+a2∗wn2m+...+an−1∗wn(n−1)∗(m+n2)f_{m+\frac{n}{2}}=a_0+a_1*w_n^{m+\frac{n}{2}}+a_2*w_n^{2m}+...+a_{n-1}*w_n^{(n-1)*(m+\frac{n}{2})}fm+2n=a0+a1∗wnm+2n+a2∗wn2m+...+an−1∗wn(n−1)∗(m+2n)
即
fm+n2=a0−a1∗wnm+a2∗wn2m+...−an−1∗wn(n−1)∗mf_{m+\frac{n}{2}}=a_0-a_1*w_n^m+a_2*w_n^{2m}+...-a_{n-1}*w_n^{(n-1)*m}fm+2n=a0−a1∗wnm+a2∗wn2m+...−an−1∗wn(n−1)∗m(假设n是偶数)
那么我们可以把这之间相同的部分拿出来
令
A0=a0+a2∗wn2m+a4∗wn4m+...+an−2∗wn(n−2)∗mA_0=a_0+a_2*w_n^{2m}+a_4*w_n^{4m}+...+a_{n-2}*w_n^{(n-2)*m}A0=a0+a2∗wn2m+a4∗wn4m+...+an−2∗wn(n−2)∗m
A1=a1+a3∗wn2m+a5∗wn4m+...+an−1∗wn(n−2)∗mA_1=a_1+a_3*w_n^{2m}+a_5*w_n^{4m}+...+a_{n-1}*w_n^{(n-2)*m}A1=a1+a3∗wn2m+a5∗wn4m+...+an−1∗wn(n−2)∗m
则
A0=a0+a2∗wn2m+a4∗wn22m+...+an−2∗wn2(n−2)∗m/2A_0=a_0+a_2*w_{\frac{n}{2}}^{m}+a_4*w_{\frac{n}{2}}^{2m}+...+a_{n-2}*w_{\frac{n}{2}}^{(n-2)*m/2}A0=a0+a2∗w2nm+a4∗w2n2m+...+an−2∗w2n(n−2)∗m/2
A1=a1+a3∗wn2m+a5∗wn22m+...+an−1∗wn2(n−2)∗m/2A_1=a_1+a_3*w_{\frac{n}{2}}^{m}+a_5*w_{\frac{n}{2}}^{2m}+...+a_{n-1}*w_{\frac{n}{2}}^{(n-2)*m/2}A1=a1+a3∗w2nm+a5∗w2n2m+...+an−1∗w2n(n−2)∗m/2
那么很明显
fm=A0+A1∗wnmf_m=A_0+A_1*w_n^mfm=A0+A1∗wnm
fm+n2=A0+A1∗wnm+n2=A0−A1∗wnmf_{m+\frac{n}{2}}=A_0+A_1*w_n^{m+\frac{n}{2}}=A_0-A_1*w_n^mfm+2n=A0+A1∗wnm+2n=A0−A1∗wnm
因此我们只要把fmf_mfm和fm+n2f_{m+\frac{n}{2}}fm+2n求出来就可以一下求出来A0A_0A0和A1A_1A1 而且fff的数据范围只有AAA的一半。也就是说只要log2(n)log_2(n)log2(n)层运算就可以求出解了,说以总的时间复杂度只有nlog2(n)nlog_2(n)nlog2(n).
a0+a1w40+a2w40+a3w40a_0+a_1w_4^0+a_2w_4^0+a_3w_4^0a0+a1w40+a2w40+a3w40 | a0+a1w41+a2w42+a3w43a_0+a_1w_4^1+a_2w_4^2+a_3w_4^3a0+a1w41+a2w42+a3w43 | a0+a1w42+a2w44+a3w46a_0+a_1w_4^2+a_2w_4^4+a_3w_4^6a0+a1w42+a2w44+a3w46 | a0+a1w43+a2w46+a3w49a_0+a_1w_4^3+a_2w_4^6+a_3w_4^9a0+a1w43+a2w46+a3w49 |
---|---|---|---|
a0+a2w20a_0+a_2w_2^0a0+a2w20 | a0+a2w21a_0+a_2w_2^1a0+a2w21 | a1+a3w20a_1+a_3w_2^0a1+a3w20 | a1+a3w21a_1+a_3w_2^1a1+a3w21 |
a0a_0a0 | a2a_2a2 | a1a_1a1 | a3a_3a3 |
为了方便处理我们将对应位置的f对着对应位置的A,表示成数字就是:
0 1 2 3 4 5 6 7
0 2 4 6 1 3 5 7
0 4 2 6 1 5 3 7
不难发现之后得到的位置的二进制就是按反过来之后排序,因为我们第一次把&1=0的移动到了前面,而吧&1=1的移到了后面,所以之后第二次把&2=0的移动到了前面,而吧&2=1的移到了后面,之后依次这样就可以了。这就是蝴蝶变换。
因此我们在实际操作的时候就反着来,从下至上依次求出每一层,这里不递归的原因是因为递归太慢了。
说以我们现在可以用O(nlog2(n))O(nlog_2(n))O(nlog2(n))的时间复杂度正着把系数表示法变成点值表示法,那么怎么倒着装呢?
很明显为了求出上面的值,我们采取的方法是倒着来,既然
fm=A0+A1∗wnmf_m=A_0+A_1*w_n^mfm=A0+A1∗wnm
fm+n2=A0−A1∗wnmf_{m+\frac{n}{2}}=A_0-A_1*w_n^mfm+2n=A0−A1∗wnm
那么
2∗A0=fm+fm+n22*A_0=f_m+f_{m+\frac{n}{2}}2∗A0=fm+fm+2n
2∗A1=(fm−fm+n2)∗wn−m2*A_1=(f_m-f_{m+\frac{n}{2}})*w_n^{-m}2∗A1=(fm−fm+2n)∗wn−m
我们先暴力把最终得到的式子求出来再统一除n(n=2某次方)n(n=2^{某次方})n(n=2某次方),得到:
4∗a04*a_04∗a0 | 4∗a14*a_14∗a1 | 4∗a24*a_24∗a2 | 4∗a34*a_34∗a3 |
---|---|---|---|
2∗(a0+a2w40)2*(a_0+a_2w_4^0)2∗(a0+a2w40) | 2∗(a1+a3w40)2*(a_1+a_3w_4^0)2∗(a1+a3w40) | 2∗(a0+a2w42)2*(a_0+a_2w_4^2)2∗(a0+a2w42) | 2∗(a1+a3w42)2*(a_1+a_3w_4^2)2∗(a1+a3w42) |
a0+a1w40+a2w40+a3w40a_0+a_1w_4^0+a_2w_4^0+a_3w_4^0a0+a1w40+a2w40+a3w40 | a0+a1w42+a2w44+a3w46a_0+a_1w_4^2+a_2w_4^4+a_3w_4^6a0+a1w42+a2w44+a3w46 | a0+a1w41+a2w42+a3w43a_0+a_1w_4^1+a_2w_4^2+a_3w_4^3a0+a1w41+a2w42+a3w43 | a0+a1w43+a2w46+a3w49a_0+a_1w_4^3+a_2w_4^6+a_3w_4^9a0+a1w43+a2w46+a3w49 |
比如这里n就是4,FFT只能解决n是二的整次幂的情况,如果开始的n不是2的整次幂那么加一堆系数为0的项就可以了,反正加的项又不到一倍,时间也多不了多少。
这样式子就可以反过来了
不过貌似再写逆变换有点麻烦,大佬们搞了个逆矩阵至今没搞懂,留坑了。
这里是代码(洛谷3803)
#include<cstdio>
#include<complex>
#include<cmath>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=4000005;
typedef complex<double> cpx;
const long double PI=acos(0.0)*2.0;
int N,M;
void FFT(cpx a[],int n,double op)
{
for(int i=1,j=0;i<n-1;i++)
{
for(int s=n;j^=(s>>=1),(~j)&s;);
if(i<j)
swap(a[i],a[j]);
}
for(int len=2,mid=1;len<=n;len<<=1,mid<<=1)
{
cpx wm(cos(2.0*PI/len),op*sin(2.0*PI/len));
for(int now=0;now<n;now+=len)
{
cpx w(1,0);
for(int k=now;k<now+mid;k++,w=w*wm)
{
cpx l=a[k],r=w*a[k+mid];
a[k]=l+r,a[k+mid]=l-r;
}
}
}
if(op==-1)
{
for(int i=0;i<n;i++)
a[i]/=n;
}
}
cpx A[MAXN],B[MAXN];
void mul(int a[],int n,int b[],int m,int c[])
{
//memset(A,0,sizeof(A));
//memset(B,0,sizeof(B));
for(int i=0;i<n;i++)
A[i]=cpx(a[i],0);
for(int i=0;i<m;i++)
B[i]=cpx(b[i],0);
int l=n+m-1,len=1;
while(len<l)
len<<=1;
FFT(A,len,1);
FFT(B,len,1);
for(int i=0;i<len;i++)
A[i]=A[i]*B[i];
FFT(A,len,-1);
for(int i=0;i<len;i++)
c[i]=int(A[i].real()+0.5);
}
int F[MAXN],G[MAXN],an[MAXN];
int main()
{
scanf("%d %d",&N,&M);
N++,M++;
for(int i=0;i<N;i++)
scanf("%d",&F[i]);
for(int i=0;i<M;i++)
scanf("%d",&G[i]);
mul(F,N,G,M,an);
for(int i=0;i<N+M-1;i++)
printf("%d%c",an[i],i==N+M-2?'\n':' ');
}