多项式全家桶笔记整理(完善中)

Part 0 泰勒展开的推广&多项式牛顿迭代

§ 0.1 记号和约定

为了不在下文引起混淆,这里简述一下这篇文章使用的记号:

  • f(x) 或者 F(x) :一个形式幂级数,此处的 x 是一个形式记号。
  • F(f(x)) :两个多项式的复合。
  • F[f(x)] :一个自变量为多项式的函数。此处的 F 的自变量是多项式,而不是实数。而 x 仍然是一个表示多项式系数位置的形式符号,没有实际意义。为了体现出它与复合函数的区别,这里使用方括号和手写体。
  • f(x) :多项式 f(x)普通导数,就是高数教材中的导数定义,但是暂时忽略函数收敛与否。区别于下文会提到的形式导数。
  • f(x) :多项式 f(x)普通积分,含义同上。区别于下文中的形式积分。
  • f(xm) :将多项式每一系数的下标翻 m 倍后得到的新多项式,并不是复合函数

接下来我们会讨论如何求出 F[f(x)] 的导数,并尝试将其泰勒展开。

§ 0.2 混账,你导了甚么!

第一个遇到的问题就是:我们该如何定义 R[[x]] 上的导函数。

要定义导函数,就需要定义微分,同时微分建立在无穷小量上,而要定义无穷小量,就需要先定义极限

可惜的是,我等凡人实在是难以想象一个 R[[x]] 上的极限该怎么定义。所幸,我们可以耍耍小聪明,绕过这一个障碍。

观察一个以多项式为自变量的函数 F[f(x)] ,在OI中,它的解析式一般也是人畜无害的多项式形式,长成这样:

F[f(x)]=k0+k1f(x)+k2f2(x)++knfn(x)

因为这里不存在针对 f(x) 的代入求值,最多也就是关于 x 求导数,所以多项式 f(x) 的存在更加接近于一个 “常量” 而非 “函数”,又考虑到多项式们有和实数几乎完全一致的加减乘运算,我们可以直接把一个多项式 f(x) 视作一个实数

F[X]=k0+k1X+k2X2++knXn

现在就是我们的小聪明了:直接照抄实数域上的导函数定义,捏一个 F 的 “导函数” 出来!

F[X]=k1+2k2X+3k3X2++nknXn1

我们暂且把这个东西叫做 “形式导数”,尽管直接跳过了极限和微分,我们仍然可以由它开始一步步打通泰勒展开的定义。

§ 0.3 噫!好!我展了!

回顾高等数学,实数域中泰勒展开的由来无非是挑选一个点,强制令新函数在这一点上的各阶导数和原来的函数相等。而且泰勒展开还有一个特点:如果被展开的函数是一个多项式函数或者能用幂级数表达的函数,其正确性的证明可以完全不依靠微分和无穷小量来得出。

如果有人想看的话我就去补写一下证明qwq
现在实在是懒得搞了

那么,既然多项式环有着和实数几乎一致的加减乘(导数需要的三种运算)和数乘运算(泰勒展开需要的额外运算),那么就算把实数版本的证明中的 x 改为多项式而不是实数,证明也依然成立。因此没有理由相信泰勒展开在多项式环上不能用。

换句话说,只要有了导数的形式,对多项式函数和指对函数泰勒展开就是正确的。

以自然底数为底的指数、对数函数可以写成无穷项幂级数的形式,所以它们也可以包含在内。

如果被展开函数是无穷多项的多项式函数,那么的确会用到极限。

但是因为求极限的对象是无穷多个多项式的加和,取到极限的变量仍是一个实数,我们仍然不需要定义 R[[x]] 上的极限。

正好,我们的 F 也有着多项式的形式,这就为我们创造了机会,可以再次跳过微分,直接捏出一个新的泰勒展开:

F[X]=k=0+F(k)[X0]k!(XX0)k

接下来可以把 X 换成我们更熟悉的多项式 f(x) .

F[f(x)]=k=0+F(k)[f0(x)]k!(f(x)f0(x))k

好了!我们做完了!

虽然可能不甚严谨,但是管他呢,用就完了。

这里有一篇博客提供了另一种理解:多项式牛顿迭代(Freopen@csdn.com)

简单来说,我们挑选所有能让 f0(x) 收敛的实数 x ,把它代入 f0(x) 可以求出一些值,于是就可以把 f0(x) 视作一个实数变量,同时自然把 F[f(x)] 变成了一个以实数变量 f0(x) 为自变量的函数,对其强行进行泰勒展开一定是正确的。

接下来再对所有的能让 f(x) 收敛的实数 x 代入其中,由于 f(x)f0(x) 的值域都为 R ,多项式表达式的差异可以大致等同于函数值(F 函数的自变量)的差异。

综上,我们就得到了这种泰勒展开的正确性。

一般会使用 ddf(x)F[f(x)] 来表示多项式为变量的函数的导数,但是这种写法会让人误以为所比的无穷小量是 “f(x) 的函数值” 而非 “f(x) 的解析式结构”。

而其实前后有着很大的区别,前者仍然是实数域上的导数,后者才是真正属于多项式环的微分。

不过笔者没有想出更好的记号来,下文中仍会沿用这种写法。

§ 0.4 开始迭代

多项式牛顿迭代可以解决这样的问题:

已知多项式 g(x) ,求满足这种条件的多项式 f(x),系数对 998244353 取模。

g(f(x))0(modxn)

换句话说就是求零点。

这里多项式取模的意思就是只保留次数最低的 n 位,后面的东西不要了。

考虑用倍增实现。每一轮通过模 xn2 意义下的解 f0(x) 得到模 xn 意义下的解 f(x) .

第一轮中,我们需要求出 g(x) 在模意义下的根,这个单独处理。

如何迭代计算模 xn 意义下的答案呢?我们可以用刚才推导出的泰勒展开,把 G(把 g 视作一个自变量为多项式的函数)在 f0(x) 处展开:

G[f(x)]=k=0+G(k)[f0(x)]k!(f(x)f0(x))k

然后,观察到 (f(x)f0(x))kk>2 情况下前 n 位都是零,这样我们就可以舍弃掉后面的东西了

G[f(x)]0(modxn)k=0+G(k)[f0(x)]k!(f(x)f0(x))k0(modxn)G[f0(x)]+G[f0(x)]×(f(x)f0(x))(modxn)G[f0(x)]+G[f0(x)]f(x)G[f0(x)]f0(x)(modxn)

当然还有另一种方式理解为什么要用倍增实现:

普通的牛顿迭代每次可以把精度翻倍,那么对应到多项式上,每次确定的项数也会翻倍。

化简式子

G[f0(x)]+G[f0(x)]f(x)G[f0(x)]f0(x)0(modxn)

得到

f(x)f0(x)G[f0(x)]G[f0(x)](modxn)

诶嘿,正好是经典牛顿迭代法的形式。难道不应该是吗

这就做完了,时间复杂度分析如下:

已知多项式求逆的复杂为 O(nlogn) 求导和加减的复杂度是 O(n) ,那么就有

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

最终的复杂度是 O(nlogn) .

但是实际跑起来常数巨大无比。

Part 1 多项式乘法逆

§ 1.1 常规方法

在很多多项式算法(例如刚刚讲到的牛顿迭代)中都需要用一个多项式在模 xn 意义下除以另一个多项式。这种操作等价于乘上另一个多项式的乘法逆元,现在我们来求这个东西。

根据刚才牛顿迭代的启发,我们用倍增解决这个问题,已知已经求得了模 xn/2 意义下 f(x) 的逆元。

f01(x)f(x)1(modxn/2)

又因为目标答案长这样:

f1(x)f(x)1(modxn/2)

两式相减,得到:

f1(x)f01(x)0(modxn/2)

式子两边平方,模数也平方,依然能整除:

f2(x)2f01(x)f1(x)+f02(x)0(modxn)

然后式子把他自己收拾好了:

f2(x)f01(x)(2f1(x)f01(x))(modxn)

两边同时乘上 f(x) :

f1(x)f01(x)(2f(x)f01(x))(modxn)

递归计算就做完了!采用本式计算乘法逆的话每轮会进行两次 NTT 和一次 INTT .

模板代码(含NTT)

namespace poly{
	const LL gn = 3;
	const LL gni = 332748118;
	int rev[maxn<<2],now_rev;
	void ReArrange(LL *F,int N)
	{
		if(now_rev != N)
		{
			rev[0] = 0; now_rev = N;
			for(int i=1; i<N; i++) rev[i] = (rev[i/2]/2) + (i&1)*N/2;
		}
		for(int i=0; i<N; i++) if(rev[i] > i) swap(F[rev[i]], F[i]);
		return;
	}

	void NTT(LL *F,int N,int inv)
	{
		ReArrange(F,N);
		for(int len=1; len<N; len<<=1)
		{
			LL stp = qpow((inv == 1) ? gn : gni, (mod-1)/(len<<1));
			for(int st=0; st<N; st+=(len<<1))
			{
				LL omega = 1;
				for(int i=0; i<len; i++)
				{
					LL U = F[st+i], V = M(F[st+len+i], omega);
					F[st+i] = madd(U,V); F[st+len+i] = msub(U,V);
					omega = M(omega,stp);
				}
			}
		}
		if(inv == -1)
		{
			LL Ninv = qpow(N,mod-2);
			for(int i=0; i<N; i++) F[i] = M(F[i],Ninv);
		}
		return;
	}

	LL pinv[maxn];
	void Inverse(LL *f1,int n)
	{
		int K = 1; while(K<n) {K = K<<1;} n = K;
		memset(tmp,0,sizeof(tmp));
		pinv[0] = qpow(f1[0],mod-2);//求出乘法逆元的第一项
		for(int len=2; len<=n; len<<=1)
		{
			K = len<<1;//两个多项式相乘后的长度
			memcpy(tmp, f1, sizeof(LL) * len); //临时复制过来f(x)
			memset(tmp+len, 0, sizeof(LL) * len); //后面补零
			NTT(tmp,K,1); NTT(pinv,K,1);
			for(int i=0; i<K; i++) pinv[i] = M(pinv[i], msub(2, M(tmp[i], pinv[i]))); //计算刚刚得出的式子
			NTT(pinv,K,-1);//变换回来
			memset(pinv+len, 0, sizeof(LL)*len); //取模,去掉多余的部分
			//此轮循环结束后,pinv[]中存储的由 f_0^{-1}(x) 变为 f^{-1}(x)
		}
		memcpy(f1, pinv, sizeof(LL)*n);
		return;
	}
}

§ 1.2 哇塞,这居然可以

既然我们有了牛顿迭代,为什么不用用它呢?

好!那么我们先构造出这么一个函数:

F[g(x)]=1g(x)f(x)

可以看出来, F 在模 xn 意义下的零点就是我们要求的乘法逆元。

接下来列出牛顿迭代的式子:

gt+1(x)gt(x)F[gt(x)]F[gt(x)](modxn)

带入解析式:

gt+1(x)gt(x)1gt(x)f(x)1gt2(x)(modxn)

整理一下:

gt+1(x)2gt(x)gt2(x)f(x)(modxn)

所有带分母的项都消去了,可以用 NTT 计算了。

等等,这不就是上面的那个式子吗

数学真奇妙

Part 2 多项式除法(取模)

§ 2.1 十分神仙的技巧

多项式除法,就是给定 f(x),g(x) 求多项式 Q(x),R(x) ,使得

f(x)Q(x)g(x)+R(x)(modxn)

商和余数的英文分别叫 quotient 和 remainder,所以一般用 Q(x)R(x) 表示。

我们观察到,如果 R(x)0(modxn) 的话,只需要求逆就可以了。而对于 R(x)0(modxn) 的情况,我们需要想办法移除掉它的影响。

而移除的方法就是把 f(x),g(x) 的系数反向。

形式化地,构造变换

f¯(x)=xnf(1x)

就可以把多项式的系数反向。

接下来对除法的式子进行这个变换(n=degf,m=degg

f(x)g(x)Q(x)+R(x)(modxn)xnf(1x)xng(1x)Q(1x)+xnR(1x)(modxn)

我们又知道,degQ=degfdegg=nm 以及 degRdegg1 ,所以就有

xnf(1x)(xmg(1x))(xnmQ(1x))+xnR(1x)(modxn)f¯(x)g¯(x)Q¯(x)+xnm+1R¯(x)(modxn)

这个时候,奇妙的事情出现了,最后余数项 xnm+1R(x) 的最低次数为 nm+1 ,正好和最高次数为 nm 的商 Q(x) 错开一位,我们把整个式子拿到模 xnm 意义下,就可以完全消去余数的影响:

f¯(x)g¯(x)Q¯(x)(modxnm)f¯(x)g¯(x)Q¯(x)(modxnm)

现在除法变成了普通的多项式求逆,我们来总结一下步骤:

  1. f,g 反向。
  2. f¯,g¯ 模上 xnm .
  3. 这个时候计算 f¯(x)/g¯(x) .
  4. 把结果模上 xnm反向,得到 Q(x) .
  5. 用结果乘一开始没有被反向、取模过的 g .
  6. 用一开始没有被反向、取模过的 f 减去上一步的乘积,得到 R(x) .

真搞不懂这个技巧是怎么想出来的=

模板代码(需要刚才的 NTT 和 Inverse)

	LL copy1[maxn],copy2[maxn];
	void Divide(LL *f1,LL *f2,int n1,int n2,LL *Q,LL *R)
	{
		memcpy(copy1,f1,sizeof(LL)*n1); memcpy(copy2,f2,sizeof(LL)*n2); //备份
		reverse(f1,f1+n1); reverse(f2,f2+n2); //两个乘数翻转
		int qlen = n1-n2+1; //模数的次数
		if(qlen < n2) memset(f2+qlen, 0, sizeof(LL) * (n2-qlen));

		Inverse(f2,qlen); //求除数的乘法逆元
		Multiply(f1,f2,qlen,qlen); //f1乘上f2的逆元
		memset(f1+qlen, 0, sizeof(LL) * qlen); //
		reverse(f1, f1+qlen); //乘积反转后就是商
		memcpy(Q, f1, sizeof(LL) * qlen); //因为下面要用到f1,这里先复制一遍

		memcpy(f2, copy2, sizeof(LL) * (n2)); //用copy2之前先把f2复制回来
		Multiply(copy2, f1, n2, qlen); //用除数乘上商
		for(int i=0; i<n2-1; i++) R[i] = msub(copy1[i],copy2[i]); //用被除数减去上一步的得数得到余数
		memcpy(f1, copy1, sizeof(LL) * n1); //最后把被除数复制回来
		return;
	}

Part 3 多项式指对函数

§ 2.1 指对函数的新定义

累了,直接上公式:

expf(x)=k=0+1k!fk(x)ln(1+f(x))=k=0+(1)n+1k!fk(x)

本质上还是之前的泰勒展开,其中对数函数因为 0 处无定义,选择在 1 处进行展开。

§ 2.2 导过去,积回来

已知多项式 f(x) ,求多项式 g(x) ,使得

g(x)=ln(f(x))

其中 ln 函数的定义如上。

如果直接按照新定义中的方法强行计算,我们会发现必须把全部的无穷项都计算出来才能得到正确答案,所以我们需要换一种方法。

为什么要计算无穷项?因为上面的定义式每多一项,多项式的每一个系数都会发生改变。

那就换一个方法吧。

既然把 ln 函数化成初等形式只需要求导,那就对 ln 关于 f形式导数,得到了这个式子:

ddxln(f(x))=1f(x)

右边是很好计算的,那就积回去!

1f(x)df(x)+C=ln(f(x))

问题出现了,我们无法关于 f(x) 积分 1f(x) ,也不知道积分常数 C 应该是多少。

这该怎么解决呢?我们发现,刚刚求不出结果的原因是我们把 ln 看做了一个以多项式为自变量的函数。

而现在我们来换一种思路:把 ln 也看成一个特殊的多项式,把 ln(f(x)) 看成求 ln(x)f(x) 这两个多项式的复合

这样,就可以关于实数 xln(f(x))普通导数

ddxln(f(x))=1f(x)f(x)

仿照上面,用积分表示左边:

ln(f(x))=f(x)f(x)dx+ln(f(0))

化简成功!

经验:多项式的操作十分灵活,既可以关于多项式求导(形式导数),也可以关于实数求导(普通导数),两种方法都可以用来化简表达式。

模板代码(需要上文的 Inverse)

	void Derivate(LL *F,int N)
	{
		for(int i=0; i<N-1; i++) F[i] = M(F[i+1],i+1);
		F[N-1] = 0;
		return;
	}

	void Intergrate(LL *F,int N)
	{
		for(int i=N-1; i>0; i--) F[i] = M(F[i-1],mcon::inv[i]);
		F[0] = 0;
		return;
	}

	LL tmpi[maxn<<1];
	void Ln(LL *F,int N)
	{
		memcpy(tmpi,F,sizeof(LL)*N);
		Inverse(tmpi,N);
		Derivate(F,N);
		Multiply(F,tmpi,N,N,1);
		Intergrate(F,N);
		return;
	}

§ 2.3 一切偷懒,终将绳之以法!

有一点需要特别注意,后面的这一部分是积分常数,因为这一项的存在,必须要求

[x0]f(x)=1

这是为什么呢?

伟大的 OI-wiki 曾经曰过:

首先,对于多项式 f(x) ,若 lnf(x) 存在,那么由其定义,其必须满足 [x0]f(x)=1

不过 OI-wiki 并没有给出进一步的解释,这让我这个蒟蒻一头雾水:为什么没有符合规定的常数项就不能计算了?

然后我翻到另一篇文章如是说:

[x0]f(x)1 ,那么结果的常数项不收敛。

这让我更加混乱了

实际上这几句话的意思是:若常数项不是 1,那么得数的常数项将无法用有理数取模表示

具体来说,根据刚才的推导,得数的常数项应为

ln([x0]f(x))

而根据 e 的超越性,我们知道 y=lnx 只有在 x=0 时才能取到有理数值,其余值均为无理数,也就没有办法取模。

这里有更加详细的证明:题解P4725(Kinesis@luogu.com.cn)

§ 2.4 多项式指数函数

已知多项式 f(x) ,求 g(x)=exp(f(x)) ,其中 exp 函数的定义同 §2.4 。

这一部分十分容易推导,用牛顿迭代法即可。构造函数如下:

F[g(x)]=ln(g(x))f(x)

此函数的零点即为所求的 exp(f(x)) .

接下来使用牛顿迭代:

gt+1(x)gt(x)F[gt(x)]F[gt(x)](modxn)

由于 F[g(x)]=1g(x) ,可得:

gt+1(x)gt(x)gt(x)(ln(gt(x))f(x))(modxn)

gt+1(x)gt(x)(1ln(gt(x))f(x))(modxn)

时间复杂度为 O(nlogn) .

模板代码(需要上文的 Ln)

	LL f1[maxn<<1],f2[maxn<<1];
	void Exp(LL *F,int N)
	{
		int K=1;
		while(K<N) K<<=1;
		memset(f1,0,sizeof(f1));
		memset(f2,0,sizeof(f2));
		f1[0] = 1;
		for(int len=2; len<=K; len<<=1)
		{
			int len2 = len<<1;
			memcpy(f2,f1,sizeof(LL)*len);
			Ln(f2,len);
			for(int i=0; i<len; i++) f2[i] = madd(msub(0,f2[i]),F[i]);
			f2[0]++;
			NTT(f1,len2,1); NTT(f2,len2,1);
			for(int i=0; i<len2; i++) f1[i] = M(f1[i],f2[i]);
			NTT(f1,len2,-1);
			memset(f1+len,0,sizeof(LL)*len);
		}
		memcpy(F,f1,sizeof(LL)*N);
	}

这里再来分析一下为什么每轮循环中需要把数组的长度开到 2×deggi(x) .

首先,我们知道在式子

gi(x)gi1(x)(1ln(gi1(x))f(x))(modx2i)

中,f(x) 高于 x2i 的项都是没有意义的,因为它最低的贡献也在 x2i 的位置,最终会全部被模掉。同理,ln(gi1(x)) 也只需要保留前 2i 项。

对应到代码中,我们只需要把 F[x] 的前 len 项复制过来用以相乘,求 ln(gi1(x)) 时也只需要求到长度为 len 为止。

另外,外层用来相乘的 gi1(x) 的次数为 2i1 ,所以最终结果(未取模)的最高次数是 1.5×2i ,而为了凑齐进行 NTT 所需的整次幂,需要把长度补齐到 2×2i .

对应到代码上,就是相乘是数组长度开到 len<<1 .

§ 2.5 多项式快速幂

Part 4 多项式开平方根

posted @   HMSF  阅读(90)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
点击右上角即可分享
微信分享提示