Jhb 的多项式浅谈

这个的后半部分似乎可以当做金策大佬论文的人话解读。

推销一下窝的博客 , 本文所有的代码都在窝的博客上。


多项式主要的用途是用来优化式子,因为笔者也刚刚入门多项式,可能谈到的内容也十分浅,有错请直接开 D。

我们从大整数乘法入手,我们经常写的算法是通过枚举两个整数的每一位,通过位权来更新答案,这里显然是 O(n2) 。还有一种是分治乘法,还是通过位权,原理是 :
(Axm+B)(Cxm+D)=ACx2m+((A+B)(C+D)ACBD))xm+BD
我们直接分治可以算出答案复杂度 O(n2) 复杂度比较优秀?其实这种想法和之后的 MTT 优化是相同的。其中 n 表示位数。

复杂度怎么算? T(n)=4T(n2)+O(n)

T(n)=O(n2)

之后我们考虑优化 ACxm+((AB)(DC)+AC+BD)×xn2+BD 这边只有 3 个乘积可以考虑优化 T(n)=3T(n2)+O(n)=O(nlog23) 可以接受。

主定理:
T(n)=aT(nb)=O(nd) T(n)={O(nd),d>logba O(ndlogn),d=logba O(nlogba),d<logba

但是我们可以我们 FFT 将系数表示法变成点值表示法,从而得到答案复杂度 O(nlogn)

我们常用的表示法是系数表示法 f(x)=i=0aixi 我们可以从中取 n + 1 个点来表示这个多项式,也就是 f(x)=a0,a1an

DFT 就是把一个多项式从系数转化成点值,IDFT 反之。

FFT

对于两个个点值表示法的多项式 f(x)=(x0,f(x0))(xn,f(xn))g(x)=(x0,g(x0))(xn,g(xn))

F(x)=f(x)g(x)=(x0,f(x0)g(x0))(xn,f(xn)g(xn)) 可以看出我们能够快速得将其相乘。

但是直接暴力 DFT,IDFT 是 O(n2) 的,有些暴力还是用高斯消元。。。

我们发现 xix=1,1 的时候是很简单的,这会让我们想到圆,之后我们需要 n + 1 个数,那么我们不妨用模长为 1 的复数。

ωn=e2πinxn=1 的解集为 ωnk|k=0,1n1 也就是将一个圆分成 n 等分。

ωn=e2πin=cos(2πn)+isin(2πn)

单位圆

在一个复平面上,以原点为圆心,1 为半径作圆,所得到的圆就是单位圆,将其 n 等分( n 等分点就是终点),形成 n 个向量,设幅角为正且最小的向量 wn ,称为 n 次单位根

剩下的复数为 wn2wnn

wn0=wnn=1

wnk=cosk×2πn+isink×2πn

单位根的幅角为周角的 1n

wnn2+k=wnk

证明考虑将其展开 wnn2×wnk

将左边的那项展开得到 1

(wnk)2=wn2k

证明考虑,复数相乘,幅角相加,模长为 1×1=1 因为都是单位根


FFT

首先我们先假设一个多项式的系数 假设 n 为偶数 A(x)=(a0,a1,an1)

$$
A(x)=a_0+a_1x+a_2{x^2}+a_3*{x^3}+a_4*{x^4}+a_5*{x^5}+ \dots+a_{n-2}*x^{n-2}+a_{n-1}*x^{n-1}
$$

我们把它的下标按奇偶性分类
{A(x)=i=0n1aixiA1(x)=i=0m1a2ix2iA2(x)=i=0m1a2i+1x2i+1m=n2
所以我们可以得到

A(x)=i=0m1a2i(x2)i+xi=0m1a2i+1(x2)i

所以我们可以将其分成两部分进行递归,也就是 A(x)=A1(x2)+xA2(x2)

但是根据主定理,这是 O(n2)

我们考虑 A(wnk)=A1((wnk)2)+wnkA1((wnk)2)

(wnk)2=wn2k

所以可以化简
m=n2A(wnk)=A1(wmk)+wnkA2(wmk) 之后来看后半部分 A(wnk+m)=A1(wmk)wnkA2(wmk)
发现只要知道了 RHS 就可以直接求出, 复杂度为 O(nlogn)

这叫蝴蝶变换


IDFF

这里就只想结论 B(x)=i=0n1bi×xiwnki

其中 0k<n 的点值

发现 wnkwnk 是对应的, wnk=wnn+k

所以我们只要用 FFT 之后再将数组反过来就可以了


递归形式的 FFT

我们发现原来的每一个位置,在结束之后会变成其的反序

具体来说

cpp
1
2
3
原来的序列: 000 001 010 011 100 101 110 111
结束的序列: 000 100 010 110 001 101 011 111
也就是二进制倒了

我们考虑求出最终的位置,之后再倒序回去,因为

cpp
1
2
3
4
i 和 i / 2 的关系:
i = i >> 1 也就是二进制向左移动 1
既然是倒序,所以要向右移动 1
之后考虑奇数的情况,最后的一个 1 只要移动到最高位上就行了

Code

cpp
1
2
3
4
5
for(int i = 0; i < lim; ++ i) 
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (len - 1));

for(int i = 0; i < lim; ++ i) if(i < rev[i])
swap(A[i], A[rev[i]]);

最后我们考虑蝴蝶操作过程

m=n2 的所有值已经计算好并且放到 A 中了
A1(wmk)=A[k],A1(wmk)=A[k+m]A(wmk)=A[k]+wnk×A[k+m]A(wmk+m)=A[k]wnk×A[k+m]
之后我们一层层向上递推就可以了


NTT

我们发现 FFT 的精度其实不是很够,最多处理 108 的数据。我们考虑用什么东西来代替这个 FFT。这里有一个很好的替代品就是原根。对于质数 p=qn+1,n=2m 原根 g 满足 gqn=1(modp) 所以将 gn 看做 ωn 也是可以的。

常用的模数是:
p={998244353=479×221+1,g=31004535809=7×17×223+1,g=3469762049=7×226+1,g=3
因为他们的原根都是 3。(这里的模数都是常在 3% NTT 中使用的)

gqn=e2πn 我们如果迭代到长度 l 的时候 ωn=gl=gnnl=gnp1l


3 模数 NTT

我们考虑如果模数不支持 NTT 怎么办?我们可以自己定模数,只要保证答案符合(大于)范围即可。

其实很简单,就是用 3 个模数做一遍 NTT 之后 CRT 合并即可,用的就是之前说的模数。


MTT

我们考虑如果模数不支持 NTT 而且还卡常怎么办?虽然说 NTT 跑得很快,如果码力不行怎么办?用 MTT。

这里其实有点争议,就是 3% NTT 到底算不算 MTT。

本文只介绍一种最简单的,但是最常用的优化。

我们考虑进行 FFT 的时候我们用到了复数。那么我们为什么不把一个多项式的系数拆成几的部分分别进行处理,这样就可以保证精度。

假设我们要求 P×Q,P=i=0pixi,Q=i=1qixi

我们考虑将系数拆成两部分

A=Pi>>15,B=Pi&(32767)

C=Qi>>15,D=Qi&(32767)

PQ=AC230+(AD+BC)215+BD

之后我们考虑通过复数来表示,设 F=A+iB,G=C+i×D

这样我们可以先通过 FFT 之后再将其表示成答案的形式,最后在计算。

但是我们发现,需要表示之前的式子需要 F,G

F 表示 F 的共轭复数。

那么我们来证明一个东西。我们不妨记 conj(x) 表示 x 的共轭复数。

Pt 表示 P 的 DFT。
证明:P = A + iB, Q = A - iB 注意这里 P, Q 与之前的不同

pt(x)=A(wnk)+iB(wnk)=j=0n1Ajwnjk+iBjwnjk=j=06n1(Aj+iBj)wnjk

之后我们考虑能不能将 Q 用 P 表示出来,我们考虑将 Q 进行展开。
Qt(x)=AwnkiB(wnk)=j=0n1(AjiBj)wnjk=j=0n1(AjiBj)(cos(2πjkn)+isin(2πjkn))=j=0n1Ajcos(2πjkn)+Bjsin(2πjkn)+i(Ajsin(2πjkn)Bjcos(2πjkn))=conj(j=0n1Ajcos(2πjkn)+Bjsin(2πjkn)i(Ajsin(2πjkn)Bjcos(2πjkn)))=conj(j=0n1(Aj+iBj)×(cos(2πjkn)isin(2πjkn)))=conj(j=0n1(Aj+iBj)(wnjk)=conj(j=0n1(Aj+iBj)(wnjk))=conj(Pt(nk))
这里有两个细节,我们一开始是将两个式子都展开,之后通过欧拉定理进行展开,猜测两个数是共轭质数。

这里变成共轭的时候,将 cos 部分变成负数。

我们考虑一个更加普通的形式对于一个 wnk 的共轭复数是多少?将 wnk 表示成 a+ib 的形式,其共轭复数容易得到是 aib 之后我们考虑在数轴上对应的是多少

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-pQ3sobeh-1618049627378)(https://z3.ax1x.com/2021/04/10/cabTrq.png)]]

可以发现其在轴上的对应点就是 wnk 可以理解成,原来是向上数 k 的单位,现在是向下数。

之后将其展开也是同理,可以得到 wnk=cos(2πkn)+isin(2πkn)

但是我们计算答案的时候可没有负的指数,我们可以用 nk 来表示 k 因为圆上总共有 n 个点。

这样我们就证明了 Q 可以由 P 表示出来。

所以我们总共只需要进行 4 次 FFT 就可以得到答案了。


多项式求逆

我们需要求 F×Q1(modxn) 中的 Q

考虑使用递归解决,假设我们已经得到 Q1×F1(modxn2)

我们需要求 F×Q1(modxn)

可以得到 F×Q1(modxn2)

考虑将两个式子进行相减来消除 1
F×(QQ1)0(modxn2)
因为 F 肯定不是 0,所以考虑将其消掉,之后再考虑平方。
(QQ1)20(modxn)Q2+Q122QQ10(modxn)Q+FQ122Q10(modxn), 这里两边乘了 FQ2Q1FQ12(modxn)
这样递归去做就可以了。


多项式 Ln

需要求 QlnF(modxn)

我们考虑直接进行 exp 发现我们还没学过

那么能消去 Ln 的还有什么呢? 求导!

QFF(modxn)

之后又手就行。


多项式 Exp

需要求 QeF(modxn)

我们优先考虑 Ln 转换成 lnQF(modxn) 之后考虑进行牛顿迭代。
y=f(x0)+f(x0)(xx0)0=f(x0)+f(x0)(xx0)0=f(x0)f(x0)x0+f(x0)x=f(x0)x0f(x0)f(x0)x=x0f(x0)f(x0)
我们考虑将之前的式子变换得到 lnQF0

我们设函数 S(G)=lnGF

然后考虑带入之前的式子

这里说明一下 S(G)=1G 的原因,我们本质上是将整个 G 看成一个变量,而不是里面的每一个 xi 所以这里并不是符合函数求导。
G=G0lnG0F1G0G=G0G0×(lnG0F)G=G0×(1lnG0+F)
之后有手就行。


多项式开根

我们需要求 F2=G 还是考虑递归,假设 F12G(modxn2)

还是考虑消元
FF1(modxn2)FF10(modxn2)F2+F122FF10(modxn)GF122F1F0(modxn)FG2F1+F12(modxn)
还是很简单。


多项式除法

给定多项式 F,G 求出多项式 R,Q 使得 F=G×Q+R

其中 F 的次数为 n,G 的次数为 m

可以想到如果没有余数的话,我们直接用多项式求逆就可以水掉。

因为这是个多项式,我们考虑将 R 给消掉,然后就可以与愉快得求逆了。

考虑构造 4 个多项式,满足 F1=G1×Q1+xkR1 这样子只要 modxk 就可以得到答案了。考虑构造这个多项式,为了让外面有 xk 这个系数,所以考虑将 F(x) 变成 F(1x)。 可以知道变成这样之后的式子仍然是满足的,我们考虑两边同时乘以 xn

xnF(1x)=xnG×Q+xnR 我们考虑 xnF(1x) 的每一个系数其实等价于将原来的 F 给翻转一下的系数。那么我们不妨让 F1=xnF(1n),Q1=xmQ(1n) 其他同理。

那么我们得到 F1=G1×Q1+xnm+1R1 看出 k=nm+1。之后发现一个神奇的事情 Q1 的次数是 nm 所以我们即使 modxnm+1 也不会影响 Q1。所以我们得到式子 F1(x)=G1(x)×Q1(x)modxnm+1 也就是 Q1(x)=F1(x)G1(x)

之后我们求出了 G1 之后将其翻转一下就变成 G 了,然后就可以得到 R(x)=F(x)G(x)×Q(x)

很简单。


这边之后还会更新。