欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[拉格朗日反演][FFT][NTT][多项式大全]详解

程序员文章站 2022-05-22 19:26:48
...

1、目录

2、多项式的两种表示法

1.系数表示法

我们最常用的多项式表示法就是系数表示法,一个次数界为n的多项式S(x)可以用一个向量s=(s0,s1,s2,,sn1)系数表示如下:

S(x)=k=0n1skxk

系数表示法很适合做加法,可以在O(n)的时间复杂度内完成,表达式为:

S(x)=A(x)+B(x)=k=0n1(ak+bk)xk

当中

sk=ak+bk

但是,系数表示法不适合做乘法,时间复杂度为O(n2),表达式为:

S(x)=A(x)B(x)=k=0n1(j=0n1ajbkj)xk

当中

sk=j=0kajbkj

这就是卷积的一般形式,记s=ab,我们要想办法加速这个过程。

2.点值表示法

顾名思义,点值就是多项式在一个点处的值。多项式A(x)点值表达是一个集合:

{(x0,y0),(x1,y1),(x2,y2),,(xn1,yn1)}

使得对于k=0,1,2,,n1xk两两不相同且yk=A(xk)

n个点可以确定唯一一个n次多项式。

点值表达有很多优良的性质,加法和乘法都可以在O(n)的时间复杂度内完成。

现有A(x)的点值表达

{(x0,y0),(x1,y1),(x2,y2),,(xn1,yn1)}
B(x)的点值表达
{(x0,y0),(x1,y1),(x2,y2),,(xn1,yn1)}

C(x)=A(x)+B(x)的点值表达为:

{(x0,y0+y0),(x1,y1+y1),(x2,y2+y2),,(xn1,yn1+yn1)}

C(x)=A(x)B(x)的点值表达为:

{(x0,y0y0),(x1,y1y1),(x2,y2y2),,(xn1,yn1yn1)}

可见,点值表示可以帮助我们更快地进行卷积,可是如何在系数表示法和点值表示法之间相互转化呢?

3、复数

x为实数时,无法很好地对转换方法进行优化。为了优化计算xn所浪费的时间,我们需要x有循环的性质。但点值表示法需要n个两两不同的值,而在实数域中只有11,因此,我们需要复数的帮助。

1.复数、复平面的定义

我们把形如a+bi的数称为复数z,其中a实部(Real),记为zb虚部(Imaginary),记为z

每一点都对应唯一复数的平面叫复平面,相当于一个把z作为横坐标,把z作为纵坐标的笛卡尔坐标系。如图:
[拉格朗日反演][FFT][NTT][多项式大全]详解

模长:复平面上原点到复数z的距离,记为|z|。根据勾股定理,|z|=|a+bi|=a2+b2

辐角:复平面上x轴与复数z所对应向量之间的夹角,在(π2,π2)之间的记为辐角主值argz

2.欧拉公式

大名鼎鼎的欧拉公式:

eit=cost+isint

根据三角函数在单位圆上的几何意义,公式是容易理解的。

几何意义:
[拉格朗日反演][FFT][NTT][多项式大全]详解
当中θ为角度,t为弧长。

则根据欧拉公式,可将一个复数表示为一个二元组(a,θ),即模长和辐角(相当于复平面上极坐标系的表示方法)。值为:a(cosθ+isinθ)

特殊情况:欧拉恒等式eiπ+1=0

3.复数的运算

(1)复数加法

运算规则:实部、虚部分别相加

(a+bi)+(c+di)=a+c+bi+di=(a+c)+(b+d)i

几何意义:如图
[拉格朗日反演][FFT][NTT][多项式大全]详解

结果相当于两个向量所构成的平行四边形的对角线。如果把一个复数所对应的向量视为一个移动的变换,那么向量加法就是连续运用这两个变换相当于的新变换。

(2)复数乘法

运算规则:展开

(a+bi)(c+di)=ac+adi+bci+bdi2=(acbd)+(ad+bc)i

几何意义:如图
[拉格朗日反演][FFT][NTT][多项式大全]详解
如图,arga+argb=arga×b,|a|×|b|=|a×b|

总结就是:模长相乘,辐角相加

因此,如果模长为1,那么它的n次方一定还在单位圆上。

证明:

根据欧拉公式,已知x=(a1,θ1)=a1(cosθ1+isinθ1),y=(a2,θ2)=a2(cosθ2+isinθ2)

x×y=a1a2(cosθ1+isinθ1)(cosθ2+isinθ2)=a1a2[(cosθ1cosθ2sinθ1sinθ2)+i(cosθ1sinθ2+sinθ1cosθ2)]=a1a2[(cos(θ1+θ2)+cos(θ1θ2)2+cos(θ1+θ2)cos(θ1θ2)2)(积化和差公式)+i(sin(θ1+θ2)sin(θ1θ2)2+sin(θ1+θ2)+sin(θ1θ2)2)]=a1a2[cos(θ1+θ2)+isin(θ1+θ2)]

|x×y|=|x|×|y|,arg(x×y)=argx+argy

证毕。

4.单位复数根

(1)基本性质

单位复数根是方程ωn=1的解,第k个解记为ωnk(这里的k事实上是乘方的含义)

n=16的解在复平面上的位置如下:
[拉格朗日反演][FFT][NTT][多项式大全]详解
可以看到,n个解把单位圆分成了n等弧,交点即为根。而且,ωnk实际上是ωnn次方,模长仍为1,辐角翻倍!

为什么呢?

|xn|=|x|n,argxn=nargx

|ω|n=|ωn|,argωn=nargω

|ω|n=1(|ω|R+),argω=360n

|ω|=1,argω=360n

这就很明显了。

所以,ωnk事实上表示的是ωnk次幂。为什么选择单位复数根呢?因为它有循环的优良性质,即ωnn=1。由于其他的都可以由ωn1得到,因此称为主n次单位根,又记为ωn

根据单位复数根的平分圆的意义和欧拉公式,ωnk=e2πikn=cos2πkn+isin2πkn

(2)计算引理

显然,由于单位复数根循环(ωnzn=e2πiz=[(eπi)2]z=1z=1),有变换恒等式

ωnk=ωnk+wn(wZ)

每一份再分成k份,编号也变成k倍,位置自然不变(ωdndk=e2πidkdn=e2πikn=ωnk),所以有消去引理

ωdndk=ωnk

由于过了n/2就会绕过半圈(ωnn/2=eπinn=eπi=1),所以有折半引理

ωnk=ωnk±n/2

对单位复数根求和,根据几何级数(等差数列求和公式),可得(k=0n1(ωnk)j=(ωnn)j1ωn11=0),即有求和引理(要注意公式的使用条件):

k=0n1(ωnk)j=0,ωnk1

4、DFT&FFT

1.DFT

DFT就是求多项式A(x)在点(ωn0,ωn1,ωn2,,ωnn1)处取值的过程。即:

yk=A(ωnk)=j=0n1ajωnkj

结果y=(y0,y1,y2,,yn1)就是a离散傅里叶变换(DFT),记为y=DFTn(a)

2.FFT

(1)递归

DFT的O(n2)算法太慢了,FFT使用分治策略优化速度到O(nlogn)

考虑奇偶分治。

现在,我们假设n=2t,设原系数a=(a0,a1,a2,,an1),分治为偶数部分a1=(a0,a2,a4,,an2),奇数部分a2=(a1,a3,a5,,an1),已经递归求出y1=DFTn/2(a1)y2=DFTn/2(a2),现在我们要合并y1,y2,得到y=DFTn(a)(蝴蝶操作)。

对于n=1的边界情况,结果是显然的:因为k=0,故ω10=1,即结果等于原系数。

对于n>1,现在我们枚举k[1,n]要合并出yk

yk=A(ωnk)=j=0n1ajωnkj=j=0n/21a2jωn2kj+j=0n/21a2j+1ωn2kj+k=j=0n/21a2jωn2kj+ωnkj=0n/21a2j+1ωn2kj(消去引理)=j=0n/21a1jωn/2kj+ωnkj=0n/21a2jωn/2kj

  • 对于k<n/2

    yk=y1k+ωnky2k

  • 对于kn/2

    (变换恒等式)yk=j=0n/21a1jωn/2(kn/2)j+ωnkj=0n/21a2jωn/2(kn/2)j=y1kn/2+ωnky2kn/2(折半引理)=y1kn/2ωnkn/2y2kn/2

我们用k+n/2替代k,就得到yk+n/2=y1kωnky2k

结合在一起就得到{yk=y1k+ωnky2kyk+n/2=y1kωnky2k
这样我们就可以把两个n/2长的序列合并为一个n长的序列了。

以下图的递归序,就可以在O(nlogn)的时间复杂度内完成求解了。
[拉格朗日反演][FFT][NTT][多项式大全]详解

(2)迭代

递归方法消耗内存、时间过大,无法承受。我们每次把下标分为奇数部分和偶数部分,是否有办法直接求出最后的递归运算顺序,以避免递归呢?

这样想:
第一次奇偶划分,我们按照二进制的倒数第一位排序
第二次奇偶划分,我们按照二进制的倒数第二位排序
n次奇偶划分,我们按照二进制的倒数第n位排序
依此类推。

因此,结果顺序就是原序列按照二进制位翻转的大小排序的结果。只要依次交换ak,arev(k),求出序列,就可以用迭代方法相邻归并实现快速傅里叶变换。

或者,我们也可以用更加代数的方法来发现这个结论。
已知现在位置为k=(b1b2b3bn)2,按照奇偶重排:

  • bn=0,则位置变为k2=(0b1b2bn1)2=(bnb1b2bn1)2,即为把最后一位提到第一位。
  • bn=1,则位置变为2n11+k+12=2n+k12=(1b1b2bn10)22=(bnb1b2bn1)2,同样是把最后一位提到第一位。

则反复n次之后,就相当于二进制反转了。

n=8时,求出二进制:

0 1 2 3 4 5 6 7
0002 0012 0102 0112 1002 1012 1102 1112

翻转:

0 1 2 3 4 5 6 7
0002 1002 0102 1102 0012 1012 0112 1112

按翻转后的值排序:

0 4 2 6 1 5 3 7
0002 0012 0102 0112 1002 1012 1102 1112

这样就可以把奇偶合并转化为左右归并,迭代实现了。

5、IDFT&IFFT

何把点值表达变回系数表达呢?如果把求值写成矩阵形式,就是:

[ωn0ωn0ωn0ωn0ωn0ωn0ωn1ωn2ωn3ωnn1ωn0ωn2ωn4ωn6ωn2(n1)ωn0ωn3ωn6ωn9ωn3(n1)ωn0ωnn1ωn2(n1)ωn3(n1)ωn(n1)2][a0a1a2a3an1]=[y0y1y2y3yn1]

如果我们要把y变成a,就需要求出第一个矩阵的逆,即:

[a0a1a2a3an1]=[ωn0ωn0ωn0ωn0ωn0ωn0ωn1ωn2ωn3ωnn1ωn0ωn2ωn4ωn6ωn2(n1)ωn0ωn3ωn6ωn9ωn3(n1)ωn0ωnn1ωn2(n1)ωn3(n1)ωn(n1)2]1[y0y1y2y3yn1]

这个范德蒙德矩阵极为特殊,它的逆矩阵是:

[a0a1a2a3an1]=1n[ωn0ωn0ωn0ωn0ωn0ωn0ωn1ωn2ωn3ωn(n1)ωn0ωn2ωn4ωn6ωn2(n1)ωn0ωn3ωn6ωn9ωn3(n1)ωn0ωn(n1)ωn2(n1)ωn3(n1)ωn(n1)2][y0y1y2y3yn1]

只是把每项取倒数,并将结果除以n即可。

证明

记原矩阵为Ann,我们给出的矩阵为Bnn

Aij=ωnij,Bij=1nωnij(0i,jn1)

ABij=k=0n1AikBkj=1nk=0n1ωnikωnkj=1nk=0n1ωnikkj=1nk=0n1(ωnij)k

  • i=j时:

ABij=1nk=0n11k=1n×n=1

  • ij时:

ABij=1nk=0n1(ωnij)k(求和引理)=0

综上所述,AB=In,即B=A1

以上,离散傅里叶逆变换(IDFT)的表达式为:

ak=1nj=0n1ykωnkj

a=IDFTn(y)

同理,可以用相同的方法把IDFT加速到O(nlogn),称为IFFT。

6、NTT

1.定义

有时候我们会想要模素数p意义下的多项式乘法。此时,由次数界为n的多项式A(x),B(x)的系数表达a,bS(x)=A(x)B(x)的系数表达s的公式为:

sk=j=0kajbkjmodp

FFT无能为力,我们需要一种新的DFT,以数论的办法进行,这就是快速数论变换(NTT)

2.原根

(1)定义

我们需要一种有数论循环性质的新数,原根恰好满足我们的要求。

m为正整数,a为整数,若am的阶等于φ(m),则称a为模m的一个原根。

假设g是素数p的原根,有1<g<p,且对于k=0,1,2,,p1,有gkmodp的结果两两不同,且gp11(modp)

可以发现,原根同样有循环的性质。因此,我们类比ωnk的定义,把原来的ωnk=e2πikn替换为gk(p1)n

(2)性质

我们来证明一些类似单位复数根的性质。

变换恒等式
因为:

gp11

所以:

g(k+n)(p1)ngk(p1)ngp1gk(p1)n


消去引理
显然有:

gdk(p1)dngk(p1)n


折半引理
因为:

g(k+n2)(p1)ngk(p1)ngp12

所以若要使:

gk(p1)n+g(k+n2)(p1)n0(modp)

成立,即:

gk(p1)2(1+gp12)0

只需证:

gp12p1

由于:

gk0,1,2,,p1

那么我们可以设:

gp12x,x=0,1,2,,p1

因为:

(gp12)2gp11

所以:

x210

即:

(x+1)(x1)0

又因为p为素数,所以有:

x+10x10

所以:

x=1,p1

又因为:

gp11,gkmodp

所以:

x=p1

即:

gp12p1

得证:

gk(p1)n+g(k+n2)(p1)n0


求和引理

k=0n1gkj(p1)nk=0n1(gj(p1)n)kgnj(p1)n1gj(p1)n1(gp1)j1gj(p1)n10

这样,我们就证明了原根由于复数单位根相同的性质。

3.公式

我们用原根替换复数单位根,得到:

yk=A(gk(p1)n)=j=0n1akgkj(p1)n

y=NTTn(a)。逆变换:

ak=1nj=0n1ykgkj(p1)n

a=INTTn(y)

7、其他扩展

1.任意模数FFT

有的时候我们需要卷积后模上一个数,这个数不是NTT模数,甚至可能不是个质数。那我们该怎么做呢?

这里使用拆系数FFT,本质是以时间换精度。

现在给定次数界为m的两个多项式A(x),B(x),要求A(x)B(x)modP

首先,令M=P,再对于每个aibi,把其变为kiM+ri(ri<M)的形式。这样,kiri就都小于等于M了。

那么卷积就可以写成:

ci=k=0iakbik=k=0i(kaiM+rai)(kbikM+rbik)=M2k=0ikaikbik+Mk=0i(kairbik+raikbik)+k=0irairbik

那么我们看到,ci可以由kr合并得到。那么我们对ka,kb,ra,rb分别做FFT,再对应算出x1=kakb,x2=karb+rakb,x3=rarb,对它们再分别做IFFT,就可以由c=M2x1+Mx2+x3得到答案了。

这么做的好处究竟在哪里呢?事实上,计算后的最大值由mP变为了mP,避免了溢出。

时间复杂度:O(nlogn)

2.多项式求逆

现在我们有一个次数界为n的多项式A(x),要求B(x)满足A(x)B(x)1(modxn)

我们考虑倍增实现。

  • n=1时,直接求逆元求得B(x)A(x)p2
  • n>1时,已有A(x)G(x)1(modxn2)

因为:

A(x)B(x)1(modxn)

又因为除了0次项之外到n1次都为0,因此到n21次项也为零:

A(x)B(x)1(modxn2)

A(x)G(x)1(modxn2)

两式相减:

A(x)[B(x)G(x)]0(modxn2)

因为:

A(x)0

所以:

B(x)G(x)0(modxn2)

既然0n21次项都为零,那么平方之后0n1次项也为零:

B(x)22B(x)G(x)+G(x)20(modxn)

A(x)B(x)1(modxn)

两边同时乘上A(x),得:

B(x)2G(x)+A(x)G(x)20(modxn)

移项,得:

B(x)2G(x)A(x)G(x)2(modxn)

这样就可以了。

时间复杂度:O(nlogn)

3.多项式开根

前置:多项式求逆。

现在我们有一个次数界为n的多项式A(x),要求B(x)满足B(x)2A(x)(modxn)

还是倍增。

  • n=1时,B(x)等于A(x)在模意义下的开根。
  • n>1时,已有G(x)2A(x)(modxn2)

因为:

G(x)2A(x)(modxn2)

移项,得:

G(x)2A(x)0(modxn2)

两边平方,同理可得:

G(x)42G(x)2A(x)+A(x)20(modxn)

所以:

[G(x)2+A(x)]24G(x)2A(x)0(modxn)

即:

[G(x)2+A(x)]24G(x)2A(x)(modxn)

除过去:

[G(x)2+A(x)]24G(x)2A(x)(modxn)

得到:

A(x)(G(x)2+A(x)2G(x))2B(x)2(modxn)

即:

B(x)G(x)2+A(x)2G(x)A(x)2G(x)+G(x)2(modxn)

这就可以了。

时间复杂度:O(nlogn)

4.多项式求导

根据导数的可加性和幂函数求导法则d(cxk)dx=ckxk1,有多项式的导数为:

d(k=0n1akxk)dx=k=0n1d(akxk)dx=k=1n1kakxk1=k=0n2(k+1)ak+1xk

时间复杂度:O(n)

5.多项式积分

根据积分的可加性和幂函数的不定积分cxkdx=ckxk+1,有多项式的不定积分为:

k=0n1akxkdx=k=0n1akxkdx=k=0n1akkxk+1=k=1nak1k1xk

时间复杂度:O(n)

6.多项式求ln

前置:多项式求导&积分&求逆

现在已知多项式A(x),要求B(x)=lnA(x)。我们两边微分,得到:

B(x)=d(lnA(x))dA(x)dA(x)dx

B(x)=A(x)A(x)

再两边积分,就得到:

B(x)=A(x)A(x)dx

因此,我们直接多项式求导+求逆+积分解决。

时间复杂度:O(nlogn)

7.多项式求exp

前置:多项式求ln

现在,我们已知多项式A=A(x)(这样写是因为在这里把A视为是与x无关的),求F(x)=exp(A)=eA。只要我们设G(x)=lnxA,就得到:

G(F(x))=lnF(x)A=0

我们考虑用牛顿迭代法倍增解这个方程。

对于牛顿迭代法的初始解,即结果的常数项,我们并不知道具体值。但是如果不对的话,也只是缺少了一个常数罢了,那我们不妨设F(x)=1

倍增:现在设我们已经求出了F(x)的前nF0(x),即:

F0(x)F(x)(modxn)

根据牛顿迭代法,我们求出下一个近似解为:

F(x)F0(x)G(F0(x))G(F0(x))F0(x)lnF0(x)AF0(x)1F0(x)(1lnF0(x)+A(x))

如此,就可以倍增实现了。

时间复杂度:O(nlogn)

8.多项式求幂

已知多项式A(x)和指数k,求A(x)k

在幂数很大的时候,为了确定出系数,时间复杂度为O(knlog(nk)),我们必须进行优化。

我们发现:

A(x)k=(elnA(x))k=eklnA(x)=exp(klnA(x))

于是我们可以用多项式求ln+多项式求exp在O(nlogn)的时间内求出。

8、代码实现

1.二进制反转

可以用一种类似dp的思想计算。

边界:0的二进制翻转为0

递归式:对于a,已经算出了reva2a就是除去最后一位的二进制翻转(reva2)向后移动一位再补上第一位。即:

rev[a]=(rev[a>>1]>>1)|((a&1)<<(l-1))

l为要翻转的位数。

2.复数类

套公式即可。

struct complex{
    double re,im;
    complex(double re=0,double im=0):re(re),im(im){}
    complex operator+(complex &b)const{return complex(re+b.re,im+b.im);}
    complex operator-(complex &b)const{return complex(re-b.re,im-b.im);}
    complex operator*(complex &b)const{return complex(re*b.re-im*b.im,re*b.im+im*b.re);}
};

3.FFT

  • 1:根据二进制翻转交换ararev(r)
  • 2:枚举归并步长i[1,n)i为二的幂;
    • 2.1: 根据欧拉公式求出ωi1=cosπi+isinπi
    • 2.2:枚举归并位置j,归并[j,j+i)[j+i,j+2i),步长为2i
      • 2.2.1:枚举x的幂数k[0,i)进行蝴蝶操作计算y,根据单位根计算ωik
        • 2.2.1.1:yj+k=yj+k+ωikyj+k+i
        • 2.2.1.2:yj+k+i=yj+kωikyj+k+i

注意:由于在C++中值会被更改,因此需要引入临时变量。

void FFT(complex c[]){
    for(int i=0;i<n;i++)if(i<r[i])swap(c[i],c[r[i]]);
    for(int i=1;i<n;i<<=1){
        complex omn(cos(pi/i),sin(pi/i));
        for(int j=0;j<n;j+=(i<<1)){
            complex om(1,0);
            for(int k=0;k<i;k++,om=om*omn){
                complex x=c[j+k],y=om*c[j+k+i];
                c[j+k]=x+y;
                c[j+k+i]=x-y;
            }
        }
    }
}

4.IFFT

由于公式中只差一个负号而已,因此引入一个参数type,在欧拉公式的地方乘上去,再除以n就可以了。

void FFT(complex c[],int tp=1){
    for(int i=0;i<n;i++)if(i<r[i])swap(c[i],c[r[i]]);
    for(int i=1;i<n;i<<=1){
        complex omn(cos(pi/i),tp*sin(pi/i));
        for(int j=0;j<n;j+=(i<<1)){
            complex om(1,0);
            for(int k=0;k<i;k++,om=om*omn){
                complex x=c[j+k],y=om*c[j+k+i];
                c[j+k]=x+y;
                c[j+k+i]=x-y;
            }
        }
    }
}
void IFFT(complex c[]){
    FFT(c,-1);
    for(int i=0;i<=m;i++)a[i].re/=n;
}

5.多项式乘法

模板:洛谷3083

注意:

1、由于FFT要求n2的幂且结果的次数界较大,所以要把两个因式的系数补到l位,l满足l=2tl/2大于等于因式的次数界。

2、FFT虽然在数学上精准,但在C++中误差巨大,因此虚部不会为0,忽略即可。实部也不为正数,可以加上0.1再向下取整。

代码:

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
const double pi=acos(-1);
struct complex{
    double re,im;
    complex(double re=0,double im=0):re(re),im(im){}
    complex operator+(complex &b)const{return complex(re+b.re,im+b.im);}
    complex operator-(complex &b)const{return complex(re-b.re,im-b.im);}
    complex operator*(complex &b)const{return complex(re*b.re-im*b.im,re*b.im+im*b.re);}
}a[2097153],b[2097153];
int n,m,l,r[2097153];
void FFT(complex c[],int tp=1){
    for(int i=0;i<n;i++)if(i<r[i])swap(c[i],c[r[i]]);
    for(int i=1;i<n;i<<=1){
        complex omn(cos(pi/i),tp*sin(pi/i));
        for(int j=0;j<n;j+=(i<<1)){
            complex om(1,0);
            for(int k=0;k<i;k++,om=om*omn){
                complex x=c[j+k],y=om*c[j+k+i];
                c[j+k]=x+y;
                c[j+k+i]=x-y;
            }
        }
    }
}
void IFFT(complex c[]){
    FFT(c,-1);
    for(int i=0;i<=m;i++)a[i].re/=n;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<=n;i++)scanf("%lf",&a[i].re);
    for(int i=0;i<=m;i++)scanf("%lf",&b[i].re);
    m+=n;
    for(n=1;n<=m;n<<=1)l++;
    for(int i=0;i<n;i++)r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
    FFT(a);
    FFT(b);
    for(int i=0;i<=n;i++)a[i]=a[i]*b[i];
    IFFT(a);
    for(int i=0;i<=m;i++)printf("%d ",int(a[i].re+0.5));
}

6.(I)FFT+(I)NTT

给出一个n次多项式和一个m次多项式的系数表达(1n,m4000000),求乘积。type=0时,直接计算;type=1时,结果取模998244353(原根为3)。

注:为了方便阅读,代码效率不高。若要提速,可以把单位根打表。而且,由于gp1np1n必须为整数,故仅在第一个比n+m大的2的整数次幂是p1的约数时才可行,此处9982443531=998244352=119×223223=8388608>n+m

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
const double pi=acos(-1);
const int p=998244353;
typedef long long ll;
struct complex{
    double re,im;
    complex(double re=0,double im=0):re(re),im(im){}
    complex operator+(complex b)const{return complex(re+b.re,im+b.im);}
    complex operator-(complex b)const{return complex(re-b.re,im-b.im);}
    complex operator*(complex b)const{return complex(re*b.re-im*b.im,re*b.im+im*b.re);}
}af[131073],bf[131073];
struct modp{
    int a;
    modp(int a=0):a(a){}
    modp operator+(modp b)const{return (a+b.a)%p;}
    modp operator-(modp b)const{return (a-b.a+p)%p;}
    modp operator*(modp b)const{return ll(a)*b.a%p;}
    modp operator/(modp b)const{return (b^(p-2))*a;}
    modp operator^(int b)const{
        modp ans=1,bs=a;
        while(b){
            if(b&1)ans=ans*bs;
            bs=bs*bs;
            b>>=1;
        }
        return ans;
    }
}an[131073],bn[131073];
const modp g=3;
int n,m,l,r[131073],type;
modp gn[18];
void init(){
    m+=n;
    for(n=1;n<=m;n<<=1)l++;
    for(int i=0;i<n;i++)r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
    for(int i=0;i<=17;i++)gn[i]=g^((p-1)/(1<<i));
}
void FFT(complex c[],int tp=1){
    for(int i=0;i<n;i++)if(i<r[i])swap(c[i],c[r[i]]);
    for(int i=1;i<n;i<<=1){
        complex omn(cos(pi/i),tp*sin(pi/i));
        for(int j=0;j<n;j+=(i<<1)){
            complex om(1,0);
            for(int k=0;k<i;k++,om=om*omn){
                complex x=c[j+k],y=om*c[j+k+i];
                c[j+k]=x+y;
                c[j+k+i]=x-y;
            }
        }
    }
}
void IFFT(complex c[]){
    FFT(c,-1);
    for(int i=0;i<=n;i++)c[i].re/=n;
}
void NTT(modp c[],int tp=1){
    for(int i=0;i<n;i++)if(i<r[i])swap(c[i],c[r[i]]);
    for(int i=1,id=1;i<n;i<<=1,id++){
        modp ggn=gn[id]^(tp==1?1:p-2);
        for(int j=0;j<n;j+=(i<<1)){
            modp gg=1;
            for(int k=0;k<i;k++,gg=gg*ggn){
                modp x=c[j+k],y=gg*c[j+k+i];
                c[j+k]=x+y;
                c[j+k+i]=x-y;
            }
        }
    }
}
void INTT(modp c[]){
    NTT(c,-1);
    for(int i=0;i<=n;i++)c[i]=c[i]/n;
}
int main(){
    scanf("%d%d%d",&n,&m,&type);
    if(type==0){
        for(int i=0;i<=n;i++)scanf("%lf",&af[i].re);
        for(int i=0;i<=m;i++)scanf("%lf",&bf[i].re);
    }else{
        for(int i=0;i<=n;i++)scanf("%d",&an[i].a);
        for(int i=0;i<=m;i++)scanf("%d",&bn[i].a);
    }
    init();
    if(type==0){
        FFT(af);
        FFT(bf);
        for(int i=0;i<=n;i++)af[i]=af[i]*bf[i];
        IFFT(af);
        for(int i=0;i<=m;i++)printf("%d ",int(af[i].re+0.5));
    }else{
        NTT(an);
        NTT(bn);
        for(int i=0;i<=n;i++)an[i]=an[i]*bn[i];
        INTT(an);
        for(int i=0;i<=m;i++)printf("%d ",an[i].a);
    }
    printf("\n");
}

常数只有上面那个的三分之一的NTT(正式考试请务必采用这种写法):

PS:有一道题上面那个3700ms,这个1000ms

inline ll pow(ll a,int b){
    ll ans=1;
    while(b){
        if(b&1)ans=ans*a%p;
        a=a*a%p;
        b>>=1;
    }
    return ans;
}
inline ll add(ll a,ll b){return a+b>p?a+b-p:a+b;}
inline ll cut(ll a,ll b){return a-b<0?a-b+p:a-b;}
void init(){
    for(n=1;n<=s;n<<=1);
    nn=n;
    gn[0][0]=gn[1][0]=1;
    gn[0][1]=pow(g,(p-1)/(n<<1));
    gn[1][1]=pow(gn[0][1],p-2);
    for(int i=2;i<(n<<1);i++){gn[0][i]=gn[0][i-1]*gn[0][1]%p;gn[1][i]=gn[1][i-1]*gn[1][1]%p;}
    inv[1]=1;
    for(int i=2;i<=(n<<1);i++)inv[i]=inv[p%i]*(p-p/i)%p;
}
void NTT(ll c[],int n,int tp=1){
    for(int i=0;i<n;i++){
        r[i]=(r[i>>1]>>1)|((i&1)*(n>>1));
        if(i<r[i])swap(c[i],c[r[i]]);
    }
    for(int i=1;i<n;i<<=1){
        for(int j=0;j<n;j+=(i<<1)){
            for(int k=0;k<i;k++){
                ll x=c[j+k],y=gn[tp!=1][nn/i*k]*c[j+k+i]%p;
                c[j+k]=add(x,y);
                c[j+k+i]=cut(x,y);
            }
        }
    }
}
void INTT(ll c[],int n){
    NTT(c,n,-1);
    for(int i=0;i<n;i++)c[i]=c[i]*inv[n]%p;
}

7.任意模数FFT

给定n,m,P,再给定次数界为n的第一个多项式和次数界为m的第二个多项式,求两个多项式的卷积模P

注意:拆系数FFT精度损失极大,需要使用long double保证正确性。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
typedef long long ll;
int x,n,m,l,r[262145],pm,p;
const long double pi=acos(-1);
struct complex{
    long double re,im;
    complex(long double re=0,long double im=0):re(re),im(im){}
    complex operator+(const complex &b)const{return complex(re+b.re,im+b.im);}
    complex operator-(const complex &b)const{return complex(re-b.re,im-b.im);}
    complex operator*(const complex &b)const{return complex(re*b.re-im*b.im,re*b.im+im*b.re);}
}k1[262145],r1[262145],k2[262145],r2[262145],c1[262145],c2[262145],c3[262145];
void FFT(complex c[],int tp=1){
    for(int i=0;i<n;i++)if(i<r[i])swap(c[i],c[r[i]]);
    for(int i=1;i<n;i<<=1){
        complex omn(cos(pi/i),tp*sin(pi/i));
        for(int j=0;j<n;j+=(i<<1)){
            complex om(1,0);
            for(int k=0;k<i;k++,om=om*omn){
                complex x=c[j+k],y=om*c[j+k+i];
                c[j+k]=x+y;
                c[j+k+i]=x-y;
            }
        }
    }
}
void IFFT(complex c[]){
    FFT(c,-1);
    for(int i=0;i<=n;i++)c[i].re/=n;
}
void init(){
    m+=n;
    for(n=1;n<=m;n<<=1)l++;
    for(int i=0;i<n;i++)r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
}
int main(){
    scanf("%d%d%d",&n,&m,&p);
    pm=sqrt(p);
    for(int i=0;i<=n;i++){
        scanf("%d",&x);
        k1[i]=x/pm;
        r1[i]=x%pm;
    }
    for(int i=0;i<=m;i++){
        scanf("%d",&x);
        k2[i]=x/pm;
        r2[i]=x%pm;
    }
    init();
    FFT(k1);
    FFT(r1);
    FFT(k2);
    FFT(r2);
    for(int i=0;i<=n;i++){
        c1[i]=k1[i]*k2[i];
        c2[i]=k1[i]*r2[i]+r1[i]*k2[i];
        c3[i]=r1[i]*r2[i];
    }
    IFFT(c1);
    IFFT(c2);
    IFFT(c3);
    for(int i=0;i<=m;i++){
        ll s1=ll(c1[i].re+0.5)%p*pm%p*pm%p,s2=ll(c2[i].re+0.5)%p*pm%p,s3=ll(c3[i].re+0.5)%p;
        printf("%lld ",((s1+s2)%p+s3)%p);
    }
}

7.多项式的运算

依赖关系:
[拉格朗日反演][FFT][NTT][多项式大全]详解
直接按照公式打就好了。

我们先修改NTT:

void NTT(modp c[],int n,int tp=1){
    for(int i=0;i<n;i++){
        r[i]=(r[i>>1]>>1)|((i&1)*(n>>1));
        if(i<r[i])swap(c[i],c[r[i]]);
    }
    for(int i=1,id=1;i<n;i<<=1,id++){
        modp ggn=gn[id]^(tp==1?1:p-2);
        for(int j=0;j<n;j+=(i<<1)){
            modp gg=1;
            for(int k=0;k<i;k++,gg=gg*ggn){
                modp x=c[j+k],y=gg*c[j+k+i];
                c[j+k]=x+y;
                c[j+k+i]=x-y;
            }
        }
    }
}
void INTT(modp c[],int n){
    NTT(c,n,-1);
    for(int i=0;i<n;i++)c[i]=c[i]/n;
}

求逆:

void inverse(modp c[],int n=n){
    static modp t[262145],tma[262145];
    t[0]=c[0]^(p-2);
    for(int k=2;k<=n;k<<=1){
        for(int i=0;i<(k<<1);i++)tma[i]=(i<k?c[i]:0);
        for(int i=(k>>1);i<(k<<1);i++)t[i]=0;
        NTT(tma,k<<1);
        NTT(t,k<<1);
        for(int i=0;i<(k<<1);i++)t[i]=t[i]*2-t[i]*t[i]*tma[i];
        INTT(t,k<<1);
    }
    memcpy(c,t,sizeof(int)*n);
}

开根(c[0]=1):

void sqrt(modp c[],int n=n){
    static modp t[262145],tma[262145],tmb[262145];
    t[0]=1;
    for(int k=2;k<=n;k<<=1){
        for(int i=0;i<k;i++)tma[i]=t[i]*2;
        inverse(tma,k);
        for(int i=0;i<(k<<1);i++)tmb[i]=(i<k?c[i]:0);
        NTT(tma,k<<1);
        NTT(tmb,k<<1);
        for(int i=0;i<(k<<1);i++){
            modp tmp=tma[i];
            tma[i]=t[i];
            t[i]=tmp*tmb[i];
        }
        INTT(t,k<<1);
        for(int i=0;i<(k<<1);i++)t[i]=(i<k?t[i]+tma[i]/2:0);
    }
    memcpy(c,t,sizeof(int)*n);
}

求导:

void derivative(modp c[],int n=n){for(int i=0;i<n;i++)c[i]=c[i+1]*(i+1);}

积分:

void integrate(modp c[],int n=n){for(int i=n-1;i>=1;i--)c[i]=c[i-1]*inv[i];c[0]=0;}

求ln:

void ln(modp c[],int n=n){
    static modp t[262145];
    for(int i=0;i<(n<<1);i++)t[i]=(i<n?c[i]:0);
    derivative(t,n);
    inverse(c,n);
    NTT(t,n<<1);
    NTT(c,n<<1);
    for(int i=0;i<(n<<1);i++)c[i]=c[i]*t[i];
    INTT(c,n<<1);
    for(int i=n;i<(n<<1);i++)c[i]=0;
    integrate(c,n);
}

求exp:

void exp(modp c[]){
    static modp t[262145],ta[262145];
    t[0]=1;
    for(int k=2;k<=n;k<<=1){
        for(int i=0;i<(k<<1);i++)ta[i]=t[i];
        ln(ta,k);
        for(int i=0;i<k;i++)ta[i]=c[i]-ta[i];
        ta[0].a++;
        NTT(t,k<<1);
        NTT(ta,k<<1);
        for(int i=0;i<(k<<1);i++)t[i]=t[i]*ta[i];
        INTT(t,k<<1);
        for(int i=k;i<(k<<1);i++)t[i]=0;
    }
    memcpy(c,t,sizeof(modp)*n);
}

求幂:
我们在多项式求exp中假定了c[0]=1,那么如果常数项不是1的话我们就把常数项变为1在运算后再用快速幂变回来即可。

void pow(modp c[],int k){
    ln(c);
    for(int i=0;i<n;i++)c[i]=c[i]*k;
    exp(c);
}

9、拉格朗日反演

1.形式幂级数

对于任意一个域F我们定义在其上的形式幂级数为:

f(x)=k=0akxk,akF

记所有的形式幂级数为F[[x]]

2.反演公式

拉格朗日反演是求关于函数方程的幂级数展开系数非常重要的工具,可以用于组合计数函数的系数提取。

公式内容

这里[xn]f(x)指取f(x)xn的系数。

f(x),g(x)F[[x]]f(g(x))=x,则称f(x)g(x)复合逆。满足:

(1)[xn]g(x)=1n[x1]1f(x)n

特别的,如果f(x)=xϕ(x),那么有:
(2)[xn]g(x)=1n[xn1]ϕ(x)n

公式的推导
由式f(x)=xϕ(x),得ϕ(x)=xf(x),代入(2)式可得:

[xn]g(x)=1n[xn1](xf(x))n

公式的推广

[xn]h(g(x))=1n[xn1]h(x)(xf(x))n

相关标签: FFT