多项式全家桶笔记整理(完善中)
Part 0 泰勒展开的推广&多项式牛顿迭代
§ 0.1 记号和约定
为了不在下文引起混淆,这里简述一下这篇文章使用的记号:
- \(f(x)\) 或者 \(F(x)\) :一个形式幂级数,此处的 \(x\) 是一个形式记号。
- \(F(f(x))\) :两个多项式的复合。
- \(\mathcal{F}[f(x)]\) :一个自变量为多项式的函数。此处的 \(\mathcal{F}\) 的自变量是多项式,而不是实数。而 \(x\) 仍然是一个表示多项式系数位置的形式符号,没有实际意义。为了体现出它与复合函数的区别,这里使用方括号和手写体。
- \(f'(x)\) :多项式 \(f(x)\) 的普通导数,就是高数教材中的导数定义,但是暂时忽略函数收敛与否。区别于下文会提到的形式导数。
- \(\int f(x)\) :多项式 \(f(x)\) 的普通积分,含义同上。区别于下文中的形式积分。
- \(f(x^m)\) :将多项式每一系数的下标翻 \(m\) 倍后得到的新多项式,并不是复合函数。
接下来我们会讨论如何求出 \(\mathcal{F}[f(x)]\) 的导数,并尝试将其泰勒展开。
§ 0.2 混账,你导了甚么!
第一个遇到的问题就是:我们该如何定义 \(R[[x]]\) 上的导函数。
要定义导函数,就需要定义微分,同时微分建立在无穷小量上,而要定义无穷小量,就需要先定义极限。
可惜的是,我等凡人实在是难以想象一个 \(R[[x]]\) 上的极限该怎么定义。所幸,我们可以耍耍小聪明,绕过这一个障碍。
观察一个以多项式为自变量的函数 \(\mathcal F[f(x)]\) ,在OI中,它的解析式一般也是人畜无害的多项式形式,长成这样:
因为这里不存在针对 \(f(x)\) 的代入求值,最多也就是关于 \(x\) 求导数,所以多项式 \(f(x)\) 的存在更加接近于一个 “常量” 而非 “函数”,又考虑到多项式们有和实数几乎完全一致的加减乘运算,我们可以直接把一个多项式 \(f(x)\) 视作一个实数:
现在就是我们的小聪明了:直接照抄实数域上的导函数定义,捏一个 \(\mathcal F\) 的 “导函数” 出来!
我们暂且把这个东西叫做 “形式导数”,尽管直接跳过了极限和微分,我们仍然可以由它开始一步步打通泰勒展开的定义。
§ 0.3 噫!好!我展了!
回顾高等数学,实数域中泰勒展开的由来无非是挑选一个点,强制令新函数在这一点上的各阶导数和原来的函数相等。而且泰勒展开还有一个特点:如果被展开的函数是一个多项式函数或者能用幂级数表达的函数,其正确性的证明可以完全不依靠微分和无穷小量来得出。
如果有人想看的话我就去补写一下证明qwq
现在实在是懒得搞了
那么,既然多项式环有着和实数几乎一致的加减乘(导数需要的三种运算)和数乘运算(泰勒展开需要的额外运算),那么就算把实数版本的证明中的 \(x\) 改为多项式而不是实数,证明也依然成立。因此没有理由相信泰勒展开在多项式环上不能用。
换句话说,只要有了导数的形式,对多项式函数和指对函数泰勒展开就是正确的。
以自然底数为底的指数、对数函数可以写成无穷项幂级数的形式,所以它们也可以包含在内。
如果被展开函数是无穷多项的多项式函数,那么的确会用到极限。
但是因为求极限的对象是无穷多个多项式的加和,取到极限的变量仍是一个实数,我们仍然不需要定义 \(R[[x]]\) 上的极限。
正好,我们的 \(\mathcal F\) 也有着多项式的形式,这就为我们创造了机会,可以再次跳过微分,直接捏出一个新的泰勒展开:
接下来可以把 \(\mathcal X\) 换成我们更熟悉的多项式 \(f(x)\) .
好了!我们做完了!
虽然可能不甚严谨,但是管他呢,用就完了。
这里有一篇博客提供了另一种理解:多项式牛顿迭代(Freopen@csdn.com)
简单来说,我们挑选所有能让 \(f_0(x)\) 收敛的实数 \(x\) ,把它代入 \(f_0(x)\) 可以求出一些值,于是就可以把 \(f_0(x)\) 视作一个实数变量,同时自然把 \(\mathcal F[f(x)]\) 变成了一个以实数变量 \(f_0(x)\) 为自变量的函数,对其强行进行泰勒展开一定是正确的。
接下来再对所有的能让 \(f(x)\) 收敛的实数 \(x\) 代入其中,由于 \(f(x)\) 和 \(f_0(x)\) 的值域都为 \(R\) ,多项式表达式的差异可以大致等同于函数值(\(\mathcal F\) 函数的自变量)的差异。
综上,我们就得到了这种泰勒展开的正确性。
一般会使用 \(\frac{\text d}{\text df(x)}\mathcal F[f(x)]\) 来表示多项式为变量的函数的导数,但是这种写法会让人误以为所比的无穷小量是 “\(f(x)\) 的函数值” 而非 “\(f(x)\) 的解析式结构”。
而其实前后有着很大的区别,前者仍然是实数域上的导数,后者才是真正属于多项式环的微分。
不过笔者没有想出更好的记号来,下文中仍会沿用这种写法。
§ 0.4 开始迭代
多项式牛顿迭代可以解决这样的问题:
已知多项式 \(g(x)\) ,求满足这种条件的多项式 \(f(x)\),系数对 \(998244353\) 取模。
换句话说就是求零点。
这里多项式取模的意思就是只保留次数最低的 \(n\) 位,后面的东西不要了。
考虑用倍增实现。每一轮通过模 \(x^{\lceil \frac{n}{2} \rceil}\) 意义下的解 \(f_0(x)\) 得到模 \(x^n\) 意义下的解 \(f(x)\) .
第一轮中,我们需要求出 \(g(x)\) 在模意义下的根,这个单独处理。
如何迭代计算模 \(x^n\) 意义下的答案呢?我们可以用刚才推导出的泰勒展开,把 \(\mathcal G\)(把 \(g\) 视作一个自变量为多项式的函数)在 \(f_0(x)\) 处展开:
然后,观察到 \((f(x) - f_0(x))^k\) 在 \(k>2\) 情况下前 \(n\) 位都是零,这样我们就可以舍弃掉后面的东西了
当然还有另一种方式理解为什么要用倍增实现:
普通的牛顿迭代每次可以把精度翻倍,那么对应到多项式上,每次确定的项数也会翻倍。
化简式子
得到
诶嘿,正好是经典牛顿迭代法的形式。难道不应该是吗
这就做完了,时间复杂度分析如下:
已知多项式求逆的复杂为 \(O(n\log n)\) 求导和加减的复杂度是 \(O(n)\) ,那么就有
最终的复杂度是 \(O(n\log n)\) .
但是实际跑起来常数巨大无比。
Part 1 多项式乘法逆
§ 1.1 常规方法
在很多多项式算法(例如刚刚讲到的牛顿迭代)中都需要用一个多项式在模 \(x^n\) 意义下除以另一个多项式。这种操作等价于乘上另一个多项式的乘法逆元,现在我们来求这个东西。
根据刚才牛顿迭代的启发,我们用倍增解决这个问题,已知已经求得了模 \(x^{n/2}\) 意义下 \(f(x)\) 的逆元。
又因为目标答案长这样:
两式相减,得到:
式子两边平方,模数也平方,依然能整除:
然后式子把他自己收拾好了:
两边同时乘上 \(f(x)\) :
递归计算就做完了!采用本式计算乘法逆的话每轮会进行两次 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 哇塞,这居然可以
既然我们有了牛顿迭代,为什么不用用它呢?
好!那么我们先构造出这么一个函数:
可以看出来, \(\mathcal F\) 在模 \(x^n\) 意义下的零点就是我们要求的乘法逆元。
接下来列出牛顿迭代的式子:
带入解析式:
整理一下:
所有带分母的项都消去了,可以用 NTT 计算了。
等等,这不就是上面的那个式子吗
数学真奇妙
Part 2 多项式除法(取模)
§ 2.1 十分神仙的技巧
多项式除法,就是给定 \(f(x),g(x)\) 求多项式 \(Q(x),R(x)\) ,使得
商和余数的英文分别叫 quotient 和 remainder,所以一般用 \(Q(x)\) 和 \(R(x)\) 表示。
我们观察到,如果 \(R(x) \equiv 0 \pmod{x^n}\) 的话,只需要求逆就可以了。而对于 \(R(x) \not\equiv 0 \pmod{x^n}\) 的情况,我们需要想办法移除掉它的影响。
而移除的方法就是把 \(f(x),g(x)\) 的系数反向。
形式化地,构造变换
就可以把多项式的系数反向。
接下来对除法的式子进行这个变换(\(n=\deg f,m=\deg g\))
我们又知道,\(\deg Q = \deg f - \deg g = n-m\) 以及 \(\deg R \leq \deg g - 1\) ,所以就有
这个时候,奇妙的事情出现了,最后余数项 \(x^{n-m+1}R(x)\) 的最低次数为 \(n-m+1\) ,正好和最高次数为 \(n-m\) 的商 \(Q(x)\) 错开一位,我们把整个式子拿到模 \(x^{n-m}\) 意义下,就可以完全消去余数的影响:
现在除法变成了普通的多项式求逆,我们来总结一下步骤:
- 把 \(f,g\) 反向。
- 把 \(\bar f,\bar g\) 模上 \(x^{n-m}\) .
- 这个时候计算 \(\bar f(x) / \bar g(x)\) .
- 把结果模上 \(x^{n-m}\) 并反向,得到 \(Q(x)\) .
- 用结果乘一开始没有被反向、取模过的 \(g\) .
- 用一开始没有被反向、取模过的 \(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 指对函数的新定义
累了,直接上公式:
本质上还是之前的泰勒展开,其中对数函数因为 \(0\) 处无定义,选择在 \(1\) 处进行展开。
§ 2.2 导过去,积回来
已知多项式 \(f(x)\) ,求多项式 \(g(x)\) ,使得
其中 \(ln\) 函数的定义如上。
如果直接按照新定义中的方法强行计算,我们会发现必须把全部的无穷项都计算出来才能得到正确答案,所以我们需要换一种方法。
为什么要计算无穷项?因为上面的定义式每多一项,多项式的每一个系数都会发生改变。
那就换一个方法吧。
既然把 \(\ln\) 函数化成初等形式只需要求导,那就对 \(\ln\) 关于 \(f\) 求形式导数,得到了这个式子:
右边是很好计算的,那就积回去!
问题出现了,我们无法关于 \(f(x)\) 积分 \(\frac{1}{f(x)}\) ,也不知道积分常数 \(C\) 应该是多少。
这该怎么解决呢?我们发现,刚刚求不出结果的原因是我们把 \(\ln\) 看做了一个以多项式为自变量的函数。
而现在我们来换一种思路:把 \(\ln\) 也看成一个特殊的多项式,把 \(\ln(f(x))\) 看成求 \(\ln(x)\) 和 \(f(x)\) 这两个多项式的复合。
这样,就可以关于实数 \(x\) 对 \(ln(f(x))\) 求普通导数:
仿照上面,用积分表示左边:
化简成功!
经验:多项式的操作十分灵活,既可以关于多项式求导(形式导数),也可以关于实数求导(普通导数),两种方法都可以用来化简表达式。
模板代码(需要上文的 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 一切偷懒,终将绳之以法!
有一点需要特别注意,后面的这一部分是积分常数,因为这一项的存在,必须要求
这是为什么呢?
伟大的 OI-wiki
曾经曰过:
首先,对于多项式 \(f(x)\) ,若 \(\ln f(x)\) 存在,那么由其定义,其必须满足 \([x^0]f(x) = 1\)
不过 OI-wiki
并没有给出进一步的解释,这让我这个蒟蒻一头雾水:为什么没有符合规定的常数项就不能计算了?
然后我翻到另一篇文章如是说:
若 \([x^0]f(x)\neq 1\) ,那么结果的常数项不收敛。
这让我更加混乱了
实际上这几句话的意思是:若常数项不是 \(1\),那么得数的常数项将无法用有理数取模表示。
具体来说,根据刚才的推导,得数的常数项应为
而根据 \(e\) 的超越性,我们知道 \(y=\ln x\) 只有在 \(x=0\) 时才能取到有理数值,其余值均为无理数,也就没有办法取模。
这里有更加详细的证明:题解P4725(Kinesis@luogu.com.cn)
§ 2.4 多项式指数函数
已知多项式 \(f(x)\) ,求 \(g(x) = \exp(f(x))\) ,其中 \(\exp\) 函数的定义同 §2.4 。
这一部分十分容易推导,用牛顿迭代法即可。构造函数如下:
此函数的零点即为所求的 \(\exp(f(x))\) .
接下来使用牛顿迭代:
由于 \(\mathcal F'[g(x)] = \frac{1}{g(x)}\) ,可得:
即
时间复杂度为 \(O(n\log n)\) .
模板代码(需要上文的 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\times \deg g_i(x)\) .
首先,我们知道在式子
中,\(f(x)\) 高于 \(x^{2^i}\) 的项都是没有意义的,因为它最低的贡献也在 \(x^{2^i}\) 的位置,最终会全部被模掉。同理,\(\ln(g_{i-1}(x))\) 也只需要保留前 \(2^{i}\) 项。
对应到代码中,我们只需要把 F[x]
的前 len
项复制过来用以相乘,求 \(\ln(g_{i-1}(x))\) 时也只需要求到长度为 len
为止。
另外,外层用来相乘的 \(g_{i-1}(x)\) 的次数为 \(2^{i-1}\) ,所以最终结果(未取模)的最高次数是 \(1.5\times 2^{i}\) ,而为了凑齐进行 NTT 所需的整次幂,需要把长度补齐到 \(2\times 2^i\) .
对应到代码上,就是相乘是数组长度开到 len<<1
.