多项式乘法入门
By SemiWaker
这是一篇蒟蒻对FFT、DFT、CZT、NTT的弱鸡理解
多项式
上面的这个形式叫做多项式。
系数: a0..n−1
项: aixi
界:n
为了方便我们系数序列就可以表示多项式。
线性卷积
简单来说,就是把两个多项式直接乘起来。
第i项的构成如下:
任取两个项 Ajxj 和 Bkxk ,如果 j+k=i ,那么得到 AjBkxi 。
将所有 j+k=i 的项系数两两相乘加起来即可。
循环卷积
比较难理解。
其实就是把多项式的-1项设为n-1项,-2项设为n-2项……
然后,线性卷积为了防止出现-1项,设定了j<=i,现在我们把它去掉。
线性卷积和循环卷积关系
我们用比较形象地方式说明。
举个例子
即 (1,2,3,4)×(5,6,7,8)
线性卷积:
循环卷积
如果把 (5,6,7,8) 复制展开
然后我们观察,设线性卷积的结果为 C0...6 ,循环卷积的结果为 D0..3
则有:
D0=C0+C4
D1=C1+C5
D2=C2+C6
D3=C3
也就是说,把线性卷积重叠在一起,就得到了循环卷积。
多项式点值表示
我们把N个数 x0..n−1 带入x,可以得到多项式的N个值 A(x0..n−1) 。
将每一个数和对应的值当成一个点 (xi,A(xi)) ,这N个点叫做多项式的点值表示。
为什么点值表示可以代表多项式呢?
因为我们可以反过来用点值表示求出多项式。
待定系数+解方程就可以了。
多项式插值
就是把点值表示转换成系数表示的一个过程。
DFT
离散傅里叶变换
考虑怎样快速求多项式卷积。
按照定义去写 O(n2) ,显然不优。
我们可以考虑点值表示。
两个多项式如果取值用的是同样的数 x0..n−1 ,那么得到的值直接乘起来就可以得到卷积之后的多项式的点值表示。
但是如果按照定义一个一个数带入求多项式的值,还是 O(n2) 的,没有优化。
我们考虑带入特殊的数去求点值表示。
DFT就是这样一个过程:将一个多项式转化为用n次单位根表示的点值表示。
FFT是实现DFT的算法。
单位根
简单来说 xn=1 的复数解。
n次单位根有n个复数解,设为 ωkn ,其中k=0…n-1。
ωkn=e2kπIn
用欧拉公式展开:
exI=cos(x)+sin(x)I
得到
(ωkn)n=cos(2kπ)+sin(2kπ)I=1
画在复数平面上,刚好是把单位元n等分的n个点。
有一些有趣的性质
ωkn=ωk(modn)n
由三角函数周期性可证。
ωn2n=−1
显然。
ωnn=1
显然。
ωkn=ωk2n2
显然。
(ωkn)2=(ωk+n2n)2
把定义带入可证。
FFT的实现
快速傅里叶变换 Cooley-Tukey算法
考虑把 ω0...n−1n 一起带入求值。
则
可以分治为两部分
两半的长度都为 n2
而 (ωkn)2j=(ωk+n/2n)2j=ωkjn2
所以而 ωkjn2 只有 n2 个取值,而且每个出现两次。
这样就通过分治减小了规模。
现在变成了对两个多项式 (a0,a2,a4...) 和 (a1,a3,a5...) 求带入 ω0..n2−1n2时的值 。
分治完了之后,我们要求回原来的多项式。
设分治的结果为 B0..n2−1 和 C0..n2−1 。
则 Ak=Bk+ωknCk
但是注意此时k只有 n2
所以 Ak+n2=Bk+ωk+n2nCk
又 ωk+n2n=−ωkn
所以 Ak+n2=Bk−ωknCk
用一个简单的图来记:
设当前分治长度为L,则
Ak=Ak+ωknAk+L2
Ak+L2=Ak−ωknAk+L2
注意变量自我迭代时,要开临时变量
这两个位置刚好交错计算,形状类似蝴蝶,所以叫做蝴蝶变换。
边界:
n=1时, ω11=1 ,所以直接把 ak 放进去就好了。
程序怎么实现呢?
直接分治是可以的,但是有更好的方法。
我们考虑分治时的分类方法:
每一层按照奇偶数。
如果我们保持编号不变,那么就变成:第i行按照从低到高第i位分。
由于是从低到高,所以最后的排列为:每一个数的位置,为编号二进制倒序后位置。
举例:有8个数,编号0~7。
分治过程:
000 001 010 011 100 101 110 111
000 010 100 110|001 011 101 111
000 100|010 110|001 101|011 111
000|100|010|110|001|101|011|111
把最后一行的二进制倒过来:
000 001 010 011 100 101 110 111
刚好是从0~7。
设Reverse(x)为x二进制倒序后的数
一开始我们可以将ai放到Reverse(i)的位置。
然后设当前层分治长度为L。
每L个一起处理,进行蝴蝶变换即可。
进一步优化,注意蝴蝶变换中 ωkn 的k取值为0~L/2
为了尽量减少对 ωkn 的计算次数,我们可以先枚举0~L/2,计算 ωkn ,然后枚举每一个分治块的相应位置进行蝴蝶变换。
IDFT
逆离散傅里叶变换
求出点值表示,再相乘之后,我们要进行插值操作。
通过对DFT求逆矩阵,我们直接给出以下结论:
将点值表示 (A0,A1...An) 转换为系数表示 (a0,a1...an) ,只需要求出:
换句话说,我们要把DFT中的每一个 ωkn 换成 ω−kn ,然后除以n即可。
代码
void FFT(Complex *A,int n,int sgn)
{for (int i=1;i<n-1;++i){int j=0;for (int t=1,k=i;t<n;t<<=1,j=((j<<1)|(k&1)),k>>=1);if (j>i) swap(A[i],A[j]);}for (int L=2;L<=n;L<<=1){int L1=L>>1;for (int i=0;i<L1;++i){Complex w=Complex(cos(sgn*PI*i/L1),sin(sgn*PI*i/L1));for (int j=i;j<n;j+=L){int k=j+L1;Complex u=A[j],v=w*A[k];A[j]=u+v;A[k]=u-v;}}}if (sgn==-1) for (int i=0;i<n;++i) A[i]=A[i]/Complex(n,0.0);
}
至于是用exp还是直接两个三角函数,一个exp应该是快些,但是只能用STL的complex。如果要手写complex就只能两个三角函数。手写会快些。
注意:因为要对2分治,所以要强行把项数补到 2n ,后面的项系数为0。如果是两个长度为n的多项式相乘,那么至少要开4n的空间。
循环卷积和线性卷积对于DFT的区别
线性卷积卷得到项数是2n-1,而循环卷积得到的项数是n。
那么我们在求点值表示的时候,线性卷积就带入2n-1个点,循环卷积就带入n个点,再相应的IDFT出来的结果就是所求的结果。
CZT
Z-变换
在用FFT的过程中,我们要强行把多项式补到 2n 项。那么,FFT出来的点值表示,实际上和原来的点值表示已经不一样了。(因为n变了)
在求线性卷积的时候,补足 2n 项没有什么问题。但是如果要求循环卷积,补足 2n 位就会产生很大的问题。(定义决定的)
求循环卷积时,我们要保证项数不变,DFT出来的点值表示才能表示原来的循环卷积。
怎样保证项数不变呢?
考虑原来的公式
即
考虑我们把jk变换一下。
其实是更加复杂了
jk=j2+k2−(j−k)22
然后带入
稍微变化下
设 Bj=ajeπj2nI
Cj=e−πj2nI
那么
右边是一个线性卷积,所以我们可以用一次FFT来完成一次DFT
注意,k-j会小于0!
怎么解决这个问题呢?
我们把C右移n位。那么此时
Cj=e−π(j−n)2nI
由于平方的存在,所以不会有问题。
此时C的长度变为2n。
卷积完之后总长3n。
我们需要考虑哪些部分是有用的。
哪些部分可以保证k-j>=0呢?
即 An..2n−1 。
那么,最后的答案应该是: Ak=eπk2nIAk+n
注意IDFT时同样要除以n。
这样我们就完成了项数不变的DFT。实际上,应该叫做CZT。
NTT
数论变换
考虑这个问题:乘起来后系数要模怎么办?
由于单位根是个复数,不能够模。
所以我们要找一个可以代替的东西。
考虑我们用了哪些性质,简单来说, ωkn 是一个n阶的循环群。
设我们现在模的质数为 P=2t⋅Q+1
考虑模P意义下的原根g。
由于 g0...P−1 两两不同, gP=g , g2t−1Q=−1 ,所以是一个 2t⋅Q 阶的循环群。
进一步,我们可以得到 (gQ)0..P−1 是 2t 阶的循环群。
让 n=2t 即可。
然后我们对应着 ωkn 的定义,定义 Gkn=g2tkQn 。
显然 Gkn 有n个,而且满足 Gkn=Gk2n2 ,也满足 Gk+n2n=−Gkn
预处理出 gkQ 代替单位根即可。
如果我们要模m,但是m不是 2tQ+1 形式的质数怎么办?
暴力法:在模m的情况下,每一个系数最大为 m-1,两个乘起来最多 (m−1)2 ,n个加起来最多 n(m−1)2 。
我们只要凑一个超过 n(m−1)2 的模数就好了。这样在NTT的过程中,就不会让系数发生变化。然后再去一项一项模。
怎么凑呢?我们用几个小的 2tQ+1 形式的质数去做NTT,然后用中国剩余定理合并起来就好了。