多项式简明百科全书

0、写在前面

为了方便本文的描述,定义如下内容:

我们说f是一个次数界为n(下文不严谨地简称为n次)的多项式,是指f是一个下标集合为0,1,2,...,n1的数组,并代表:

i=0n1fixi

这个多项式。

我们用f(x0)代表多项式fx0处的值。

1、多项式乘法

利用FFT就可以将多项式乘法的复杂度优化到O(nlogn)。由于其讲解较为繁琐并且已逐渐普及~~(想必读者也不是来学这个的)~~,因此略过FFT以及同样普及的NTT

至于MTT,我们可以将系数拆成32768a+b的形式缩小值域,将一个多项式拆为两个。做四次卷积以后,将得到的答案合并并取模即可。

当然MTT还有利用FFT模数计算再用中国剩余定理合并的做法,但第一种做法更加直观。

2、分治FFT

有两种分治FFT,先来讲比较简单的第一种。

如果我们有一个长度为n的数组a,并且希望求:

i=0n1(x+ai)

直接按照暴力计算,是O(n2)的(因为每一个只有两项,所以一次暴力乘法只有O(n))。如果我们把乘法用FFT来实现,我们就得到了O(n2logn)优秀复杂度。

怎么做才会更快呢?

分治往往能带来更加优秀的复杂度。直接利用FFT之所以慢,是因为相乘的两个多项式次数相差太大,这会非常浪费。

那我们不妨考虑先计算左半部分和右半部分的积。这是两个O(n)次的多项式,直接用FFTO(nlogn)的时间内相乘即可。那么我们的递归式就是:

T(n)=2T(n/2)+O(nlogn)

解得T(n)=O(nlog2n),复杂度得到了很大优化。

那么再来讲讲dark的第二种分治FFT

你有一个下标集合为{1,2,...,n}的数组f,你想要求一个下标集合为{0,1,2,...,n}的数组g,满足:

gi=j=1ifjgij

边界条件g0=1

看上去直接求好像有点困难,那么我们来考虑分治。我们把数组g拆为前后两半,显然后一半对前一半是没有影响的,直接递归求解前一半。求解完毕以后我们来计算前一半对后一半的贡献,这时我们直接根据上面的式子FFT即可,因为此时前后的影响已经被我们用分治消除了。最后我们递归继续后一半的求解。

综上所述,递归式为:

T(n)=2T(n/2)+O(nlogn)

解得T(n)=O(nlog2n),复杂度较优秀。

P.S.第二个问题其实有复杂度更优秀的解法,可以参见下面的“多项式求逆”。但是分治的思想更加直观,并且有的类型并不能利用“多项式求逆”来做,因此分治FFT的作用还是很大的。

3、多项式倍增(思想)

倍增是接下来对许多多项式问题求解的重要方法。

在多项式中可能有一些我们要求的多项式次数是无限的,那么一般我们只需要知道它的前n项,即modxn的结果即可。直接求解或许也比较困难,此时可以考虑倍增。

我们先用简单的方法求出modx的平凡情况,然后就只需要考虑如何从modxk 的结果f^推到modx2k的结果f即可。一般来说,利用(ff^)20(modx2k)这个性质就可以得到很方便的求解方法。

4、多项式求逆

多项式求逆是这样一个问题,已知一个n次多项式f,我们希望求一个n次多项式g=f1,即:

fg1(modxn)

显然要求f的常数项不为0。那么在modx意义下,直接求得g0=f01

现在我们考虑从modxk的结果g^推出modx2k的结果g。我们有:

(gg^)20(modx2k)

把平方展开,得到:

g22gg^+g^20(modx2k)

这个式子好像有点dark,但是我们已经知道fg1(modx2k),因此我们对等式两边同时乘以f,得到:

g2g^+fg^20(modx2k)

移项,得:

g2g^fg^2(modx2k)

就可以直接进行倍增。递归式为T(n)=T(n/2)+O(nlogn),解得T(n)=O(nlogn)

多项式求逆也是很多多项式问题的基础,在接下来的几种多项式黑科技中起到了很大的作用。

那么再来举一个应用多项式求逆的例子,就是在之前的“分治FFT”里提到的第二个问题。为了保持完整我们再陈述一遍:

你有一个下标集合为{1,2,...,n}的数组f,你想要求一个下标集合为{0,1,2,...,n}的数组g,满足:

gi=j=1ifjgij

边界条件g0=1

我们先想办法把上面的式子化成普通的卷积形式。我们把j的枚举范围从1,2,...,i改为卷积形式的0,1,2,...,i,此时我们发现多计算了f0gi这一项。那么我们不妨令f0=0,我们就消去了这一项的贡献。同时注意到作为边界的g0如果直接用这个卷积形式计算会得到0,那么我们可以强制将结果加1。经过这番操作卷积的答案就为g了。于是我们得到了非常优美的式子:

g=fg+1

我们移项,得到:

(1f)g=1

利用多项式求逆求出(1f)1(modxn+1),在两边同时乘上,于是得到了:

g11f(modxn+1)

由于我们只要知道g的前n+1项,因此modxn+1是没有关系的,于是我们就利用多项式求逆在O(nlogn)的时间内解决了这个问题。

5、多项式除法、取模

给你一个n次多项式f,以及一个m次多项式gn>m),我们希望求一个nm+1次多项式q以及一个m1次多项式r,满足:

f=qg+r

那么我们称qf除以g的商,而称rf除以g的余数。

现在我们来考虑如何求qr。我们先定义n次多项式f的一种变换fR

fR=f(1x)xn1

不难看出fR即是将f的系数反转一下。因此我们可以在O(n)的时间内完成这个变换。

那么我们来推式子,我们不妨用1x替换多项式内的x,这样的结果显然依旧正确:

f(1x)=q(1x)g(1x)+r(1x)

接着考虑在两边同乘xn1,得到:

f(1x)xn1=q(1x)xm1g(1x)xnm+r(1x)xm2xnm+1

不难发现我们已经有意为那个变换做好了准备,那么我们将四个多项式都用变换来代替:

fR=qRgR+rRxnm+1

我们考虑消去最后一项,那么不妨把式子放到modxnm+1的意义下。此时最后一项肯定为0,于是就消失了:

fRqRgR(modxnm+1)

我们发现既然qnm+1次的,那么qR也是nm+1次的,modxnm+1对其没有影响。于是我们求gR的逆,就得到:

qRfRgR(modxnm+1)

于是我们就求得了qR,然后简单地再做一次变换就得到了q。此时算r就很方便了,直接计算fqg即可。

下面来讲一个多项式取模的应用——常系数齐次线性递推。

常系数齐次线性递推是指这样一个问题,给你一个数k,接着对于i=1,2,...,k,给出ai,代表递推的系数。再对于i=0,1,2,...,k1,给出fi作为初始值,对于ik,数列f满足:

fi=j=1kajfij

最后给你一个数n,需要求出fnn的范围很大。

显然直接递推是不可行的,我们不妨把思路放的巧妙一些。我们设有一个奇怪的x,其满足xi=fi(有点难理解,你不妨假设x只是一个代号,它根本不是一个数),那么它一定满足:

xk=i=1kajxki

于是我们移项,得到:

xki=1kaixki=0

我们将其当作一个k+1次的多项式来看待,那么如果我们将xn对这个多项式取模,得到了商和余数。因为模多项式为0,因此商乘以这个模多项式的答案没有贡献,我们只关心这个余数。显然余数是一个k次多项式,你只需要知道x0,x1,...,xk1即可得到答案。而数列f的前k项是给出的,因此代入计算就可以了。因此整个过程只需要一个意义为卷积的乘法,以及意义为多项式取模的取模的快速幂就可以在O(klogklogn)的时间内完成这个操作。

upd:更详细的证明可以看这里 (opens new window)

6、多项式求导、积分

这是两个很简单的变换,是为了下面的内容做好准备。

f是一个n次多项式,那么f的导数f就是一个n1次多项式,并且满足:

fi=(i+1)fi+1

直接O(n)计算即可。

f是一个n次多项式,那么f的积分f就是一个n+1次多项式,并且满足:

fi=fi1i

边界条件f0=0

同样O(n)计算即可。

7、多项式ln

事情开始变得诡异起来了。

多项式求ln是这样一个问题,给你一个n次多项式f,满足f0=1,你需要求一个n次多项式g,满足:

glnf(modxn)

看到这个是不是一下子有点懵啊...没关系,我们来冷静分析。函数ln(x)的一个特点就是ln(x)=1x,这就一下子好办了,我们把这个结论照搬到多项式上面去,利用复合函数的求导法则,对上面的等式两边求导,得到:

gff(modxn)

这是不是非常简单啊,直接多项式求逆就得到了g,那么我们再将等式两边积分,就得到:

gff(modxn)

不过还有一个小疑问,求导一次再积分一次不是会丢掉常数项吗,这怎么办呢。其实没关系,我们已经保证f0=1,而ln1=0,因此g的常数项就是0。于是我们就做完了。

8、多项式牛顿迭代、泰勒展开(思想)

接下来我们讲一下多项式牛顿迭代和泰勒展开,为接下来的工作做准备。话说多项式真是强啊什么都能套上去。

先来讲牛顿迭代。

我们知道,如果我们希望在实数域内求得某个处处可导函数f(x)的零点的近似值,我们就可以用牛顿迭代来解决。我们随意选择一个初值,设其为x0,然后开始迭代。每一次从xi迭代到xi+1,我们的精度会得到很大提升,迭代式子为:

xi+1=xif(xi)f(xi)

其实就相当于xi+1f(x)xi处的这条切线和横轴的交点,可以自己推一下式子证明。

我们将牛顿迭代应用到多项式上,具体来说:

我们给出一个n次多项式f以及一个以多项式为参数,并且结果也是一个多项式的函数G,并希望计算出一个n次多项式g,满足:

G(g)f(modxn)

把问题简单转化一下,就是希望求得Gf的零点了。那么我们依然先求出modx意义下的简单结果,接着套用牛顿迭代的公式。设上一次迭代的答案为g^,我们就得到:

g=g^G(g^)fG(g^)

为什么求导的时候多项式f会消失呢?这是因为G是关于多项式的函数,其导数反映的是当多项式变化时的变化幅度,因此不变的多项式f就被看作是常数项了。

同时我们可能还有一个疑问:牛顿迭代不是用来提高精度的吗?怎么在多项式上应用呢?我们感性理解一下,假设g^modxk意义下的结果,那么迭代式子显然可以得到一个2k次多项式,此时我们给出结论:通过这个牛顿迭代的式子,我们可以得到modx2k意义下的结果。对它的证明将放到泰勒展开的讲解内。

于是我们就可以发现,利用牛顿迭代公式,我们可以做到类似倍增的效果。那么许多看似复杂的问题就可以解决了。

现在来讲一下多项式的泰勒展开。

我们刚刚已经得到了牛顿迭代在多项式上的应用,但是对于它的正确性,我们却是一知半解。幸好我们还有一种可以较简单的进行证明的方法:多项式泰勒展开。同时利用泰勒展开我们还可以获得一些问题求解的思路。

我们已经知道泰勒展开式为:

f(x)=i=0f(i)(x0)i!(xx0)i

我们可以证明其对以多项式为参数的函数也成立,这和一般情况是一样的。那么我们依然考虑之前牛顿迭代所讲的问题,我们以modxk意义下的答案g^代入x0,求modx2k意义下的答案g,那么我们有:

G(g)fG(g^)f+G(g^)(gg^)+...(modx2k)

我们再次利用这个性质:(gg^)20(modx2k),那么泰勒展开式中除了前两项以外都变成了0,于是便不用再考虑后面了,得到:

G(g)fG(g^)f+G(g^)(gg^)(modx2k)

而我们又有G(g)f0(modx2k),于是左边就为0,我们移项得:

G(g^)(gg^)G(g^)f(modx2k)

两边同除G(g^)

gg^G(g^)fG(g^)(modx2k)

再将g^加到右边:

gg^G(g^)fG(g^)(modx2k)

我们此时发现这就是牛顿迭代的公式,于是就完成了对牛顿迭代正确性的证明。当然泰勒展开的作用不只是证明牛顿迭代,我们在做题时可能推出一个关于某个多项式的奇怪式子,如果你能发现它正好是一个常见函数的泰勒展开,就可以用专门求这个函数的方法来解决——例如我们即将要讲的exp

9、多项式exp

多项式求exp是这样一个问题,给你一个n次多项式f,满足f0=0,你需要求一个n次多项式g,满足:

g=ef(modxn)

由于我们已经有了关于ln的经验,看到这个就不要害怕了(都是纸老虎)。

但是直接利用ln求导积分的那一套似乎不可行,因为exp的导数依然是exp。但是我们正好可以利用求ln是简单的这一点,结合刚刚所讲的牛顿迭代来解决这个问题。

我们可以很快把exp转化为这样的问题:求一个n次多项式g,满足:

lngf(modxn)

那么问题转化为求lngf的零点,我们套用牛顿迭代的公式来倍增。因为f的常数项一定是0,因此g的常数项就是1,那么对于已知的modxk意义下的答案g^,我们求modx2k意义下的答案g

g=g^lng^flng^

而我们知道lng^=g^1,因此:

g=g^(1lng^+f)

就可以倍增求得答案了。递归式为T(n)=T(n/2)+O(nlogn),解得T(n)=O(nlogn)

expln结合起来还可以得到多项式快速幂的O(nlogn)算法,具体来说:

给出一个n次多项式f,以及次数k,我们希望求:

fk(modxn)

直接利用快速幂来计算,每次乘法是O(nlogn)的,一共O(logk)次乘法,总复杂度为O(nlognlogk),因为modxn,所以可以选择在每次乘法以后只保留前n项。这是一个优秀的算法,但是下面的做法可以比这更优秀:

假设多项式f的系数不为0的最低次是p,那么我们可以先计算(fapxp)k,最后再简单地乘以apkxpk即可,经过这样的处理,我们就保证了常数项为1lnf就是可以计算的。

那么套用数域上的结论:xk=e(lnx)k,我们得到:

fk=e(lnf)k

于是我们只要先求lnf,再把每一项乘以k,最后进行一次exp即可。如果之前有过处理,就像之前说的一样处理回去。这样的复杂度就是O(nlogn)的。

10、多项式开方

多项式开方是这样一个问题,给你一个n次多项式f,满足数域内存在某个数t,使得t2=f0,你需要求一个n次多项式g,满足:

gf(modxn)

头回生二回熟,我们立刻可以发现这也可以利用牛顿迭代来解决对吧。我们现在是要求g2f的零点,那么modx时的答案就是f0的平方根(我们已保证了这一点可以做到),考虑用牛顿迭代公式进行倍增:

gg^g^2f(g^2)(modx2k)

显然(g^2)=2g^,那么把式子转化成好求的形式:

g12(g^+fg^)(modx2k)

这样做的复杂度也是O(nlogn)

11、多项式多点求值

事情好像更加毒瘤了。

我们可以利用FFTO(nlogn)的时间内求出n次多项式fn个单位复数根处的值。那么我们自然还很好奇这样的一个问题:如果我随意给出n个点,有什么快速的方法可以求得它的值吗?答案是肯定的,利用下面的方法,我们可以在O(nlog2n)的时间内求出一个n次多项式在任意n个点处的值。

首先给出一个简单的结论。回顾之前的多项式除法,对于n次多项式ff(x0)的值就是:

f(x0)=fmod(xx0)

这个应该很好证明,xx0x=x0时值为0,因此它的倍数在求x0时都为0,都没有影响,就只剩下一个常数,那就是答案。

现在的问题,就转化成对于下标集合为{0,1,2,...,n1}的数组a,我们希望快速求得fmod(xai)的值,其中i=0,1,2,...,n1

此时我们不妨考虑一下我们的老朋友分治。能否通过进行一些简单的处理,就能将f变成两个n2次多项式递归求解呢?答案依然是肯定的。我们以左半部分为例,考虑将f变成:

fmodi=0n21(xai)

这样的答案是不变的,因为在x等于左半部分任意的a值时,后面的连乘结果为0,没有影响,因此其倍数也不会有影响,而这式子是一个n2+1次的多项式,f对其取模以后就得到了n2次的多项式。对于右半部分,显然也是一样的。

再考虑如何快速求那个连乘。显然这是一个分治FFT可以解决的问题,我们不妨先用O(nlog2n)的时间预处理出每一层的连乘,接着再做一遍上述分治做法,我们在每一层的复杂度就是O(nlogn),解得T(n)=O(nlog2n)

12、多项式多点插值

我们既然已经知道如何多点求值,自然也会好奇如何进行多点插值。这当然也是可以快速做到的,复杂度也是O(nlog2n)。不过其过程会更加复杂一些。

假设我们已经知道n个互不相同的点值(x0,y0),(x1,y1),...,(xn1,yn1),那么由这n个点插值出来的多项式一定是唯一的。那么我们不妨假设我们可以构造n次多项式i,满足当xxi时值为1,取xj(ji)时值为0,那么显然这个多项式就是:

f=i=0n1iyi

现在来考虑怎么构造它,这其实非常简单,只需:

i=jixxjxixj

发现当xxj(ji)时分子必有一个为0,因此值为0。取xi时分母分子相互抵消,答案就为1。于是我们有:

f=i=0n1yiji(xixj)ji(xxj)

直接根据定义计算显然是O(n2)的,我们来考虑一下怎么做会更快。

我们先来计算ji(xixj)这一部分如何。我们考虑计算出n+1次多项式g,满足:

g=i=0n1(xxi)

这可以用分治FFTO(nlog2n)的时间内解决。那么对于每一个i,我们就是要求:

g(xi)xxi

可是这个式子分子分母都是0啊...这怎么办办...这时我们的救星出现了,它叫做洛必达法则:

对于函数f(x)g(x),如果当xa时,有f(x)0g(x)0,那么有f(a)g(a)=f(a)g(a)

这其实也不难理解,两个趋近于0的数相除,我们用它们的变化率相除作为结果应该很自然。那么我们就可以利用洛必达法则,而(xxi)=1,于是我们对于i,要求的值就是g(xi)。于是我们利用多项式多点求值,求出这n个答案,这样也是O(nlog2n)的。

现在我们已经解决了第一部分,不妨用vi=yiji(xixj)代替原来这一长串。于是我们现在要求的是:

i=0n1(viji(xxj))

又到我们的老朋友出场了,我们再次利用分治来解决问题。我们先递归求解左右两半的子问题,接着以左半部分为例,不难发现我们需要的贡献就是左半部分的答案i=n2n1(xxi),因为所有左半部分都是需要乘以它的。这又是一个分治FFT的形式,于是先用O(nlog2n)的时间处理好,那么我们的每一层就是O(nlogn)的。解得T(n)=O(nlog2n),我们终于解决了问题。

13、多项式三角函数

毒瘤ing...

多项式三角函数是这样一个问题,给你一个n次多项式f,满足f0=0,你需要求两个n次多项式gh,满足:

gcosf(modxn),hsinf(modxn)

cossin是最基本的三角函数,知道它们就可以知道所有三角函数。我们来考虑如何解决这个问题,从优美的欧拉公式入手:

eix=cos(x)+isin(x)

我们将多项式f代入公式中,得到:

eif=cosf+isinf

那么我们可以将其代入复数域内计算,先将所有系数移到虚部再进行exp得到结果,将得到的结果取实部和虚部即可。因为f0=0,因此if0=0,可以进行exp

虽然需要在复数域内计算,但是多项式三角函数应该也是可以取模的,我们考虑对实部和虚部的系数分别取模,把每次卷积运算拆做四次,再将贡献算入结果的实部或虚部即可。