多项式全家桶

打算开 GF 这个万恶之源了,但在此之前先把多项式的那堆板子理理清楚吧。代码没有刻意卡常,而且写成的年代不同,码风和实现方法会有一点不一样,板子也不会太全,之后会遇到问题会在这里慢慢补充。

多项式乘法

给定一个 n 次多项式 F(x)=i=0nfixim 次多项式 G(x)=j=0mgjxj,求出 F(x)G(x) 的卷积:

i=0nj=0mfigjxi+j

注意到多项式乘法的过程,单从卷积定义角度考虑是难以优化的,所以我们从多项式本身考虑。

多项式有两种表示方法,点值表示法和系数表示法。其中系数表示法就是我们常用的形式:

F(x)={f0,f1,f2,,fn}

其中 fi 表示 xi 前面的系数。这显然可以唯一确定一个 n 次多项式。而点值表示法是这样的:

F(x)={(x0,F(x0)),(x1,F(x1)),,(xn,F(xn))}

注意到这 n+1 个点也能表示一个 n 次多项式,可以从高斯消元的角度考虑。

显然我们常用的(题目输入和要求输出的)都是第一种表示方法,那第二种表示方法有啥用呢。对于两个 n 次多项式 F(x),G(x),它们的卷积 H(x) 在点值表示法下能很轻易地被求出:

H(x)={(x0,F(x0)G(x0)),(x1,F(x1)G(x1)),,(xn,F(xn)G(xn))}

时间复杂度仅有 O(n)。但问题是题目不认点值表示法,而暴力在两种表示方法之间的转化复杂度是很高的。所以我们现在的任务就是找到在两种表示方法之间转化的合适方法。

FFT/快速傅里叶变换

前置知识:复数。

可以做到在 O(nlogn) 的时间复杂下完成点值表示法和系数表示法的相互转化。

我们先来看从系数表示法到点值表示法,注意到这里的瓶颈在于算 xi 和计算 F(xi)。这两个问题本质上其实挺像的,都是要求寻找一个合适的 x,使得 xi 具有一些美妙的性质。

注意到 1,1 的幂都是很好算的,但这样仅仅够我们算两个点,一共可是需要 n+1 个。所以我们要更多的点,更具体地讲,我们需要更多满足 |ωk|=1ω。可以发现需要引入复数了,i,i 显然是方程的解,除此之外,在单位圆上所有向量对应的复数都满足:

(应该都能看出来是从 OI wiki 拿的图吧)

严谨讲,我们定义 xn=1C 中的解是 n 次复根。根据上图,我们能发现这样的解有 n 个,根据欧拉公式 eix=cosx+isinx,我们定义:

ωn=e2πin

为单位复根,可以发现这个复数对应了把单位圆 n 等分的第一个角对应的向量。则 xn=1 的解集能用 wn 的幂表示:

{ωnk|k[0,n)Z}

说了这么多,真正要投入应用的话,我们还需要知道单位复根的一些性质。对于任意的正整数 n 和整数 k,有:

ωnn=1,wnk=ω2n2k,w2nk+n=w2nk

均可以通过把复数看成向量,用几何意义证明,不再赘述。而有了这些性质,就可以开始进入正片了。

FFT 的基本思路是分治,递归处理当 x=wnk 时,f(x) 的值。我们来举个例子吧,8 项的多项式:

f(x)=a0+a1x+a2x2++a7x7

考虑奇偶分治,把式子按照下标的奇偶性分成两坨:

f(x)=(a0+a2x2+a4x4+a6x6)+(a1x1+a3x3+a5x5+a7x7)=(a0+a2x2+a4x4+a6x6)+x(a1+a3x2+a5x4+a7x6)

分别对奇偶项建立新函数:

g(x)=a0+a2x+a4x2+a6x3,h(x)=a1+a3x+a5x2+a7x3

则显然有:

f(x)=g(x2)+xh(x2)

好了,现在就可以代入 ωnk 了:

f(ωnk)=g((ωnk)2)+ωnkh((ωnk)2)=g(ωn2k)+ωnkh(ωn2k)=g(ωn2k)+ωnkh(ωn2k)

好像还是看不出来什么,再代入个 ωnk+n2 试试?推导过程省略,跟上面差不多,我们能得到:

f(ωnk+n2)=g(wn2k)ωnkh(ωn2k)

好了,现在我们就完全能看出来了,求出 g(ωn2k),h(ωn2k) 之后我们就能一次处理处两个函数值!而处理这俩函数的过程又是一次递归的过程!这下找到了方向了,我们对于每个函数值,需要代入 n 个不同的值,每次会把多项式长度缩减 12,所以最终复杂度就是 O(nlogn)

值得注意的是,分治的时候需要保证每次分治两边长短相同,换句话说,需要 n=2m,mN。如果原多项式不满足的话,就用 0 补就好了。

好了,现在我们能把系数表示法换成点值表示法了,愉快地把函数值相乘,然后呢?然后我们就需要把点值表示法换成系数表示法了。这一过程通常被称为 IFFT,即逆 FFT。

考虑原本的多项式是 f(x)=i=0n1aixi,而现在我们已知 yi=f(ωni),i[0,n)Z,现在要求 {a0,a1,,an1}。考虑设:

A(x)=i=0n1yixi

现在我们就要在 A(x) 上面搞搞事了。

考虑将 ωni 分别代入 A(x),则有:

A(ωnk)=i=0n1f(ωni)ωnik=i=0n1ωnikj=0n1ajωij=i=0n1j=0n1ajωi(jk)=j=0n1aji=0n1ωi(jk)

S(ωna)=i=0n1(ωnai),则当 a0(modn) 时,显然 S(ωna)=n(因为 ωna=1 嘛)。

而当 a0(modn) 时,考虑经典 trick 错位相减:

S(ωna)=i=0n1(ωnai)ωnaS(ωna)=i=1n(ωnai)S(ωna)=ωnanωna0ωna1=0

综上,我们有:

S(ωna)={na0(modn)0a0(modn)

带回原式:

A(ωnk)=j=0n1ajS(ωnjk)=akn

非常神奇,如果令 bk=ωnk,则 A 的点值表示法就是:

{(b0,a0n),(b1,a1n),,(bn1,an1n)}

综上,我们取单位根为其倒数,然后对 A 做一遍上述的 FFT 过程,再除以 n 就得到了系数表示法。

递归版实现:code

好的现在您写完了递归版,非常愉快地交了模板题,非常愉快地获得了 100pts,但这一切快乐都在您打开提交记录,按照运行时间排序后结束了,“他们的为啥跑的这么快??”

直觉告诉我们是递归的锅。递归带来了大常数,让本就因为实数运算常数不小的 FFT 雪上加霜。所以我们要考虑把它变成非递归版本。

考虑模拟算法中递归分治的过程:

{0,1,2,3,4,5,6,7}{0,2,4,6}{1,3,5,7}{0,4}{2,6}{1,5}{3,7}{0}{4}{2}{6}{1}{5}{3}{7}

分完了。看不出来什么?考虑把分治前的数和分治后的数都变成二进制表示:

{000,001,010,011,100,101,110,111}{000,100,010,110,001,101,011,111}

这下明显了吧,分治前后的数二进制位是反过来的!那我们只需要提前把数交换到对应的位置,然后模拟递归的过程就好了。

现在的问题是怎么把二进制位给反过来。O(nlogn) 固然是一种方法,不过我们能做到 O(n)。考虑递推,设 revx 表示 x 的二进制位反过来的结果,显然有 rev0=0。而对于一个数 x,如果除去最高位不算,revx=revx22,也就是把 x 右移一位的数反转一下再右移一位。而对于最高位,它取决于 x 的奇偶性,所以有:

revx=revx22+(xmod2)2k1

其中 k 是二进制表示下的位数。

非递归版实现:code

NTT/快速数论变换

前置知识:原根

刚刚我们看到了 FFT,这里还有一种思路类似的 NTT,是 FFT 在数论基础上的实现。由于 FFT 涉及到大量的实数运算,丢精度,速度慢就成为了不可避免的问题。而 NTT 是实现在数论基础上的,运算都是整数,准确度和速度都会更快,但时间复杂度依然是 O(nlogn)

这俩的基本思路都差不多,而我们的关键就是在数论领域中找出一个东西来代替单位复根。我们发现,对于质数 p=qn+1(n=2m),它的原根 g 恰好就满足刚刚所说的性质:

gqn1(modp)

而如果我们把 gq 记作 gn,会发现它也有类似的性质,如:

gnn1(modp),gnn21(modp)

然后就结束了,只需要把 FFT 里的单位复根扣掉换成原根就行了。常见的质数原根:

p=998,244,353=7×17×223+1,g=3

其余的质数可以参考求原根的方法考场现求。

实现:code

分治 FFT/NTT

给出序列 g1n,求出序列 f0n,其中:

f0=1,fi=j=1ifijgj

答案对 998,244,353 取模。

能看到很明显的卷积特征,但很遗憾,我们不能直接进行一个积的卷,因为对于 fi 来说,它的值是依赖以前求出来的值的,换句话说,我们要求在线的卷积。(这也是为什么这个算法在国外有时也被称为在线卷积)

考虑参考 cdq 分治的思路,递归处理区间。先处理左区间,然后处理左区间对有区间的贡献,最后处理右区间,这样能保证每次卷积需要的值都已经被求出了。

具体来讲,对于当前区间 [l,r),我们先递归求出 [l,mid)f 的值,然后将 flmid1g0rl1 给卷起来,这样能得到左边对右边 fmid,r1 的贡献。之后递归处理右区间 [mid,r) 即可。时间复杂度:

T(n)=2T(n2)+O(nlogn)=O(nlog2n)

实现:code

注意卷积之前要补 0 清空而不是不管!

MTT

任意模数 NTT,不会。

多项式牛顿迭代

给出多项式 g(x),已知存在一个多项式 f(x),满足:

g(f(x))0(modxn)

求出模 xn 意义下的 f(x)

考虑倍增的思路。首先考虑边界,对于 n=1 的情况,我们需要单独求出 [x0]g(f(x))=0 的解。然后对于其余的 n,我们考虑先递归计算 n2 的情况,假设得到在模 xn2 意义下的解是 f0(x),则现在我们的目标就是要用 f0(x) 表示 f(x)

考虑将 g(f(x))f0(x) 处泰勒展开,则原同余方程可化为:

i0g(i)(f0(x))i!(f(x)f0(x))i0(modxn)

其中 g(i) 表示 gi 阶导。注意到 f(x)f0(x) 这个式子的最低非 0 项次数最低是 n2,因为在这之前它们都一样,所以有:

i2:(f(x)f0(x))i0(modxn)

这样,原同余方程又能化为:

g(f0(x))+g(f0(x))(f(x)f0(x))0(modxn)

简单的代数变化:

f(x)f0(x)g(f0(x))g(f0(x))(modxn)

这下式子有了,至于怎么求,怎么用,且听下文分解。

多项式求逆

给定一个多项式 f(x),求出一个多项式 g(x) 满足:

f(x)g(x)1(modxn)

系数对 998,244,353 取模。

考虑套刚刚的多项式牛顿迭代。为了避免跟上文的符号矛盾,我们记待求逆的函数为 h(x),求逆结果函数为 f(x),则有:

g(f(x))=1f(x)h(x)0(modxn)

套牛顿迭代的式子有:

f(x)f0(x)1f0(x)h(x)1f02(x)(modxn)f0(x)(2f0(x)h(x))(modxn)

注意这里求导的时候把 h(x) 当成常数来做了,然后就结束了,时间复杂度:

T(n)=T(n2)+O(nlogn)=O(nlogn)

实现:code

注意,在实现牛顿迭代的时候,类似分治 FFT,卷积之前一定要记得清空不用的!

多项式开根

给出一个 n1 次多项式 A(x),找出一个模 xn 意义下的多项式 B(x) 使得

B2(x)A(x)(modxn)

系数对 998,244,353 取模。

依然考虑牛顿迭代。类似求逆的推导过程,我们依然记 h(x) 为待开根函数,f(x) 为答案,则有:

g(f(x))=f2(x)h(x)0(modxn)

还是套牛顿迭代的式子:

f(x)f0(x)f02(x)h(x)2f0(x)(modxn)f02(x)+h(x)2f0(x)

多项式求逆就可以做不带余数的除法了。类似求逆,时间复杂度 O(nlogn)

实现:code

多项式求导/积分

这个比较简单了,主要是一些公式。

首先由于求导和积分的线性性:

(f(x)+g(x))=f(x)+g(x)(cf(x))=cf(x)(f(x)+g(x))=f(x)+g(x)cf(x)=cf(x)

所以对于多项式 F(x)=i=0n1aixi,它的导数和积分分别为:

F(x)=i=1n1aiixi1F(x)=i=0n2aixi+1i+1

可以 O(n) 求解。

还有一些比较常用的求导公式:

(F(x)G(x))=F(x)G(x)+F(x)G(x)(F(x)G(x))=F(x)G(x)F(x)G(x)G2(x)(G(F(x)))=G(F(x))F(x)

更多的东西比如积分公式啊,更多的求导公式啊,常见的函数的积分导数啊,建议大概学一下微积分。

多项式带余除法

给出一个 n 次多项式 F(x) 和一个 m 次多项式 G(x),求出多项式 Q(x),R(x) 满足以下条件:

  • Q(x) 次数为 nmR(x) 次数小于 m
  • F(x)=Q(x)G(x)+R(x)

系数对 998,244,353 取模。

刚刚其实我们也做过分数运算(在牛顿迭代那一块),但这里不一样的是,题目还要求求出 R(x),即余数。这不太好办,所以考虑消去 R(x) 的影响。

有个很妙的思想 (不知道咋想到的) ,考虑构造多项式 FR(x)

FR(x)=xnF(1x)

容易想到 FR(x) 的实质其实就把系数反转一下。顺着这个思路,将带余除法的等式两边同时乘上 xn 并把函数里的自变量都改为 1x 有“

xnF(1x)=xnmQ(1x)xmG(1x)+xnm+1xm1R(1x)FR(x)=QR(x)GR(x)+xnm+1RR(x)

诶,这个式子就特殊了,注意到只有 RR(x) 这一项前面有一个 xnm+1,那我们只需要把上式放在模 xnm+1 意义下不就把 RR(x) 干掉了吗,即:

FR(x)QR(x)GR(x)(modxnm+1)

问题是,这会不会对 QR(x) 造成影响导致我们求出的值不精确呢?显然不会,因为 QR(x) 的次数仅有 nm,小于模数的 nm+1 次,所以 QR(x) 并不会受到影响。

这样求个逆就能把 Q(x) 求出来了。求出来之后再反代回去就有 R(x) 了。时间复杂度 O(nlogn)

实现:code

多项式 ln

给出 n1 次多项式 A(x),求一个模 xn 意义下的多项式 B(x) 满足:

B(x)lnA(x)(modxn)

系数对 998,244,353 取模。

考虑设 G(x)=lnx,则原题相当于求 G(F(x)),考虑给这玩意求个导:

(G(F(x)))=G(F(x))F(x)

由于,

(lnx)=1x

则上式为:

(G(F(x)))=F(x)F(x)

这样我们就能求出带求多项式的导数了,之后只需要积分回来即可:

F(x)F(x)

这样直接求导+求逆+积分这题就做完了。时间复杂度 O(nlogn)

实现:code

多项式 Exp

给出 n1 次多项式 A(x),求一个模 xn 意义下的多项式 B(x) 满足:

B(x)eA(x)(modxn)

系数对 998,244,353 取模。

考虑多项式牛顿迭代法。记 A(x)h(x)B(x)f(x),则有:

g(f(x))=lnf(x)h(x)0(modxn)

套式子:

f(x)=f0(x)lnf0(x)h(x)1f0(x)=f0(x)(1lnf0(x)+h(x))

求个 ln,再加加减减后跟其他的乘起来就完事了。时间复杂度 O(nlogn)

实现:code

posted @   zhiyangfan  阅读(184)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示