# 多项式学习笔记
做多项式题就像嗑药,出多项式题就像贩毒。—— 某 FJOI 某知名选手
快速傅里叶变换,指利用计算机快速高效的计算离散傅里叶变换的方法,简称FFT。
这是一种将多项式的系数表示法变为点值表示法的一种方法,简称 DFT,时间复杂度为优秀的 O(nlogn)。
我们有下面的多项式:
A(x)=a0+a1x+a2x2+a3x3+...+anxn
再定义以下两个多项式:
A1(x)A2(x)=a0+a2x+a4x2+...+anx2n=a1+a3x+a5x2+...+an−1x2n−1
此时,我们试着用 A1 和 A2 表示 A:
A(x)=A1(x2)+xA2(x2)
我们引入单位根这个神奇的东西
满足 xn=1 的 x 在复数域上的 n 个解,记为 ωn,称为单位根。
ωn1 称作 n 次单位根。
ωnk=(ωn1)k
ωnk=en2πki=cosn2kπ+sinn2kπi
我们不难发现单位根有许多方便的性质:
ωnk=ωtntk
ωnk=ωnk%n
ωnk+2n=−ωnk
(复数域上乘法的几何意义:模长相乘,辐角相加。)
回到正题,直接做系数表示与点值表示的时间复杂度为 O(n) ,为加速变换过程,我们选择一些特殊的点值 —— ωn0~ωnn−1 这 n 个,同时为了处理的方便, n 的值取 2 的整次幂。
将单位根代入 A(x) 中得到:
A(ωnk)A(ωnk+2n)=A1(ωn2k)+ωnkA2(ωn2k)=A1(ω2nk)+ωnkA2(ω2nk)=A1(ωn2k)−ωnkA2(ωn2k)=A1(ω2nk)−ωnkA2(ω2nk)
据此,我们就可以分治去算啦。
有了 DFT,我们还需要一种将点值表示转化为系数表示的算法,直接转化的时间复杂度为 O(n2), 这是我们不能接受的,而 IDFT 则是最佳选择。
我们有一个多项式:
A(x)=i=0∑n−1aixi
将 n 个单位根代入得到 n 个点值,ωni 对应的点值记为 yi。
令 B(x)=i=0∑n−1yixi ,将 ωnk 的倒数(共轭复数)ωn−k 代入:
B(ωn−k)∴ak=i=1∑n−1yiωn−ik=i=0∑n−1(j=0∑n−1ajωnij)ωn−ik=j=0∑n−1aji=0∑n−1(ωnj−k)i=nak=nB(ωn−k)
总结一下就是:
fk=i=0∑n−1ωnigi⟺gk=n1i=0∑n−1ωn−ifi
在 FFT 中,复数和正余弦函数的使用会造成不可避免的精度误差
” 在离散正交变换的理论中,已经证明在复数域内,具有循环卷积特性的唯一变换是 DFT,所以在复数域中不存在具有循环卷积性质的更简单的离散正交变换。“
"也就提出了因此提出了以数论为基础的具有循环卷积性质的快速数论变换(NTT),用有限域上的单位根来取代复平面上的单位根。"
原根与单位根有着一些相似的性质,说明原根或许能在一定程度上代替单位根进行多项式卷积。
一般来说,最小正原根并不大,所以可以枚举求出。
g 为模 p 意义下的原根的充要条件为:
gpip−1≡1(modp) 恒不成立,且 gp−1≡1(modp) 。其中,pi 为 p 的质因子
我们记 g 为模 p 意义下的原根,记 gn=gnp−1
我们可以得到如下性质:
gnngn2ngtntkgnngnkgn2n(gnk+2n)2=gn⋅np−1=gp−1=g2n⋅np−1=g2p−1=gnk≡1≡gnk%n≡−1≡gn2k+n≡gn2k(modp)(modp)(modp)(modp)
借以上性质,我们将 gnk 和 gnk+2n 代入上文多项式 A(x) 、 A1(x) 、 A2(x) 与 ωnk 和 ωnk+2n 本质无异。
在 IDFT 中,我们将单位根的倒数(共轭复数)代入上文的多项式 B(x) 中得到转换后的系数,而在 INTT 中代入的则是原根在模 p 意义下的逆元。
# Coding Time
| #define long long long |
| auto NTT(long *A, int type) -> void |
| { |
| long x, y, g0, gn; |
| for(int i = 0; i < limit; i += 1) |
| if(i < r[i]) swap(A[i], A[r[i]]); |
| for(int midl = 1; midl < limit; midl <<= 1) |
| { |
| int len = midl<<1; |
| g0 = ksm((type?g:invg), (p-1)/len); |
| for(int j = 0; j < limit; j += len) |
| { |
| gn = 1; |
| for(int k = 0; k < midl; k += 1, (gn *= g0) %= p) |
| { |
| x = A[j+k]%p; |
| y = gn*A[j+k+midl]%p; |
| A[j+k] = (x+y)%p; |
| A[j+k+midl] = (x-y+p)%p; |
| } |
| } |
| } |
| } |
# 任意模数的快速数论变换 (任意模数 NTT)
在 NTT 中,模数是有讲究的,这些质数要可以写成a⋅2k+1 的形式。
比如:
469762049=7∗226+1,(g=3)
998244353=119∗223+1,(g=3)
1004535809=479∗221+1,(g=3)
. . . . . .
而 2k 则是 NTT 能够运算的最大长度。
对于题目要求的其他模数,比如 109+7,或者普通的 NTT 就显得心有余而力不足了。
(道高一尺,魔高一丈)
任意模数 NTT(也可以称作三模数 NTT)在众望中出世。
我们知道 (不知道) : FFT 的运算结果 ≤NP2, 所以我们可以找三个大质数相乘大于这个结果,对三个模数分别计算出结果,最后中国剩余定理合并,然后模提莫要求的模数即可。
当然,直接合并这么大的数是会 Bomb 的,我们可以先合并前两个,再使用一些神奇的操作求出答案。
记最终答案为 ans, 选择的三个质数分别为 p1,p2,p3,
记三次 NTT 的结果为:
ans≡a1(modp1)ans≡a2(modp2)ans≡a3(modp3)
先用 CRT 合并得到:
ans≡a4(modp1p2)
转化为等式为:
ans=kp1p2+a4
若求出 k 便可得到 ans:
∵ans∴kp1p2+a4∴k∴ans≡a3≡a3≡(a3−a4)p1−1p2−1≡kp1p2+a4(modp3)(modp3)(modp3)(modp1p2p3)
因为 ans 是小于 p1p2p3 的,所以ans=kp1p2+a4。(别忘记取模)
# 多项式乘法逆
定义多项式 F−1 为 多项式 F 的乘法逆元,满足
F∗F−1≡1(modxn)
若我们已经得知 F∗H≡1(modn2n),需要我们求 F∗G≡1(modxn)。
首先,F∗G≡1(modx2n) 也是成立的,接下来就是推式子的时间:
∵F∗HF∗G∴F∗(H−G)(H−G)(H−G)2F∗(H2−2HG+G2)FH2−2HFG+FG2∵F∗G∴FH2−2H+GG≡1(modx2n)≡1(modx2n)≡0(modx2n)≡0(modx2n)≡0(modxn)≡0(modxn)≡0(modxn)≡1(modxn)≡0(modxn)≡2H−FH2(modxn)
# Coding Time
| #define long long long |
| auto MUL(long *A, long *B, int len) -> void |
| { |
| long invlim; |
| limit = 1, l = 0; |
| while(limit <= (len<<1)) limit <<= 1, l += 1; |
| for(int i = 1; i < limit; i += 1) |
| r[i] = (r[i>>1]>>1)|((i&1)<<(l-1)); |
| for(int i = len; i < limit; i += 1) A[i]=0; |
| NTT(A, 1); NTT(B, 1); |
| for(int i = 0; i < limit; i += 1) |
| B[i] = (2-A[i]*B[i]%p+p)%p*B[i]%p; |
| NTT(B, 0); |
| invlim = ksm(limit, p-2); |
| for(int i = 0; i < limit; i += 1) (B[i] *= invlim) %= p; |
| for(int i = len; i < limit; i += 1) B[i] = 0; |
| } |
# 多项式对数函数 (多项式 ln)
# 前置知识
# 简单导数
常数和基本初等函数导数公式:
(C)′(xμ)′(sinx)′(cosx)′(ax)′(ex)′(logax)′(lnx)′=0=μxμ−1=cosx=−sinx=axlna(a>0,a=1)=ex=xlna1(a>0,a=1)=x1
复合函数的求导法则:
定理 1: 如果 u=g(x) 在点 x 可导,y=f(u) 在点 u=g(x),那么复合函数 y=f(g(x)) 在点 x 可导,且其导数为 dxdy=f′(u)⋅g′(x) 或 dxdy=dudy⋅dxdu。
# 简单积分
定义 1: 如果在区间 I 上,可导函数 F(x) 的导数为 f(x),即 ∀x∈I,都有 F′(x)=f(x) 或 dF(x)=f(x)dx
那么函数 F(x) 就称为 f(x) 或 f(x)dx 在区间 I 上的一个原函数。
原函数存在定理:连续函数一定有原函数
定义 2: 在区间 I 上,函数 f(x) 的带有任意常数项的原函数称为 f(x)(或 f(x)dx) 在区间 I 上的不定积分,记作 ∫f(x)dx。
其中,记号 ∫ 称为积分号,f(x) 称为被积函数, f(x)dx 称为被积表达式,x 称为积分变量。
# 多项式对数函数
我们有多项式 F(x) 和 G(x),记 G≡lnF(modxn)
推式子时间:
GG′G′G′≡lnF≡(lnF)′≡(ln′F)∗F′≡FF′(modxn)(modxn)(modxn)(modxn)
我们就可以先把 G′ 搞出来,然后再积回去,就是答案啦。
# Coding Time
| #define long long long |
| auto main() -> signed |
| { |
| scanf("%d", &n); |
| for(int i = 0; i < n; i += 1) |
| scanf("%d", &a[i]); |
| inv(b, n); |
| der(a, n); |
| NTT::MUL(a, b, n); |
| itg(a, n); |
| for(int i = 0; i < n; i += 1) |
| printf("%lld ", a[i]); |
| return 0; |
| } |
| auto inv(long *B, int n) -> void |
| { |
| if(n == 1) return (void)(B[0] = ksm(a[0], p-2)); |
| inv(B, (n+1)>>1); |
| for(int i = 0; i < n; i += 1) c[i] = a[i]; |
| NTT::INV(c, B, n); |
| } |
| |
| auto der(long *A, int n) -> void |
| { |
| for(int i = 1; i < n; i += 1) |
| A[i-1] = A[i]*(i)%p; |
| A[n-1] = A[n] = 0; |
| } |
| |
| auto itg(long *A, int n) -> void |
| { |
| for(int i = n; i >= 1; i -= 1) |
| A[i] = A[i-1]*ksm(i, p-2)%p; |
| A[0] = 0; |
| } |
# 多项式指数函数 (多项式 exp)
# 前置知识
# 泰勒展开 (Taylor)
泰勒中值定理 1: 如果函数在 x0 处有 n 阶导数,那么存在 x0 的一个邻域,对于该邻域内任一 x,有
f(x)=f(x0)+f′(x0)(x−x0)+2!f′′(x0)(x−x0)2+...+n!f(n)(x0)(x−x0)n+Rn(x)
其中,Rn(x)=ο((x−x0)n).
泰勒展开在 x0 处展开的封闭形式如下: n=0∑+∞n!f(n)(x0)(x−x0)n
# 牛顿迭代 (Newton's method)
牛顿迭代用于求函数零点,通过不断地切线逼近所求值,但最终也只是近似值,迭代的次数越多,精确度越高,误差越小。
若我们要求函数 f(x) 的零点,初始近似值为 x0,切线方程为 y=f′(x0)(x−x0)+f(x0)
令 y=0,得 x=x0−f′(x0)f(x0).
还需提的一点是,在多项式中,每迭代一次,精度翻倍。
# 多项式指数函数
我们有多项式 F(x) 和 A(x), 记多项式的对数函数为 F≡eA(modxn).
对两边分别进行 ln 运算,得到 lnF≡A(modxn).
移项,得 lnF−A≡0(modxn)
记 G(F)=lnF−A,有 G(F)≡0(modxn)
另外。我们有 G(F0)≡0(modx2n)。
这时,我们就可以带入牛顿迭代中求解,推式子时间到:
G(F0)FFFF≡0(modx2n)≡F0−G′(F0)G(F0)≡F0−F0∗G(F0)≡F0−F0(lnF0−A)≡F0(A−lnF0+1)(modxn)(modxn)(modxn)(modxn)
又可以递归求解啦
# Coding Time
| #define long long long |
| auto exp(long *B, int n) -> void |
| { |
| if(n == 1) return (void)(B[0] = 1); |
| exp(B, (n+1)>>1); |
| memset(c, 0, sizeof(c)); |
| lnz(B, c, n); |
| for(int i = 0; i < n; i += 1) |
| c[i] = (a[i]-c[i]+p)%p; |
| c[0] += 1; |
| NTT::MUL(B, c, n); |
| } |
| auto lnz(long *A, long *B, int n) -> void |
| { |
| memset(d, 0, sizeof(d)); inv(A, B, n); |
| memset(d, 0, sizeof(d)); der(A, d, n); |
| NTT::MUL(B, d, n); |
| itg(B, n); |
| } |
| auto inv(long *A, long *B, int n) -> void |
| { |
| if(n == 1) return (void)(B[0] = ksm(A[0], p-2)); |
| inv(A, B, (n+1)>>1); |
| for(int i = 0; i < n; i += 1) d[i] = A[i]; |
| NTT::INV(d, B, n); |
| } |
| auto der(long *A, long *B, int n) -> void |
| { |
| for(int i = 1; i < n; i += 1) |
| B[i-1] = A[i]*i%p; |
| B[n-1] = B[n] = 0; |
| } |
| auto itg(long *A, int n) -> void |
| { |
| for(int i = n; i >= 1; i -= 1) |
| A[i] = A[i-1]*ksm(i, p-2)%p; |
| A[0] = 0; |
| } |
# 多项式开根
我们有多项式 F(x) 和 A(x),满足 F2(x)≡A(x)(modxn).
要去求多项式 F(x),首先我们要有一个多项式 H(x) 满足 H2(x)≡A(x)(modx2n).
显然,F2(x)≡A(x)(modx2n) 成立。
记 G(F)=F2−A.
又是令人愉悦的推式子时间:
F2(x)H2(x)H2−AG(H)FFF≡A(x)≡A(x)≡0≡0≡H−G′(H)G(H)≡H−2HH2−A≡2HH2+A(modxn)(modx2n)(modx2n)(modx2n)(modxn)(modxn)(modxn)
还是递归下去,求个逆元乘起来就好啦。
# Coding Time
| #define long long long |
| auto sqrt(long *A, long *B, int n) -> void |
| { |
| if(n == 1) return (void)(B[0] = 1); |
| sqrt(A, B, (n+1)>>1); |
| memset(c, 0, sizeof(c)); |
| inv(B, c, n); |
| for(int i = 0; i < n; i += 1) d[i] = A[i]; |
| NTT::SRT(d, B, c, n); |
| } |
| auto inv(long *A, long *B, int n) -> void |
| { |
| if(n == 1) return (void)(B[0] = ksm(a[0], p-2)); |
| inv(A, B, (n+1)>>1); |
| for(int i = 0; i < n; i += 1) d[i] = A[i]; |
| NTT::INV(d, B, n); |
| } |
还有 NTT::SRT
里的 :
| #define long long long |
| auto SRT(long *A, long *B, long *C, int len) -> void |
| { |
| limit = 1, l = 0; |
| while(limit <= (len<<1)) limit <<= 1, l += 1; |
| for(int i = len; i < limit; i += 1) d[i] = 0; |
| for(int i = 1; i < limit; i += 1) |
| r[i] = (r[i>>1]>>1)|((i&1)<<(l-1)); |
| NTT(A, 1); NTT(B, 1); NTT(C, 1); |
| for(int i = 0; i < limit; i += 1) |
| B[i] = (B[i]*B[i]%p+A[i]%p)*C[i]%p*inv2%p; |
| NTT(B, 0); liminv = ksm(limit, p-2); |
| for(int i = 0; i < limit; i += 1) |
| (B[i] *= liminv) %= p; |
| for(int i = len; i < limit; i += 1) B[i] = 0; |
| } |
Tobecontinued...
牛顿迭代图片摘自此篇博客