多项式全家桶
打算开 GF 这个万恶之源了,但在此之前先把多项式的那堆板子理理清楚吧。代码没有刻意卡常,而且写成的年代不同,码风和实现方法会有一点不一样,板子也不会太全,之后会遇到问题会在这里慢慢补充。
多项式乘法
给定一个 \(n\) 次多项式 \(F(x)=\sum_{i=0}^n f_ix^i\) 和 \(m\) 次多项式 \(G(x)=\sum_{j=0}^m g_jx^j\),求出 \(F(x)\) 和 \(G(x)\) 的卷积:
注意到多项式乘法的过程,单从卷积定义角度考虑是难以优化的,所以我们从多项式本身考虑。
多项式有两种表示方法,点值表示法和系数表示法。其中系数表示法就是我们常用的形式:
其中 \(f_i\) 表示 \(x^i\) 前面的系数。这显然可以唯一确定一个 \(n\) 次多项式。而点值表示法是这样的:
注意到这 \(n+1\) 个点也能表示一个 \(n\) 次多项式,可以从高斯消元的角度考虑。
显然我们常用的(题目输入和要求输出的)都是第一种表示方法,那第二种表示方法有啥用呢。对于两个 \(n\) 次多项式 \(F(x),G(x)\),它们的卷积 \(H(x)\) 在点值表示法下能很轻易地被求出:
时间复杂度仅有 \(\mathcal{O}(n)\)。但问题是题目不认点值表示法,而暴力在两种表示方法之间的转化复杂度是很高的。所以我们现在的任务就是找到在两种表示方法之间转化的合适方法。
FFT/快速傅里叶变换
前置知识:复数。
可以做到在 \(\mathcal{O}(n\log n)\) 的时间复杂下完成点值表示法和系数表示法的相互转化。
我们先来看从系数表示法到点值表示法,注意到这里的瓶颈在于算 \(x^i\) 和计算 \(F(x^i)\)。这两个问题本质上其实挺像的,都是要求寻找一个合适的 \(x\),使得 \(x^i\) 具有一些美妙的性质。
注意到 \(1,-1\) 的幂都是很好算的,但这样仅仅够我们算两个点,一共可是需要 \(n+1\) 个。所以我们要更多的点,更具体地讲,我们需要更多满足 \(|\omega^k|=1\) 的 \(\omega\)。可以发现需要引入复数了,\(i,-i\) 显然是方程的解,除此之外,在单位圆上所有向量对应的复数都满足:
(应该都能看出来是从 OI wiki 拿的图吧)
严谨讲,我们定义 \(x^n=1\) 在 \(\mathbb{C}\) 中的解是 \(n\) 次复根。根据上图,我们能发现这样的解有 \(n\) 个,根据欧拉公式 \(e^{ix}=\cos x+i\sin x\),我们定义:
为单位复根,可以发现这个复数对应了把单位圆 \(n\) 等分的第一个角对应的向量。则 \(x^n=1\) 的解集能用 \(w_n\) 的幂表示:
说了这么多,真正要投入应用的话,我们还需要知道单位复根的一些性质。对于任意的正整数 \(n\) 和整数 \(k\),有:
均可以通过把复数看成向量,用几何意义证明,不再赘述。而有了这些性质,就可以开始进入正片了。
FFT 的基本思路是分治,递归处理当 \(x=w_n^k\) 时,\(f(x)\) 的值。我们来举个例子吧,\(8\) 项的多项式:
考虑奇偶分治,把式子按照下标的奇偶性分成两坨:
分别对奇偶项建立新函数:
则显然有:
好了,现在就可以代入 \(\omega^k_n\) 了:
好像还是看不出来什么,再代入个 \(\omega^{k+\frac{n}{2}}_n\) 试试?推导过程省略,跟上面差不多,我们能得到:
好了,现在我们就完全能看出来了,求出 \(g(\omega_{\frac{n}{2}}^k),h(\omega^k_{\frac{n}{2}})\) 之后我们就能一次处理处两个函数值!而处理这俩函数的过程又是一次递归的过程!这下找到了方向了,我们对于每个函数值,需要代入 \(n\) 个不同的值,每次会把多项式长度缩减 \(\frac{1}{2}\),所以最终复杂度就是 \(\mathcal{O}(n\log n)\)。
值得注意的是,分治的时候需要保证每次分治两边长短相同,换句话说,需要 \(n=2^m,m\in\mathbb{N}\)。如果原多项式不满足的话,就用 \(0\) 补就好了。
好了,现在我们能把系数表示法换成点值表示法了,愉快地把函数值相乘,然后呢?然后我们就需要把点值表示法换成系数表示法了。这一过程通常被称为 IFFT,即逆 FFT。
考虑原本的多项式是 \(f(x)=\sum_{i=0}^{n-1} a_ix^i\),而现在我们已知 \(y_i=f(\omega_n^i),i\in[0,n)\cap\mathbb{Z}\),现在要求 \(\{a_0,a_1,\cdot\cdot\cdot,a_{n-1}\}\)。考虑设:
现在我们就要在 \(A(x)\) 上面搞搞事了。
考虑将 \(\omega_n^{-i}\) 分别代入 \(A(x)\),则有:
设 \(S(\omega_n^a)=\sum_{i=0}^{n-1}(\omega_n^{ai})\),则当 \(a\equiv0\pmod{n}\) 时,显然 \(S(\omega_n^a)=n\)(因为 \(\omega_n^a=1\) 嘛)。
而当 \(a\not \equiv0\pmod{n}\) 时,考虑经典 trick 错位相减:
综上,我们有:
带回原式:
非常神奇,如果令 \(b_k=\omega_n^{-k}\),则 \(A\) 的点值表示法就是:
综上,我们取单位根为其倒数,然后对 \(A\) 做一遍上述的 FFT 过程,再除以 \(n\) 就得到了系数表示法。
递归版实现:\(\tt code\)
好的现在您写完了递归版,非常愉快地交了模板题,非常愉快地获得了 \(\tt 100pts\),但这一切快乐都在您打开提交记录,按照运行时间排序后结束了,“他们的为啥跑的这么快??”
直觉告诉我们是递归的锅。递归带来了大常数,让本就因为实数运算常数不小的 FFT 雪上加霜。所以我们要考虑把它变成非递归版本。
考虑模拟算法中递归分治的过程:
分完了。看不出来什么?考虑把分治前的数和分治后的数都变成二进制表示:
这下明显了吧,分治前后的数二进制位是反过来的!那我们只需要提前把数交换到对应的位置,然后模拟递归的过程就好了。
现在的问题是怎么把二进制位给反过来。\(\mathcal{O}(n\log n)\) 固然是一种方法,不过我们能做到 \(\mathcal{O}(n)\)。考虑递推,设 \(rev_x\) 表示 \(x\) 的二进制位反过来的结果,显然有 \(rev_0=0\)。而对于一个数 \(x\),如果除去最高位不算,\(rev_x=\left\lfloor\dfrac{rev_{\lfloor\frac{x}{2}\rfloor}}{2}\right\rfloor\),也就是把 \(x\) 右移一位的数反转一下再右移一位。而对于最高位,它取决于 \(x\) 的奇偶性,所以有:
其中 \(k\) 是二进制表示下的位数。
非递归版实现:\(\tt code\)
NTT/快速数论变换
前置知识:原根
刚刚我们看到了 FFT,这里还有一种思路类似的 NTT,是 FFT 在数论基础上的实现。由于 FFT 涉及到大量的实数运算,丢精度,速度慢就成为了不可避免的问题。而 NTT 是实现在数论基础上的,运算都是整数,准确度和速度都会更快,但时间复杂度依然是 \(\mathcal{O}(n\log n)\)。
这俩的基本思路都差不多,而我们的关键就是在数论领域中找出一个东西来代替单位复根。我们发现,对于质数 \(p=qn+1(n=2^m)\),它的原根 \(g\) 恰好就满足刚刚所说的性质:
而如果我们把 \(g^q\) 记作 \(g_{n}\),会发现它也有类似的性质,如:
然后就结束了,只需要把 FFT 里的单位复根扣掉换成原根就行了。常见的质数原根:
其余的质数可以参考求原根的方法考场现求。
实现:\(\tt code\)
分治 FFT/NTT
给出序列 \(g_{1\sim n}\),求出序列 \(f_{0\sim n}\),其中:
答案对 \(998,244,353\) 取模。
能看到很明显的卷积特征,但很遗憾,我们不能直接进行一个积的卷,因为对于 \(f_i\) 来说,它的值是依赖以前求出来的值的,换句话说,我们要求在线的卷积。(这也是为什么这个算法在国外有时也被称为在线卷积)
考虑参考 \(\rm cdq\) 分治的思路,递归处理区间。先处理左区间,然后处理左区间对有区间的贡献,最后处理右区间,这样能保证每次卷积需要的值都已经被求出了。
具体来讲,对于当前区间 \([l,r)\),我们先递归求出 \([l,mid)\) 中 \(f\) 的值,然后将 \(f_{l\sim mid-1}\) 与 \(g_{0\sim r-l-1}\) 给卷起来,这样能得到左边对右边 \(f_{mid,r-1}\) 的贡献。之后递归处理右区间 \([mid,r)\) 即可。时间复杂度:
实现:\(\tt code\)
注意卷积之前要补 \(0\) 清空而不是不管!
MTT
任意模数 NTT,不会。
多项式牛顿迭代
给出多项式 \(g(x)\),已知存在一个多项式 \(f(x)\),满足:
求出模 \(x^n\) 意义下的 \(f(x)\)。
考虑倍增的思路。首先考虑边界,对于 \(n=1\) 的情况,我们需要单独求出 \([x^0]g(f(x))=0\) 的解。然后对于其余的 \(n\),我们考虑先递归计算 \(\left\lceil\dfrac{n}{2}\right\rceil\) 的情况,假设得到在模 \(x^{\lceil\frac{n}{2}\rceil}\) 意义下的解是 \(f_0(x)\),则现在我们的目标就是要用 \(f_0(x)\) 表示 \(f(x)\)。
考虑将 \(g(f(x))\) 在 \(f_0(x)\) 处泰勒展开,则原同余方程可化为:
其中 \(g^{(i)}\) 表示 \(g\) 的 \(i\) 阶导。注意到 \(f(x)-f_0(x)\) 这个式子的最低非 \(0\) 项次数最低是 \(\lceil\frac{n}{2}\rceil\),因为在这之前它们都一样,所以有:
这样,原同余方程又能化为:
简单的代数变化:
这下式子有了,至于怎么求,怎么用,且听下文分解。
多项式求逆
给定一个多项式 \(f(x)\),求出一个多项式 \(g(x)\) 满足:
系数对 \(998,244,353\) 取模。
考虑套刚刚的多项式牛顿迭代。为了避免跟上文的符号矛盾,我们记待求逆的函数为 \(h(x)\),求逆结果函数为 \(f(x)\),则有:
套牛顿迭代的式子有:
注意这里求导的时候把 \(h(x)\) 当成常数来做了,然后就结束了,时间复杂度:
实现:\(\tt code\)
注意,在实现牛顿迭代的时候,类似分治 FFT,卷积之前一定要记得清空不用的!
多项式开根
给出一个 \(n-1\) 次多项式 \(A(x)\),找出一个模 \(x^n\) 意义下的多项式 \(B(x)\) 使得
系数对 \(998,244,353\) 取模。
依然考虑牛顿迭代。类似求逆的推导过程,我们依然记 \(h(x)\) 为待开根函数,\(f(x)\) 为答案,则有:
还是套牛顿迭代的式子:
多项式求逆就可以做不带余数的除法了。类似求逆,时间复杂度 \(\mathcal{O}(n\log n)\)。
实现:\(\tt code\)
多项式求导/积分
这个比较简单了,主要是一些公式。
首先由于求导和积分的线性性:
所以对于多项式 \(F(x)=\sum_{i=0}^{n-1}a_ix^i\),它的导数和积分分别为:
可以 \(\mathcal{O}(n)\) 求解。
还有一些比较常用的求导公式:
更多的东西比如积分公式啊,更多的求导公式啊,常见的函数的积分导数啊,建议大概学一下微积分。
多项式带余除法
给出一个 \(n\) 次多项式 \(F(x)\) 和一个 \(m\) 次多项式 \(G(x)\),求出多项式 \(Q(x),R(x)\) 满足以下条件:
- \(Q(x)\) 次数为 \(n-m\),\(R(x)\) 次数小于 \(m\)。
- \(F(x)=Q(x)G(x)+R(x)\)。
系数对 \(998,244,353\) 取模。
刚刚其实我们也做过分数运算(在牛顿迭代那一块),但这里不一样的是,题目还要求求出 \(R(x)\),即余数。这不太好办,所以考虑消去 \(R(x)\) 的影响。
有个很妙的思想 (不知道咋想到的) ,考虑构造多项式 \(F^R(x)\):
容易想到 \(F^R(x)\) 的实质其实就把系数反转一下。顺着这个思路,将带余除法的等式两边同时乘上 \(x^n\) 并把函数里的自变量都改为 \(\frac{1}{x}\) 有“
诶,这个式子就特殊了,注意到只有 \(R^R(x)\) 这一项前面有一个 \(x^{n-m+1}\),那我们只需要把上式放在模 \(x^{n-m+1}\) 意义下不就把 \(R^R(x)\) 干掉了吗,即:
问题是,这会不会对 \(Q^R(x)\) 造成影响导致我们求出的值不精确呢?显然不会,因为 \(Q^R(x)\) 的次数仅有 \(n-m\),小于模数的 \(n-m+1\) 次,所以 \(Q^R(x)\) 并不会受到影响。
这样求个逆就能把 \(Q(x)\) 求出来了。求出来之后再反代回去就有 \(R(x)\) 了。时间复杂度 \(\mathcal{O}(n\log n)\)。
实现:\(\tt code\)
多项式 ln
给出 \(n-1\) 次多项式 \(A(x)\),求一个模 \(x^n\) 意义下的多项式 \(B(x)\) 满足:
系数对 \(998,244,353\) 取模。
考虑设 \(G(x)=\ln x\),则原题相当于求 \(G(F(x))\),考虑给这玩意求个导:
由于,
则上式为:
这样我们就能求出带求多项式的导数了,之后只需要积分回来即可:
这样直接求导+求逆+积分这题就做完了。时间复杂度 \(\mathcal{O}(n\log n)\)。
实现:\(\tt code\)
多项式 Exp
给出 \(n-1\) 次多项式 \(A(x)\),求一个模 \(x^n\) 意义下的多项式 \(B(x)\) 满足:
系数对 \(998,244,353\) 取模。
考虑多项式牛顿迭代法。记 \(A(x)\) 为 \(h(x)\),\(B(x)\) 为 \(f(x)\),则有:
套式子:
求个 \(\ln\),再加加减减后跟其他的乘起来就完事了。时间复杂度 \(\mathcal{O}(n\log n)\)。
实现:\(\tt code\)