多项式全家桶

[学习笔记] 多项式全家桶 学习笔记 写在前面:建议初一就学懂这个东西,这是多项式题目的基础。 0.复数类的实现 定义一个复数为 a+bia+bia+bi 。 加法: a+bi+c+di=(a+c)+(b+d)ia+bi+c+di=(a+c)+(b+d)ia+bi+c+di=(a+c)+(b+d)i 减法:同上,系数改成 −1-1−1 乘法:(a+bi)(c+di)=ac+adi+bci+bdi2=(ac−bd)+(ad+bc)i(a+bi)(c+di)=ac+adi+bci+bdi^2=(ac-bd)+(ad+bc)i(a+bi)(c+di) 阅读详情

下面的所有知识均属多项式范畴。


前言

本文略去多项式的定义,毕竟初中数学都讲过。

本文会出现一些可能没有见过的表达:

  • 对于一个关于 x x x 的多项式 f ( x ) f(x) f(x),有时也称其最高次项次数为 f ( x ) f(x) f(x) 的度数。
  • 多项式同样定义了四则运算,包括常数与多项式的四则运算和多项式之间的四则运算,同时也衍生出了同余符号 ≡ \equiv ≡、对数函数 ln ⁡ ( f ( x ) ) \ln(f(x)) ln(f(x)),指数函数 exp ⁡ ( f ( x ) ) \exp(f(x)) exp(f(x)) 等等。
  • 下文一般采用 f ( x )     m o d   x n f(x)~\bmod x^n f(x) modxn 来表示多项式 f ( x ) f(x) f(x) 的所有次数小于 n n n 的项之和。
  • 下文有时会采用 [ x k ] f ( x ) [x^k]f(x) [xk]f(x) 表示多项式次数为 k k k 的项。

注意:本文的知识环环相扣,请勿乱序阅读。


拉格朗日插值

插值,通俗地讲,就是指通过多项式在某些点的值求出整个多项式的表达式。

而拉格朗日插值就是计算插值的一种方法,它支持通过 n + 1 n + 1 n+1 个点插出一个度数为 n n n 的多项式。

按惯例,献上模板题。

首先对于多项式 f ( x ) f(x) f(x),必然满足 f ( x ) ≡ f ( a )   (   m o d   ( x − a ) ) f(x) \equiv f(a)~(\bmod (x - a)) f(x)≡f(a) (mod(x−a))。

证明:

转化为 f ( x ) − f ( a ) ≡ 0   (   m o d     ( x − a ) ) f(x) - f(a) \equiv 0~(\bmod~(x - a)) f(x)−f(a)≡0 (mod (x−a))。

设左侧的多项式为 g ( x ) g(x) g(x),显然其常数项为 0 0 0。

又因为其第 i i i 项为 p i ( x i − a i ) p_i(x^i - a^i) pi​(xi−ai)( p i p_i pi​ 为系数),并且 x i − a i x^i - a^i xi−ai 必定可以分解为 ( x − a ) ⋅ q ( x ) (x - a) \cdot q(x) (x−a)⋅q(x) 的形式( q ( x ) q(x) q(x) 为另一个多项式),所以得证。

所以我们可以得到一个关于 f ( x ) f(x) f(x) 线性同余方程组:
{ f ( x ) ≡ y 1   (   m o d   ( x − x 1 ) ) f ( x ) ≡ y 2   (   m o d   ( x − x 2 ) ) f ( x ) ≡ y 3   (   m o d   ( x − x 3 ) ) ⋮ f ( x ) ≡ y n   (   m o d   ( x − x n ) ) \left\{ \begin{matrix} f(x) \equiv y_1~(\bmod (x - x_1)) \\ f(x) \equiv y_2~(\bmod (x - x_2)) \\ f(x) \equiv y_3~(\bmod (x - x_3)) \\ \vdots \\ f(x) \equiv y_n~(\bmod (x - x_n)) \end{matrix} \right. ⎩⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎧​f(x)≡y1​ (mod(x−x1​))f(x)≡y2​ (mod(x−x2​))f(x)≡y3​ (mod(x−x3​))⋮f(x)≡yn​ (mod(x−xn​))​
这里可以使用中国剩余定理解方程组(这里就没有必要关注它们是否互质了)。

我们可以设 M = ∏ i = 1 n ( x − x i ) M = \prod_{i = 1}^n (x - x_i) M=∏i=1n​(x−xi​), M i = M ( x − x i ) = ∏ j ≠ i ( x − x j ) M_i = \frac{M}{(x - x_i)} = \prod_{j \ne i} (x - x_j) Mi​=(x−xi​)M​=∏j​=i​(x−xj​)。

则 M i M_i Mi​ 在模 x − x i x - x_i x−xi​ 意义下的逆元为 ∏ j ≠ i 1 x i − x j \prod_{j \ne i}\frac{1}{x_i - x_j} ∏j​=i​xi​−xj​1​。

(因为 M i M i − 1 = ∏ j ≠ i x − x j x i − x j ≡ 1   (   m o d   ( x − x i ) ) M_i M_i^{-1} = \prod_{j \ne i}\frac{x - x_j}{x_i - x_j} \equiv 1~(\bmod (x - x_i)) Mi​Mi−1​=∏j​=i​xi​−xj​x−xj​​≡1 (mod(x−xi​)),则对于每一个 j ≠ i j \ne i j​=i 都存在 $ x - x_j \equiv x_i - x_j~(\bmod (x - x_i))$,移项可得 x − x i ≡ 0   (   m o d   ( x − x i ) ) x - x_i \equiv 0~(\bmod (x - x_i)) x−xi​≡0 (mod(x−xi​)),这显然是成立的。)

所以有 f ( x ) = ∑ i = 1 n y i ∏ j ≠ i x − x j x i − x j   (   m o d     M ) f(x) = \sum_{i = 1}^n y_i\prod_{j \ne i} \frac{x - x_j}{x_i - x_j}~(\bmod~M) f(x)=∑i=1n​yi​∏j​=i​xi​−xj​x−xj​​ (mod M),它在模意义下是唯一的。

(实际上有另外的证明方式证明了其在通常意义下也是唯一的,这里略去。)

本题中,我们只需要代入 k k k 求解即可,时间复杂度为 O ( n 2 ) \mathcal{O}(n^2) O(n2)。不过更多时候,我们需要求出其所有系数,也可以做到 O ( n 2 ) \mathcal{O}(n^2) O(n2),甚至还存在更低的复杂度,这里就不展开介绍了。

代码就不贴了。


多项式乘法

多项式乘法的定义,这里就不必多言了,初中数学都讲过。

代数形式:

设有多项式 f ( x ) = a 0 + a 1 x + a 2 x 2 + ⋯ + a n x n f(x) = a_0 + a_1 x+ a_2x^2 + \cdots+ a_nx_n f(x)=a0​+a1​x+a2​x2+⋯+an​xn​, g ( x ) = b 0 + b 1 x + b 2 x 2 + ⋯ + b m x m g(x) = b_0 + b_1 x+ b_2x^2 + \cdots+ b_mx_m g(x)=b0​+b1​x+b2​x2+⋯+bm​xm​。

则它们的乘法积为 t ( x ) = c 0 + c 1 x + c 2 x 2 + ⋯ + c n + m x n + m t(x) = c_0 + c_1 x + c_2 x^2 + \cdots +c_{n + m}x^{n + m} t(x)=c0​+c1​x+c2​x2+⋯+cn+m​xn+m。

其中,对于 t ( x ) t(x) t(x) 的系数 c k c_k ck​ 有公式: c k = ∑ i = 0 k a i b k − i c_k = \sum_{i = 0}^k a_i b_{k - i} ck​=∑i=0k​ai​bk−i​。

可以发现这个公式的形式十分类似卷积。但请注意:它们不是同一个东西,不要弄混淆了。

事实上,多项式乘法就是用于求卷积的。如果您对卷积感兴趣,建议百度。


现在,给出两个多项式 f ( x ) f(x) f(x) 和 g ( x ) g(x) g(x) 的系数,求出它们的乘法积 t ( x ) t(x) t(x)。 f ( x ) f(x) f(x) 和 g ( x ) g(x) g(x) 的次数小于 1 0 5 10^5 105。

根据上面的公式,我们可以得到一个 O ( n 2 ) \mathcal{O}(n^2) O(n2) 的优秀算法。

我们考虑换一种思想计算这个乘法积。

众所周知,有这样一条著名的定理:任意 k k k 个点可以唯一确定一个至多为 k − 1 k - 1 k−1 次的多项式。

所以我们得到了一个新的思路:先求出足够多的 f ( x ) f(x) f(x) 和 g ( x ) g(x) g(x) 的点值,将它们用 O ( n ) \mathcal{O}(n) O(n) 的复杂度乘起来(横坐标不变,纵坐标相乘),再将这些点转化为一个多项式(上面提到了这样的过程叫做插值)。

这种方法叫做离散傅里叶变换(即著名的 DFT ⁡ \operatorname{DFT} DFT)。

问题是,如果钦定一些 x x x 去求对应的 f ( x ) f(x) f(x) 的值,时间复杂度还是 O ( n 2 ) \mathcal{O}(n^2) O(n2) 的。

所以,下面介绍快速傅里叶变换,同时会介绍快速数论变换(即著名的 FFT ⁡ \operatorname{FFT} FFT 和 NTT ⁡ \operatorname{NTT} NTT)。


前置知识:单位根,原根。

还记得二次函数 y = x 2 y = x^2 y=x2 的性质吗?

它关于 y y y 轴对称,所以我们如果代入 x = k x = k x=k,同样可以得到 x = − k x = -k x=−k 时 y y y 的值。

这启发我们,可以选取一些特殊的 x x x,使得我们可以在求出一些点值的同时,快速地求出另外的一些点值。

这个时候,单位根就派上用场了。

先讲一些使得单位根能够应用于此的性质:

  1. 记 ω n \omega_n ωn​ 为 n n n 次单位根,即满足 ω n k = e 2 k π i n \omega_n^k = e^{\frac{2k\pi i}{n}} ωnk​=en2kπi​,而 e 2 k π i n = cos ⁡ 2 k π n + i sin ⁡ 2 k π n e^{\frac{2k\pi i}{n}}=\cos \frac{2k\pi}{n} + i \sin \frac{2k\pi}{n} en2kπi​=cosn2kπ​+isinn2kπ​, i i i 为虚数单位。

(注意 n n n 次单位根的 n n n 一般取 2 2 2 的正整数次幂,而下文皆如此。)

这个计算公式不仅可以让我们计算出单位根的具体数值,并且可以得出以下性质:

  • ω n n = cos ⁡ 2 π + i sin ⁡ 2 π = 1 = ω n 0 \omega_n^n = \cos 2\pi + i \sin 2\pi = 1 = \omega_n^0 ωnn​=cos2π+isin2π=1=ωn0​。

这条性质说明 n n n 次单位根存在长度为 n n n 的指数循环节;

  • ω n n 2 = cos ⁡ π + i sin ⁡ π = − 1 \omega_n^{\frac{n}{2}} = \cos \pi + i \sin \pi= -1 ωn2n​​=cosπ+isinπ=−1。

这条性质间接地证明了 n n n 次单位根的最小指数循环节长度为 n n n,同时为下文折半引理的证明做了铺垫。

  1. 单位根消去引理: ω 2 n 2 k = ω n k \omega_{2n}^{2k} = \omega_n^k ω2n2k​=ωnk​。

根据公式有 ω 2 n 2 k = e 4 k π i 2 n = e 2 k π i n = ω n k \omega_{2n}^{2k}= e^{\frac{4k\pi i}{2n}} = e^{\frac{2k\pi i}{n}} = \omega_{n}^k ω2n2k​=e2n4kπi​=en2kπi​=ωnk​,得证。

这条性质可以做为沟通不同次单位根的桥梁,同时也证明了 n n n 次单位根的值域是 2 n 2n 2n 次单位根值域的真子集。

  1. 单位根折半引理: ω n k + n 2 = − ω n k \omega_n^{k+\frac{n}{2}} = -\omega_{n}^k ωnk+2n​​=−ωnk​。

根据上面的性质有 ω n k + n 2 = ω n k ⋅ ω n n 2 = − ω n k \omega_n^{k + \frac{n}{2}} = \omega_n^k \cdot \omega_n^{\frac{n}{2}} = -\omega_n^k ωnk+2n​​=ωnk​⋅ωn2n​​=−ωnk​,得证。

这条性质可以做为沟通同次不同指数的单位根的桥梁,为 DFT ⁡ \operatorname{DFT} DFT 的分治优化创造了可能。

而我们该如何运用单位根的这些性质呢?

我们将 f ( x ) = a 0 + a 1 x + a 2 x 2 + a 3 x 3 f(x) = a_0 + a_1 x + a_2 x^2 + a_3x^3 f(x)=a0​+a1​x+a2​x2+a3​x3 做一个小小的改动:
f ( x ) = a 0 + a 1 x + a 2 x 2 + a 3 x 3 = a 0 + a 2 x 2 + a 1 x + a 3 x 3 = ( a 0 + a 2 x 2 ) + x ( a 1 + a 3 x 2 ) = f 1 ( x 2 ) + x f 2 ( x 2 ) \begin{aligned} f(x) &= a_0 + a_1 x + a_2 x^2 + a_3x^3 \\ &=a_0 + a_2 x^2 + a_1 x + a_3x^3 \\ &= (a_0 + a_2 x^2) + x(a_1 + a_3 x^2) \\ &= f_1(x^2) + xf_2(x^2) \end{aligned} f(x)​=a0​+a1​x+a2​x2+a3​x3=a0​+a2​x2+a1​x+a3​x3=(a0​+a2​x2)+x(a1​+a3​x2)=f1​(x2)+xf2​(x2)​
可以发现,这样的变换,会使得原多项式被拆分为两个规模缩小了一半多项式。

所以,我们又有了一个新的算法模型:

我们考虑将 f ( x ) f(x) f(x) 拆分为两个规模缩小了一半的多项式 f 1 ( x ) , f 2 ( x ) f_1(x), f_2(x) f1​(x),f2​(x),然后对它们进行分治,接着,我们可以用 f 1 ( x ) , f 2 ( x ) f_1(x),f_2(x) f1​(x),f2​(x) 在 x = k 2 x = k^2 x=k2 处的点值计算出 f ( x ) f(x) f(x) 在 x = k x = k x=k 处的点值。如果分治时每一层的复杂度都能够做到 O ( n ) \mathcal{O}(n) O(n),那么总的时间复杂度就是 O ( n log ⁡ 2 n ) \mathcal{O}(n \log_2 n) O(nlog2​n)。

接下来,我们考虑利用单位根的性质,将每一层转化点值的操作复杂度变为 O ( n ) \mathcal{O}(n) O(n)。

如果我们将单位根 ω n k \omega_{n}^k ωnk​ 和 ω n k + n 2 \omega_n^{k + \frac{n}{2}} ωnk+2n​​ 分别代入多项式 f ( x ) f(x) f(x):
f ( ω n k ) = f 1 ( ω n 2 k ) + ω n k f 2 ( ω n 2 k ) = f 1 ( ω n 2 k ) + ω n k f 2 ( ω n 2 k ) f ( ω n k + n 2 ) = f 1 ( ω n 2 k + n ) + ω n k + n 2 f 2 ( ω n 2 k + n ) = f 1 ( ω n 2 k ) − ω n k f 2 ( ω n 2 k ) f(\omega_{n}^{k}) = f_1(\omega_n^{2k}) + \omega_n^kf_2(\omega_n^{2k}) = f_1(\omega_{\frac{n}{2}}^{k}) + \omega_n^kf_2(\omega_{\frac{n}{2}}^{k}) \\ f(\omega_{n}^{k + \frac{n}{2}}) = f_1(\omega_n^{2k + n}) + \omega_n^{k + \frac{n}{2}}f_2(\omega_n^{2k + n}) = f_1(\omega_{\frac{n}{2}}^{k}) - \omega_n^kf_2(\omega_{\frac{n}{2}}^{k}) f(ωnk​)=f1​(ωn2k​)+ωnk​f2​(ωn2k​)=f1​(ω2n​k​)+ωnk​f2​(ω2n​k​)f(ωnk+2n​​)=f1​(ωn2k+n​)+ωnk+2n​​f2​(ωn2k+n​)=f1​(ω2n​k​)−ωnk​f2​(ω2n​k​)
可以发现,只要我们求出了 f 1 ( x ) , f 2 ( x ) f_1(x),f_2(x) f1​(x),f2​(x) 在 x = ω n 2 k x = \omega_{\frac{n}{2}}^k x=ω2n​k​ 处的点值,我们就可以直接得出 f ( x ) f(x) f(x) 在 x = ω n k , x = ω n k + n 2 x = \omega_n^k,x = \omega_n^{k + \frac{n}{2}} x=ωnk​,x=ωnk+2n​​ 两处的点值!

具体来说,我们只要在所有长度为 n n n 的多项式中求出在所有 n n n 次单位根处的点值即可。

这样,做到 O ( n ) \mathcal{O}(n) O(n) 的复杂度已经易如反掌了。

我们再来理一遍这个算法吧:(同时加入了一些很多细节)

  1. 我们找到一个 p p p,使得 p p p 是 2 2 2 的正整数次幂且 p ≥ n + m + 1 p \ge n + m + 1 p≥n+m+1。

这一步操作之前没有提到,但是很有必要,它为分治算法的进行提供了方便,这样我们就无需考虑多项式的度数;同时,这一步操作不会影响到点值的大小,也不会影响到最终插出的多项式的度数 (想一想,为什么)。

代码:( q q q 的作用下文会提及)

int p = 1, q = -1;
while(p <= n + m) ++q, p <<= 1;
  1. 接着,我们对两个多项式 f ( x ) f(x) f(x) 和 g ( x ) g(x) g(x) 分别跑 FFT ⁡ \operatorname{FFT} FFT。在 FFT ⁡ \operatorname{FFT} FFT 运行过程中,如果当前多项式长度为 1 1 1,直接返回系数;否则,将当前的多项式 f ( x ) f(x) f(x) 按照系数底数的奇偶性将项分类(这里请读者自己想一想为什么不可以直接对半分类),将其拆分为两个多项式,然后分治。

但是,如果这一步直接采用递归,时间似乎会有些吃不消。

我们不妨来探讨一下系数究竟是怎样分配的:(以 n = 8 n = 8 n=8 为例)
{ a 0 , a 1 , a 2 , a 3 , a 4 , a 5 , a 6 , a 7 } ↓ { a 0 , a 2 , a 4 , a 6 }   { a 1 , a 3 , a 5 , a 7 } ↓ { a 0 , a 4 }   { a 2 , a 6 }   { a 1 , a 5 }   { a 3 , a 7 } ↓ { a 0 }   { a 4 }   { a 2 }   { a 6 }   { a 1 }   { a 5 }   { a 3 }   { a 7 } \{a_0, a_1, a_2, a_3, a_4, a_5, a_6, a_7\} \\ \downarrow \\ \{a_0,a_2,a_4,a_6\}~\{a_1,a_3,a_5,a_7\} \\ \downarrow \\ \{a_0, a_4\}~\{a_2, a_6\}~\{a_1, a_5\}~\{a_3, a_7\} \\ \downarrow \\ \{a_0\}~\{a_4\}~\{a_2\}~\{a_6\}~\{a_1\}~\{a_5\}~\{a_3\}~\{a_7\} {a0​,a1​,a2​,a3​,a4​,a5​,a6​,a7​}↓{a0​,a2​,a4​,a6​} {a1​,a3​,a5​,a7​}↓{a0​,a4​} {a2​,a6​} {a1​,a5​} {a3​,a7​}↓{a0​} {a4​} {a2​} {a6​} {a1​} {a5​} {a3​} {a7​}
把每个系数的底数分离:

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

转换成二进制:

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

我们再把它们翻转过来:
000 , 001 , 010 , 011 , 100 , 101 , 110 , 111 000, 001, 010, 011, 100, 101, 110, 111 000,001,010,011,100,101,110,111

这不就是从小到大排序的吗?

所以我们就有了如下代码:(设 r i r_i ri​ 为分配完系数后第 i i i 个位置上的系数)

for(register int i = 0; i < p; i++) r[i] = (r[i >> 1] >> 1) | ((i & 1) << q);

解释一下:第 i i i 个位置的 r r r,不就等于第 ⌊ i 2 ⌋ \lfloor\frac{i}{2}\rfloor ⌊2i​⌋ 个位置的 r r r 右移一位吗?

如果 i i i 是奇数,我们再在最高位补上一个 1 1 1 即可( q q q 的作用就在这里)。

以上过程称为 Rader ⁡ \operatorname{Rader} Rader 排序。

  1. 用拆分出的两个多项式的点值,求出当前多项式的点值。

代码:( Complex ⁡ \operatorname{Complex} Complex 为复数类,定义了构造函数,重定义了乘法)

inline void FFT(Complex *f, int n)
{
	for(register int i = 0; i < n; i++) if(i < rev[i]) swap(f[i], f[rev[i]]);
	for(register int len = 2; len <= n; len <<= 1)
	{
	   	int k = len >> 1;
		Complex _w(cos(PI / k), t * sin(PI / k));
		for(register int i = 0; i < n; i += len)
		{
			Complex w(1.0, 0.0);
			for(register int j = 0; j < k; j++)
			{
				Complex u = f[i + j], v = w * f[i + j + k];
				f[i + j] = u + v;
				f[i + j + k] = u - v;
           	     w = w * _w;
			}
		}
	}
	return;
}
  1. 接着,我们将 f ( x ) f(x) f(x) 和 g ( x ) g(x) g(x) 对应的点值相乘,时间复杂度为 O ( n ) \mathcal{O}(n) O(n)。

你以为结束了?并没有,我们还有第五步:

  1. 最后,我们将多项式插值出每一项的系数,算法结束。我们称这一步叫 IFFT ⁡ \operatorname{IFFT} IFFT。

“算法结束” 四个字就在这里,近在咫尺,却又远在天边。

如果用拉格朗日插值计算,时间复杂度还是 O ( n 2 ) \mathcal{O}(n^2) O(n2),功亏一篑。

难道前面的分析过程都只能打水漂?

不不不,我们冷静分析一波。

看看下面这张图:

多项式乘法的过程实际上类似上图中的矩阵乘法。

那么我们想得到后面的系数矩阵,不就只要把中间的这个矩阵求逆就行了吗!

而这个逆矩阵非常特殊:

它等于原矩阵的每个值求倒数,再乘上一个 1 n \frac{1}{n} n1​。

(然而笔者线性代数太菜了,无法解释其原因。)

既然无法解释,那就考虑推式子证明吧:

设 y k y_{k} yk​ 是 f ( x ) , g ( x ) f(x),g(x) f(x),g(x) 在傅里叶变换后,在 x = ω p k x = \omega_p^k x=ωpk​ 处点值相乘的结果。

我们设多项式 s ( x ) = y 0 + y 1 x + y 2 x 2 + ⋯ + y p − 1 x p − 1 s(x) = y_0 + y_1 x + y_2 x^2 + \cdots + y_{p - 1} x ^{p - 1} s(x)=y0​+y1​x+y2​x2+⋯+yp−1​xp−1 在 x = ω n 0 , ω n − 1 , ⋯ ⋯   , ω n − ( p − 1 ) x = \omega_n^{0}, \omega_n^{-1}, \cdots\cdots,\omega_n^{-(p - 1)} x=ωn0​,ωn−1​,⋯⋯,ωn−(p−1)​ 处的点值分别为 u 0 ∼ p − 1 u_{0 \sim p - 1} u0∼p−1​,即满足:
u k = ∑ i = 0 p − 1 y i ( ω p − k ) i u_k = \sum_{i = 0}^{p - 1} y_i(\omega_p^{-k})^i uk​=i=0∑p−1​yi​(ωp−k​)i
现在我们要用它证明等式 c k = u k p c_k = \frac{u_k}{p} ck​=puk​​ 成立,其中 c k c_k ck​ 指最终 f ( x ) f(x) f(x) 和 g ( x ) g(x) g(x) 的乘法积的系数。

我们直接莽式子:
u k = ∑ i = 0 p − 1 y i ( ω p − k ) i = ∑ i = 0 p − 1 ( ∑ j = 0 p − 1 c j ( ω p i ) j ) ( ω p − k ) i = ∑ i = 0 p − 1 ∑ j = 0 p − 1 c j ( ω p j − k ) i = ∑ j = 0 p − 1 c j ∑ i = 0 p − 1 ( ω p j − k ) i \begin{aligned} u_k &= \sum_{i = 0}^{p - 1} y_i(\omega_p^{-k})^i \\ &= \sum_{i = 0}^{p - 1} (\sum_{j = 0}^{p - 1}c_j(\omega_p^i)^j)(\omega_p^{-k})^i \\ &= \sum_{i = 0}^{p - 1} \sum_{j = 0}^{p - 1}c_j(\omega_p^{j - k})^i \\ &= \sum_{j = 0}^{p - 1}c_j \sum_{i = 0}^{p - 1} (\omega_p^{j - k})^i \end{aligned} uk​​=i=0∑p−1​yi​(ωp−k​)i=i=0∑p−1​(j=0∑p−1​cj​(ωpi​)j)(ωp−k​)i=i=0∑p−1​j=0∑p−1​cj​(ωpj−k​)i=j=0∑p−1​cj​i=0∑p−1​(ωpj−k​)i​
然后对 ∑ i = 0 p − 1 ( ω p j − k ) i \sum_{i = 0}^{p - 1} (\omega_p^{j - k})^i ∑i=0p−1​(ωpj−k​)i 这一部分分讨,设 v = j − k v = j - k v=j−k,则该式等于 ∑ i = 0 p − 1 ( ω p v ) i \sum_{i = 0}^{p - 1} (\omega_p^v)^i ∑i=0p−1​(ωpv​)i。

在这个式子的意义下,这就是一个等比数列求和。

如果公比 ω p v = 1 \omega_p^v = 1 ωpv​=1,即 v = 0 v = 0 v=0,则此时原式等于 p p p;

否则,套用等比数列公式,原式等于:
( ω v ) p − 1 ω p v − 1 = ( ω p ) v − 1 ω p v − 1 = 1 − 1 ω p v − 1 = 0 \begin{aligned} &\frac{(\omega^v)^p - 1}{\omega_p^v - 1} \\ =&\frac{(\omega^p)^v - 1}{\omega_p^v - 1} \\ =&\frac{1 - 1}{\omega_p^v - 1} \\ =& 0 \end{aligned} ===​ωpv​−1(ωv)p−1​ωpv​−1(ωp)v−1​ωpv​−11−1​0​
故当且仅当 v = j − k = 0 v = j - k = 0 v=j−k=0,即 j = k j = k j=k 时,后面的这一部分才会产生贡献,则有 u k = p c k u_k = pc_k uk​=pck​,即 c k = 1 p u k c_k = \frac{1}{p}u_k ck​=p1​uk​。

得证。

然后可以发现, FFT ⁡ \operatorname{FFT} FFT 和 IFFT ⁡ \operatorname{IFFT} IFFT 的代码可以合为一份:

inline void FFT(Complex *f, int n, double t = 1.0)
{
	for(register int i = 0; i < n; i++) if(i < rev[i]) swap(f[i], f[rev[i]]);
	for(register int len = 2; len <= n; len <<= 1)
	{
		int k = len >> 1;
		Complex _w(cos(PI / k), t * sin(PI / k));
		for(register int i = 0; i < n; i += len)
		{
			Complex w(1.0, 0.0);
			for(register int j = 0; j < k; j++)
			{
				Complex u = f[i + j], v = w * f[i + j + k];
				f[i + j] = u + v;
				f[i + j + k] = u - v;
               	w = w * _w;
			}
		}
	}
   	if(t == -1) for(register int i = 0; i < n; i++) f[i].r /= n, f[i].i /= n;
	return;
}

当运行 FFT ⁡ \operatorname{FFT} FFT 时,我们令 t = 1 t = 1 t=1 即可;当运行 IFFT ⁡ \operatorname{IFFT} IFFT 时,我们令 t = − 1 t = -1 t=−1 即可。

(注意:由于 cos ⁡ \cos cos 和 sin ⁡ \sin sin 的精度本身就十分地不稳定,加上复数乘法的运算次数较多,我们最后计算答案的时候需要处理这个误差。)

这样,算法真正大功告成啦!


优化:

可以发现,对于一个复数 a + b i a + bi a+bi,有 ( a + b i ) 2 = ( a 2 − b 2 ) + 2 a b i (a + bi)^2 = (a^2 - b^2) + 2abi (a+bi)2=(a2−b2)+2abi。

如果我们把 g ( x ) g(x) g(x) 放到 f ( x ) f(x) f(x) 的虚部上,对 f ( x ) f(x) f(x) 跑一次 FFT ⁡ \operatorname{FFT} FFT,然后将 f ( x ) f(x) f(x) 的每个点值变为其平方。那么,虚部除以 2 2 2 不就是我们想要的值吗?

这样,我们就可以只跑一遍 FFT ⁡ \operatorname{FFT} FFT,一遍 IFFT ⁡ \operatorname{IFFT} IFFT 即可,常数优化至原来的 2 3 \frac{2}{3} 32​。


然而, FFT ⁡ \operatorname{FFT} FFT 的精度仍然是一个令人头疼的问题。我们想考虑一种方式,使得整个运算建立在整数域上。

整数,想到数论,有一个东西恰好符合其所有要求——原根。

关于原根的定义和性质,可以参考我的博客整除和同余相关理论。

可以发现,使得单位根可用于 FFT ⁡ \operatorname{FFT} FFT 的性质,原根同样满足:

  1. n n n 次原根计算公式为 g n k = g k ( p − 1 ) n   m o d     p g_n^k = g^{\frac{k(p - 1)}{n}} \bmod~p gnk​=gnk(p−1)​mod p,其中 p p p 为模数, g g g 特指模 p p p 的最小原根。

这里的 p p p 一般取 998244353 998244353 998244353,它的最小原根等于 3 3 3,并且有 998244353 = 2 23 × 7 × 17 + 1 998244353 = 2^{23}\times 7\times 17 + 1 998244353=223×7×17+1,其中 2 23 = 8388608 2^{23} = 8388608 223=8388608,其大小完全可以满足我们的需求。

同样,我们也可以得出 g n n = g n 0 ≡ 1   (   m o d     p ) g_n^n = g_n^0 \equiv 1~(\bmod ~p) gnn​=gn0​≡1 (mod p) 和 g n n 2 ≡ − 1   (   m o d     p ) g_n^{\frac{n}{2}} \equiv -1~(\bmod ~p) gn2n​​≡−1 (mod p) 以及它们的推论。

  1. 原根同样存在消去引理和折半引理。

所以,在 FFT ⁡ \operatorname{FFT} FFT 中对多项式的变换,在 NTT ⁡ \operatorname{NTT} NTT 中同样适用。同时,也需要 Rader ⁡ \operatorname{Rader} Rader 排序。

同理,对于 INTT ⁡ \operatorname{INTT} INTT,原根同样可以在原来的基础上求倒数再乘 1 n \frac{1}{n} n1​,只需要求逆元即可。

代码:

#define MOD 998244353
#define Min_G 3
#define Inv_Min_G 332748118

inline int Get_MOD(int k) { if(k >= MOD) k -= MOD; return k; }

inline int Pow(int a, int b)
{
	int res = 1;
	while(b)
	{
		if(b & 1) res = 1LL * res * a % MOD;
		a = 1LL * a * a % MOD;
		b >>= 1;
    }	
	return res;
}

inline void NTT(int *f, int n, bool Inverse = false)
{
	for(register int i = 0; i < n; i++) if(i < rev[i]) swap(f[i], f[rev[i]]);
	for(register int len = 2; len <= n; len <<= 1)
	{
		int k = (len >> 1), g = Pow(Inverse ? Inv_Min_G : Min_G, (MOD - 1) / len);
		for(register int i = 0; i < n; i += len)
		{
			int w = 1;
			for(register int j = 0; j < k; j++)
			{
				int u = f[i + j], v = 1LL * w * f[i + j + k] % MOD;
				f[i + j] = Get_MOD(u + v);
				f[i + j + k] = Get_MOD(u - v + MOD); 
				w = 1LL * w * g % MOD;
			}
		}
	}
	if(Inverse) 
	{
		int Inv_N = Pow(n, MOD - 2);
		for(register int i = 0; i < n; i++) f[i] = 1LL * f[i] * Inv_N % MOD;
	}
	return;
}

后记:

实际上, FWT ⁡ \operatorname{FWT} FWT 的思想和它们几乎是一模一样的,不过 FWT ⁡ \operatorname{FWT} FWT 没有涉及到较多数学知识,会相对简单一些。

强推神佬的视频:快速傅里叶变换( FFT ⁡ \operatorname{FFT} FFT)——有史以来最巧妙的算法?

强推神佬的博客: FFT ⁡ \operatorname{FFT} FFT NTT ⁡ \operatorname{NTT} NTT


多项式全家桶

到这里,正文才刚刚开始。。。

首先我们要知道一点求导和积分的前置知识。

对于要用到的结论,这里就直接摆出来了:
f ( x ) = e x ⇒ f ′ ( x ) = e x f ( x ) = ln ⁡ x ⇒ f ′ ( x ) = 1 x f ( x ) = a x b ⇒ f ′ ( x ) = a b x b − 1 ( f ( x ) ± g ( x ) ) ′ = f ′ ( x ) ± g ′ ( x ) ( f ( g ( x ) ) ′ = f ′ ( g ( x ) ) ∗ g ′ ( x ) \begin{aligned} & f(x) = e^x \Rightarrow f'(x) = e^x \\ & f(x) = \operatorname{ln} x \Rightarrow f'(x) = \frac{1}{x} \\ & f(x) = ax^b \Rightarrow f'(x) = abx^{b - 1} \\ & (f(x) \pm g(x))' = f'(x) \pm g'(x) \\ & (f(g(x))' = f'(g(x)) * g'(x) \end{aligned} ​f(x)=ex⇒f′(x)=exf(x)=lnx⇒f′(x)=x1​f(x)=axb⇒f′(x)=abxb−1(f(x)±g(x))′=f′(x)±g′(x)(f(g(x))′=f′(g(x))∗g′(x)​
其中 ′ ' ′ 表示求导。

所以我们可以推出多项式求导公式:
f ( x ) = ∑ i = 0 n a i x i ⇒ f ′ ( x ) = ∑ i = 0 n − 1 ( i + 1 ) a i + 1 x i f(x) = \sum_{i = 0}^n a_i x^i \Rightarrow f'(x) = \sum_{i = 0}^{n - 1}(i + 1)a_{i + 1} x^i f(x)=i=0∑n​ai​xi⇒f′(x)=i=0∑n−1​(i+1)ai+1​xi
当然,如果知道了多项式的导数,我们也可以用积分求出原多项式:
f ′ ( x ) = ∑ i = 0 n a i x i ⇒ f ( x ) = ∑ i = 1 n + 1 a i − 1 x i i f'(x) = \sum_{i = 0}^na_ix^i \Rightarrow f(x) = \sum_{i = 1}^{n + 1}\frac{a_{i - 1}x^i}{i} f′(x)=i=0∑n​ai​xi⇒f(x)=i=1∑n+1​iai−1​xi​
注意:多项式积分是不能得到正确的常数项的。不过大部分题目在数据上做了保证,可以直接认为其为 0 0 0。

这两个过程的代码写起来就很简单了,时间复杂度都是 O ( n ) \mathcal{O}(n) O(n) 的:

求导:(把度数为 n − 1 n-1 n−1 的多项式 f ( x ) f(x) f(x) 求导,结果为 g ( x ) g(x) g(x))

inline void Get_Derivative(int *f, int *g, int n)
{
	for(register int i = 1; i < n; i++)
        g[i - 1] = 1LL * i * f[i] % MOD;
	return g[n - 1] = 0, void();
}

积分:(把多项式 f ( x ) f(x) f(x) 求积分,结果为度数为 n − 1 n-1 n−1 的多项式 g ( x ) g(x) g(x))

inline void Get_Integral(int *f, int *g, int n)
{
	for(register int i = n - 1; i >= 1; i--)
        g[i] = 1LL * f[i - 1] * Pow(i, MOD - 2) % MOD;
	return g[0] = 0, void();
}

在文章后面的部分,有很多地方会把一些式子通过求导变成方便计算的形式,然后再用积分求出答案。


以上结论已经能够满足大部分 OI ⁡ \operatorname{OI} OI 题目的需求了。

如果对证明感兴趣,或者想知道更加严谨的结论,可以查找更多资料学习。(显然笔者也只会结论。。。)


多项式乘法逆元

给出一个度数为 n − 1 n - 1 n−1 的多项式 f ( x ) f(x) f(x),求出一个度数同为 n − 1 n - 1 n−1 多项式 g ( x ) g(x) g(x),满足 f ( x ) ∗ g ( x ) ≡ 1   (   m o d   x n ) f(x)*g(x) \equiv 1~(\bmod x^n) f(x)∗g(x)≡1 (modxn)。

其中 ∗ * ∗ 表示多项式乘法。系数对 998244353 998244353 998244353 取模。


先给出算法:倍增。

假如我们已经求出了一个多项式 t t t,满足 f ( x ) ∗ t ( x ) ≡ 1   (   m o d     x ⌈ n 2 ⌉ ) f(x) * t(x) \equiv 1~(\bmod~x^{\lceil\frac{n}{2}\rceil}) f(x)∗t(x)≡1 (mod x⌈2n​⌉)。

又因为 f ( x ) ∗ g ( x ) ≡ 1   (   m o d   x n ) ⇒ f ( x ) ∗ g ( x ) ≡ 1   (   m o d     x ⌈ n 2 ⌉ ) f(x)*g(x)\equiv 1~(\bmod x^n) \Rightarrow f(x) * g(x) \equiv 1~(\bmod~x^{\lceil\frac{n}{2}\rceil}) f(x)∗g(x)≡1 (modxn)⇒f(x)∗g(x)≡1 (mod x⌈2n​⌉)。

所以 f ( x ) ∗ ( g ( x ) − t ( x ) ) ≡ 0   (   m o d     x ⌈ n 2 ⌉ ) f(x) * (g(x) - t(x)) \equiv 0~(\bmod~x^{\lceil\frac{n}{2}\rceil}) f(x)∗(g(x)−t(x))≡0 (mod x⌈2n​⌉)。

f ( x ) f(x) f(x) 由于要求逆元,所以不可能为 0 0 0,那么有 g ( x ) − t ( x ) ≡ 0   (   m o d     x ⌈ n 2 ⌉ ) g(x) - t(x) \equiv 0~(\bmod~x^{\lceil\frac{n}{2}\rceil}) g(x)−t(x)≡0 (mod x⌈2n​⌉)。

平方后移项有: g 2 ( x ) ≡ 2 ∗ g ( x ) ∗ t ( x ) − t 2 ( x )   (   m o d     x n ) g^2(x) \equiv 2 * g(x) * t(x) - t^2(x)~(\bmod~x^{n}) g2(x)≡2∗g(x)∗t(x)−t2(x) (mod xn)。

然后两边再乘上一个 f ( x ) f(x) f(x): f ( x ) ∗ g 2 ( x ) ≡ 2 ∗ f ( x ) ∗ g ( x ) ∗ t ( x ) − f ( x ) ∗ t 2 ( x )   (   m o d     x n ) f(x) * g^2(x) \equiv 2 * f(x) * g(x) * t(x) - f(x) * t^2(x)~(\bmod~x^{n}) f(x)∗g2(x)≡2∗f(x)∗g(x)∗t(x)−f(x)∗t2(x) (mod xn)。

根据前面的结论,可以化简为: g ( x ) ≡ 2 ∗ t ( x ) − f ( x ) ∗ t 2 ( x )   (   m o d     x n ) g(x) \equiv 2 * t(x) - f(x) * t^2(x)~(\bmod~x^{n}) g(x)≡2∗t(x)−f(x)∗t2(x) (mod xn)。

我们知道 f ( x ) f(x) f(x) 和 t ( x ) t(x) t(x),那么自然也可以求出 g ( x ) g(x) g(x) 了。

对于常数项,直接求出其逆元即可。

时间复杂度 O ( n log ⁡ 2 n ) \mathcal{O}(n \log_2n) O(nlog2​n)。

贴上代码:(注意数组之间的覆盖与清零)

inline void Init(int n)
{
	p = 1, q = -1;
	while(p < n) p <<= 1, ++q;
	for(register int i = 0; i < p; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << q);
	return;
}

inline void Get_Operation_Sequence(int n, int *seq)
{
	seq[0] = 0;
	while(n != 1)
	{
		seq[++seq[0]] = n;
		n = (n + 1) >> 1;
	}
	seq[++seq[0]] = 1;
	return;
}

inline void Get_Inverse_Element(int *f, int *g, int n)
{
	Get_Operation_Sequence(n, seq[0]);
	for(register int k = seq[0][0]; k >= 1; k--)
	{
		int m = seq[0][k];
		if(m == 1)
		{
			g[0] = Pow(f[0], MOD - 2);
			continue;
		}
		Init(m << 1);
		memcpy(t, f, sizeof(int) * m);
		fill(t + m, t + p, 0);
		NTT(t, p), NTT(g, p);
		for(register int i = 0; i < p; i++) g[i] = 1LL * g[i] * Get_MOD(2LL - 1LL * t[i] * g[i] % MOD + MOD) % MOD;
		NTT(g, p, true), fill(g + m, g + p, 0);
		continue;
	}
}

多项式除法

(话说多项式除法的地位和多项式乘法比起来为何如此卑微。。。)

给出一个度数为 n − 1 n - 1 n−1 的多项式 f ( x ) f(x) f(x) 和一个度数为 m − 1 m - 1 m−1 多项式 g ( x ) g(x) g(x),请求出多项式 q ( x ) q(x) q(x) 和 r ( x ) r(x) r(x)。

满足:

  • f ( x ) = g ( x ) ∗ q ( x ) + r ( x ) f(x) = g(x) * q(x) + r(x) f(x)=g(x)∗q(x)+r(x);
  • q ( x ) q(x) q(x) 度数为 n − m n - m n−m, r ( x ) r(x) r(x) 度数小于 m − 1 m - 1 m−1;

所有系数对 998244353 998244353 998244353 取模。


没记错的话,初中数学老师讲过一种 “大除法”。

不过本题是在模意义下计算的,所以说这种方法不是很适用。

接下来,我们需要考虑用先前学过的知识

那么这操作就很关键了:系数翻转。

(为了方便,下文设 f ( x ) f(x) f(x) 度数为 n n n,设 g ( x ) g(x) g(x) 度数为 m m m,其余的度数变化同理。)

我们设对 f ( x ) f(x) f(x) 的第 0 ∼ n 0\sim n 0∼n 个系数翻转后为 f r ( n ) ( x ) f^{r(n)}(x) fr(n)(x)。

(注意下文不会刻意地区分 f r ( n ) ( x ) f^{r(n)}(x) fr(n)(x) 中 r ( n ) r(n) r(n) 和余数 r ( x ) r(x) r(x) 的区别。)

那么可以发现有:
f r ( n ) ( x ) = ∑ i = 0 n a n − i x i ⇓ f r ( n ) ( x ) = x n f ( 1 x ) f^{r(n)}(x) = \sum_{i = 0}^{n} a_{n - i} x^i \\ \Downarrow \\ f^{r(n)}(x) = x^{n} f(\frac{1}{x}) fr(n)(x)=i=0∑n​an−i​xi⇓fr(n)(x)=xnf(x1​)
又因为 f ( 1 x ) = g ( 1 x ) ∗ q ( 1 x ) + r ( 1 x ) f(\frac{1}{x}) = g(\frac{1}{x}) * q(\frac{1}{x}) + r(\frac{1}{x}) f(x1​)=g(x1​)∗q(x1​)+r(x1​),所以:
f r ( n ) ( x ) = x n ∗ ( g ( x ) ∗ q ( x ) + r ( x ) ) = x n ∗ ( g ( x ) ∗ q ( x ) ) + x n r ( x ) = x m ∗ g ( x ) ∗ x n − m ∗ q ( x ) + x n r ( x ) = g r ( m ) ( x ) ∗ q r ( n − m ) ( x ) + r r ( n ) ( x ) \begin{aligned} f^{r(n)}(x) &= x^n * (g(x) * q(x) + r(x)) \\ &= x^n * (g(x) * q(x)) + x^n r(x) \\ &= x^m * g(x) * x^{n - m} * q(x) + x^nr(x) \\ &= g^{r(m)}(x) * q^{r(n - m)}(x) + r^{r(n)}(x) \end{aligned} fr(n)(x)​=xn∗(g(x)∗q(x)+r(x))=xn∗(g(x)∗q(x))+xnr(x)=xm∗g(x)∗xn−m∗q(x)+xnr(x)=gr(m)(x)∗qr(n−m)(x)+rr(n)(x)​
然后,既然 r ( x ) r(x) r(x) 的度数小于 m m m,那么翻转过后, r r ( n ) ( x ) r^{r(n)}(x) rr(n)(x) 的前 n − m n - m n−m 个系数就必然为 0 0 0。

所以,如果我们在   m o d     x n − m + 1 \bmod~x^{n - m + 1} mod xn−m+1 意义下做上述运算,不仅可以去掉余数 r ( x ) r(x) r(x) 的影响,同时我们还可以发现,这样完美地保留了 q ( x ) q(x) q(x)。

那么有:
f r ( n ) ( x ) ≡ g r ( m ) ( x ) ∗ q r ( n − m ) ( x )   (   m o d     x n − m + 1 ) ⇓ q r ( n − m ) ( x ) ≡ g r ( m ) ( x ) f r ( n ) ( x )   (   m o d     x n − m + 1 ) ⇓ q ( x ) = ( g r ( m ) ( x ) f r ( n ) ( x ) ) r ( n − m )   (   m o d     x n − m + 1 ) f^{r(n)}(x) \equiv g^{r(m)}(x) * q^{r(n - m)}(x)~(\bmod~x^{n - m + 1}) \\ \Downarrow \\ q^{r(n - m)}(x) \equiv \frac{g^{r(m)}(x)}{f^{r(n)}(x)} ~(\bmod~x^{n - m + 1}) \\ \Downarrow \\ q(x) = (\frac{g^{r(m)}(x)}{f^{r(n)}(x)})^{r(n - m)} ~(\bmod~x^{n - m + 1}) fr(n)(x)≡gr(m)(x)∗qr(n−m)(x) (mod xn−m+1)⇓qr(n−m)(x)≡fr(n)(x)gr(m)(x)​ (mod xn−m+1)⇓q(x)=(fr(n)(x)gr(m)(x)​)r(n−m) (mod xn−m+1)
至于余数,我们直接把 g ( x ) g(x) g(x) 和 q ( x ) q(x) q(x) 乘起来,再用 f ( x ) f(x) f(x) 减去即可。

代码:

inline void Multiply(int *f, int n, int *g, int m, int *v)
{
    Init(n + m - 1);
	memcpy(h, f, sizeof(int) * n), fill(h + n, h + p, 0);
	memcpy(s, g, sizeof(int) * m), fill(s + m, s + p, 0);
	NTT(h, p), NTT(s, p);
	for(register int i = 0; i < p; i++) v[i] = 1LL * h[i] * s[i] % MOD;
	return NTT(v, p, true), fill(v + n + m - 1, v + p, 0), void();
}

inline void Divide(int *f, int n, int *g, int m, int *v)
{
	memcpy(s, g, sizeof(int) * m);
	reverse(s, s + m);
	Get_Inverse_Element(s, r, n - m + 1);
	memcpy(t, f, sizeof(int) * n);
	reverse(t, t + n);
	Multiply(t, n, r, n - m + 1, v);
	return reverse(v, v + n - m + 1);
}

inline void Get_Remainder(int *f, int n, int *g, int m, int *v, int *x)
{
	Multiply(g, m, v, n - m + 1, x);
	for(register int i = 0; i < m; i++) x[i] = Get_MOD(f[i] - x[i] + MOD);
	return;
}

多项式开根

给出一个度数为 n − 1 n - 1 n−1 的多项式 f ( x ) f(x) f(x),求出一个度数同为 n − 1 n - 1 n−1 多项式 g ( x ) g(x) g(x),满足 g 2 ( x ) ≡ f ( x )   (   m o d   x n ) g^2(x) \equiv f(x)~(\bmod x^n) g2(x)≡f(x) (modxn)。

保证有 [ x 0 ] f ( x ) = 1 [x^0]f(x) = 1 [x0]f(x)=1。系数对 998244353 998244353 998244353 取模。


我们还是用倍增法来做。

临界条件: n = 1 n = 1 n=1 时, [ x 0 ] g ( x ) = 1 [x^0]g(x) = 1 [x0]g(x)=1。

设已经求出了一个多项式 t ( x ) t(x) t(x),满足 t 2 ( x ) ≡ f ( x   (   m o d     x ⌈ n 2 ⌉ ) t^2(x) \equiv f(x~(\bmod~x^{\lceil\frac{n}{2}\rceil}) t2(x)≡f(x (mod x⌈2n​⌉)。

同时设 g ( x ) 2 ≡ f ( x )   (   m o d   x n ) g(x)^2\equiv f(x)~(\bmod x^n) g(x)2≡f(x) (modxn)。

然后又是一波推式子:
t 2 ( x ) ≡ f ( x )   (   m o d     x ⌈ n 2 ⌉ ) t 2 ( x ) − f ( x ) ≡ 0   (   m o d     x ⌈ n 2 ⌉ ) ( t 2 ( x ) − f ( x ) ) 2 ≡ 0   (   m o d     x n ) ( t 2 ( x ) + f ( x ) ) 2 ≡ 4 ∗ t 2 ( x ) ∗ f ( x )   (   m o d     x n ) ( t 2 ( x ) + f ( x ) 2 ∗ t ( x ) ) 2 ≡ f ( x )   (   m o d     x n ) t 2 ( x ) + f ( x ) 2 ∗ t ( x ) ≡ g ( x )   (   m o d     x n ) t ( x ) 2 + f ( x ) 2 ∗ t ( x ) ≡ g ( x )   (   m o d     x n ) \begin{aligned} & t^2(x) \equiv f(x)~(\bmod~x^{\lceil\frac{n}{2}\rceil}) \\ & t^2(x) - f(x)\equiv 0~(\bmod~x^{\lceil\frac{n}{2}\rceil}) \\ & (t^2(x) - f(x))^2 \equiv 0~(\bmod~x^n) \\ & (t^2(x) + f(x))^2 \equiv 4* t^2(x) * f(x)~(\bmod~x^n) \\ & (\frac{t^2(x) + f(x)}{2*t(x)})^2 \equiv f(x)~(\bmod~x^n) \\ & \frac{t^2(x) + f(x)}{2*t(x)} \equiv g(x)~(\bmod~x^n) \\ & \frac{t(x)}{2} + \frac{f(x)}{2 * t(x)} \equiv g(x)~(\bmod~x^n) \end{aligned} ​t2(x)≡f(x) (mod x⌈2n​⌉)t2(x)−f(x)≡0 (mod x⌈2n​⌉)(t2(x)−f(x))2≡0 (mod xn)(t2(x)+f(x))2≡4∗t2(x)∗f(x) (mod xn)(2∗t(x)t2(x)+f(x)​)2≡f(x) (mod xn)2∗t(x)t2(x)+f(x)​≡g(x) (mod xn)2t(x)​+2∗t(x)f(x)​≡g(x) (mod xn)​
推到这里式子就好做了。时间复杂度 O ( n log ⁡ 2 n ) \mathcal{O}(n \log_2n) O(nlog2​n)。

当然还有一种常数较小的写法:在维护 t ( x ) t(x) t(x) 的同时维护 1 t ( x ) \frac{1}{t(x)} t(x)1​,因为开根的递归结构和求逆是一样的。

贴上代码:(没有写这种常数较小的写法)

inline void Get_Sqrt(int *f, int *g, int n)
{
	Get_Operation_Sequence(n, seq[1]);
	for(register int k = seq[1][0]; k >= 1; k--)
	{
		int m = seq[1][k];
		if(m == 1)
		{
			g[0] = Quadratic_Residue :: Solve(f[0]);
			continue;
		}
		Init(m << 1);
		memset(h, 0, sizeof(int) * p);
		Get_Inverse_Element(g, h, m);
		memcpy(s, f, sizeof(int) * m);
		fill(s + m, s + p, 0);
		NTT(g, p), NTT(h, p), NTT(s, p);
		int Inv2 = Pow(2, MOD - 2);
		for(register int i = 0; i < p; i++) g[i] = 1LL * Inv2 * Get_MOD(g[i] + 1LL * h[i] * s[i] % MOD) % MOD;
		NTT(g, p, true), fill(g + m, g + p, 0);
		continue;
	}
	return;
}

当然,还可以去做一下加强版,仅需要在求常数项时套上一个二次剩余即可。

写好二次剩余后,就只需要改 Get_Sqrt函数的第一行即可。

代码就不放出来了。


多项式对数函数

给出一个度数为 n − 1 n - 1 n−1 的多项式 f ( x ) f(x) f(x),求出一个度数同为 n − 1 n - 1 n−1 多项式 g ( x ) g(x) g(x),满足 g ( x ) ≡ ln ⁡ ( f ( x ) )   (   m o d   x n ) g(x) \equiv \operatorname{ln}(f(x))~(\bmod x^n) g(x)≡ln(f(x)) (modxn)。

保证有 [ x 0 ] f ( x ) = 1 [x^0]f(x) = 1 [x0]f(x)=1。系数对 998244353 998244353 998244353 取模。


看到这里,大多数读者应该都是一脸懵:怎么对一个多项式求自然对数啊?

不要急,还记得前置知识吗?求导和积分。

前面还提到了一句话:把一些式子通过求导变成方便计算的形式,然后再用积分求出答案。

那么,做法已经呼之欲出了:

对 g ( x ) g(x) g(x) 求导有: g ′ ( x ) ≡ ln ⁡ ′ ( f ( x ) ) ∗ f ′ ( x ) ≡ f ′ ( x ) f ( x )   (   m o d     x n ) g'(x) \equiv \operatorname{ln}'(f(x)) * f'(x) \equiv \frac{f'(x)}{f(x)}~(\bmod~x^n) g′(x)≡ln′(f(x))∗f′(x)≡f(x)f′(x)​ (mod xn)。

这不就是求一个导数和一个逆元吗?

最后积分回去就好啦。

对于常数项,显然有 e 0 = 1 e^0 = 1 e0=1,即 ln ⁡ 1 = 0 \ln 1 = 0 ln1=0,所以保证了 [ x 0 ] f ( x ) = 1 [x^0]f(x) = 1 [x0]f(x)=1,否则是很难做的。

时间复杂度 O ( n log ⁡ 2 n ) \mathcal{O}(n \log_2n) O(nlog2​n)。

贴上代码:

inline void Get_Ln(int *f, int *g, int n)
{
	Get_Inverse_Element(f, g, n);
	memset(t, 0, sizeof(int) * p);
	Get_Derivative(f, t, n);
	NTT(g, p), NTT(t, p);
	for(register int i = 0; i < p; i++) g[i] = 1LL * g[i] * t[i] % MOD;
	return NTT(g, p, true), Get_Integral(g, g, n), void();
}

多项式牛顿迭代

给出一个度数为 n − 1 n - 1 n−1 的多项式 f ( x ) f(x) f(x),求出一个度数同为 n − 1 n - 1 n−1 多项式 g ( x ) g(x) g(x),满足 f ( g ( x ) ) ≡ 0   (   m o d   x n ) f(g(x)) \equiv 0~(\bmod x^n) f(g(x))≡0 (modxn)。

保证有 [ x 0 ] f ( x ) = 0 [x^0]f(x) = 0 [x0]f(x)=0。系数对 998244353 998244353 998244353 取模。


首先需要知道前置知识:泰勒展开。

对于多项式 f ( x ) f(x) f(x),如果我们已经知道了 f ( x 0 ) f(x_0) f(x0​) 的值,那么 f ( x ) f(x) f(x) 的值可以用如下方法表示:
f ( x ) = ∑ i = 0 ∞ ( f ( i ) ( x 0 ) i ! ∗ ( x − x 0 ) i ) = f ( x 0 ) + ∑ i = 1 ∞ ( f ( i ) ( x 0 ) i ! ∗ ( x − x 0 ) i ) f(x) = \sum_{i = 0}^{\infin} (\frac{f^{(i)}(x_0)}{i!} * (x - x_0)^i) = f(x_0) + \sum_{i = 1}^{\infin} (\frac{f^{(i)}(x_0)}{i!} * (x - x_0)^i) f(x)=i=0∑∞​(i!f(i)(x0​)​∗(x−x0​)i)=f(x0​)+i=1∑∞​(i!f(i)(x0​)​∗(x−x0​)i)
其中 f ( i ) ( x 0 ) f^{(i)}(x_0) f(i)(x0​) 指的是多项式 f ( x ) f(x) f(x) 在 x 0 x_0 x0​ 处的 i i i 阶导数,简单来讲就是对它求 i i i 次导数得出的值。

这个公式的证明和其它用法大可不必知道,我们只会用它做一些最基本的操作,不会超出我们的认知范围。


还是考虑倍增。

对于 n = 1 n = 1 n=1 的情况,此时我们一般会默认 [ x 0 ] g ( x ) = 1 [x^0]g(x) = 1 [x0]g(x)=1。

设求出了一个多项式 t ( x ) t(x) t(x),满足 f ( t ( x ) ) ≡ 0   (   m o d     x ⌈ n 2 ⌉ ) f(t(x)) \equiv 0~(\bmod~x^{\lceil\frac{n}{2}\rceil}) f(t(x))≡0 (mod x⌈2n​⌉)。

那么我们要求多项式 g ( x ) g(x) g(x),满足 f ( g ( x ) ) ≡ 0   (   m o d     x n ) f(g(x)) \equiv 0~(\bmod~x^{n}) f(g(x))≡0 (mod xn)。

又可以得出: f ( g ( x ) ) ≡ 0   (   m o d     x ⌈ n 2 ⌉ ) f(g(x)) \equiv 0~(\bmod~x^{\lceil\frac{n}{2}\rceil}) f(g(x))≡0 (mod x⌈2n​⌉)。

如果您阅读完了上面的《拉格朗日插值》一节,就一定可以知道这个定理:

对于多项式 f ( x ) f(x) f(x),必然满足 f ( x ) ≡ f ( a )   (   m o d   ( x − a ) ) f(x) \equiv f(a)~(\bmod (x - a)) f(x)≡f(a) (mod(x−a))。

那么我们很快可以得出结论: g ( x ) − t ( x ) ≡ 0   (   m o d     x ⌈ n 2 ⌉ ) g(x) - t(x) \equiv 0~(\bmod~x^{\lceil\frac{n}{2}\rceil}) g(x)−t(x)≡0 (mod x⌈2n​⌉)。

而这个结论有什么作用呢?往下看。

我们要求的是 g ( x ) g(x) g(x),满足 f ( g ( x ) ) ≡ 0   (   m o d     x n ) f(g(x)) \equiv 0~(\bmod~x^{n}) f(g(x))≡0 (mod xn)。

那么我们在 t ( x ) t(x) t(x) 处进行泰勒展开,得到:

f ( g ( x ) ) ≡ f ( t ( x ) ) + ∑ i = 1 ∞ ( f ( i ) ( t ( x ) ) i ! ∗ ( g ( x ) − t ( x ) ) i ) ≡ 0   (   m o d     x n ) f(g(x)) \equiv f(t(x)) + \sum_{i = 1}^{\infin} (\frac{f^{(i)}(t(x))}{i!} * (g(x) - t(x))^i) \equiv 0~(\bmod ~x^{n}) f(g(x))≡f(t(x))+i=1∑∞​(i!f(i)(t(x))​∗(g(x)−t(x))i)≡0 (mod xn)

又由于上面的结论,我们可以发现,在 ( g ( x ) − t ( x ) ) i (g(x) - t(x))^i (g(x)−t(x))i 这个式子中,当 i ≥ 2 i\ge 2 i≥2 时,前 n n n 位必然为 0 0 0。

所以整个式子就等于:
f ( g ( x ) ) ≡ f ( t ( x ) ) + f ′ ( t ( x ) ) ∗ ( g ( x ) − t ( x ) ) ≡ 0   (   m o d     x n ) f(g(x)) \equiv f(t(x)) + f'(t(x)) * (g(x) - t(x)) \equiv 0~(\bmod~x^{n}) f(g(x))≡f(t(x))+f′(t(x))∗(g(x)−t(x))≡0 (mod xn)
稍加推导一下就有:
g ( x ) = t ( x ) − f ( t ( x ) ) f ′ ( t ( x ) )   (   m o d     x n ) g(x) = t(x) - \frac{f(t(x))}{f'(t(x))}~(\bmod~x^{n}) g(x)=t(x)−f′(t(x))f(t(x))​ (mod xn)
这样就可以倍增求出 g ( x ) g(x) g(x) 了。时间复杂度 O ( n log ⁡ 2 n ) \mathcal{O}(n \log_2n) O(nlog2​n)。


多项式指数函数

给出一个度数为 n − 1 n - 1 n−1 的多项式 f ( x ) f(x) f(x),求出一个度数同为 n − 1 n - 1 n−1 多项式 g ( x ) g(x) g(x),满足 g ( x ) ≡ e f ( x )   (   m o d   x n ) g(x) \equiv e^{f(x)}~(\bmod x^n) g(x)≡ef(x) (modxn)。

保证有 [ x 0 ] f ( x ) = 0 [x^0]f(x) = 0 [x0]f(x)=0。系数对 998244353 998244353 998244353 取模。


显然把 e f ( x ) e^{f(x)} ef(x) 求导不会有任何变化。

所以,我们考虑用刚刚学到的牛顿迭代法做这道题。

我们构造出一个多项式(不管它的实际系数) h ( g ( x ) ) ≡ ln ⁡ ( g ( x ) ) − f ( x )   (   m o d     x n ) h(g(x)) \equiv \operatorname{ln}(g(x)) - f(x)~(\bmod~x^n) h(g(x))≡ln(g(x))−f(x) (mod xn)。

那么把 g ( x ) ≡ e f ( x )   (   m o d   x n ) g(x) \equiv e^{f(x)}~(\bmod x^n) g(x)≡ef(x) (modxn) 代入就有:
h ( g ( x ) ) ≡ ln ⁡ ( g ( x ) ) − f ( x ) ≡ ln ⁡ ( e f ( x ) ) − f ( x ) ≡ f ( x ) − f ( x ) ≡ 0   (   m o d     x n ) h(g(x)) \equiv \operatorname{ln}(g(x)) - f(x)\equiv\operatorname{ln}(e^{f(x)}) - f(x) \equiv f(x) - f(x)\equiv0~(\bmod~x^n) h(g(x))≡ln(g(x))−f(x)≡ln(ef(x))−f(x)≡f(x)−f(x)≡0 (mod xn)
然后我们就可以套用上面的多项式牛顿迭代了:
g ( x ) ≡ t ( x ) − ln ⁡ ( t ( x ) ) − f ( x ) 1 t ( x ) ≡ t ( x ) − t ( x ) ∗ ( ln ⁡ ( t ( x ) ) − f ( x ) )   (   m o d     x n ) g(x) \equiv t(x) - \frac{\operatorname{ln}(t(x))-f(x)}{\frac{1}{t(x)}}\equiv t(x) - t(x) * (\operatorname{ln}(t(x)) - f(x))~(\bmod~x^{n}) g(x)≡t(x)−t(x)1​ln(t(x))−f(x)​≡t(x)−t(x)∗(ln(t(x))−f(x)) (mod xn)
注意一点: f ( x ) f(x) f(x) 在求导的时候是被看作常数的。

对于常数项,可以参考上文《多项式对数函数》的证明。

时间复杂度 O ( n log ⁡ 2 n ) \mathcal{O}(n \log_2n) O(nlog2​n)。

代码:

inline void Get_Exp(int *f, int *g, int n)
{
	Get_Operation_Sequence(n, seq[2]);
	for(register int k = seq[2][0]; k >= 1; k--)
	{
		int m = seq[2][k];
		if(m == 1)
		{
			g[0] = 1;
			continue;
		}
		Get_Ln(g, h, m);
		Init(m << 1);
		fill(h + m, h + p, 0);
		memcpy(s, f, sizeof(int) * m);
		fill(s + m, s + p, 0);
		NTT(g, p), NTT(h, p), NTT(s, p);
		for(register int i = 0; i < p; i++) g[i] = 1LL * g[i] * Get_MOD(Get_MOD(1LL - h[i] + MOD) + s[i]) % MOD;
		NTT(g, p, true), fill(g + m, g + p, 0), memset(h, 0, sizeof(int) * p), void();
	}
}

学完了这里,您可以尝试用其去推一推多项式求逆和多项式开根的式子,可以发现和原来推出来的式子是一样的。


多项式快速幂

给出一个度数为 n − 1 n - 1 n−1 的多项式 f ( x ) f(x) f(x) 和一个正整数 k k k,其中 k ≤ 1 0 1 0 5 k \le 10^{10^5} k≤10105,求出一个度数同为 n − 1 n - 1 n−1 多项式 g ( x ) g(x) g(x),满足 g ( x ) ≡ e f ( x )   (   m o d   x n ) g(x) \equiv e^{f(x)}~(\bmod x^n) g(x)≡ef(x) (modxn)。

保证有 [ x 0 ] f ( x ) = 1 [x^0]f(x) = 1 [x0]f(x)=1。系数对 998244353 998244353 998244353 取模。


如果刷高中数学题刷得有感觉,应该可以一眼看出这里可以求 ln ⁡ \ln ln 后求 exp ⁡ \exp exp。

所以,我们求出 t ( x ) = ln ⁡ ( f ( x ) ) t(x) = \ln(f(x)) t(x)=ln(f(x)),那么有 ln ⁡ ( g ( x ) ) = k ∗ t ( x ) \ln(g(x)) = k * t(x) ln(g(x))=k∗t(x)。

于是我们把 t ( x ) t(x) t(x) 的每一项的系数乘上一个 k k k,最后对 t ( x ) t(x) t(x) 求 exp ⁡ \exp exp,得到的即为 g ( x ) g(x) g(x)。

时间复杂度 O ( n log ⁡ 2 n ) \mathcal{O}(n \log_2n) O(nlog2​n)。


不过在贴代码前,有一个地方需要思考: k k k 在读入时应该对什么取模?

可能会有人脱口而出是 φ ( 998244353 ) \varphi(998244353) φ(998244353)。但是,需要郑重声明的是:欧拉定理在多项式的运算意义下是不成立的,正确的模数应当为 998244353 998244353 998244353 本身(阅读完下文后,可以发现这也是规定了 [ x 0 ] f ( x ) = 1 [x^0]f(x) = 1 [x0]f(x)=1 的原因)。

证明:

我们考虑证明一个更一般的情况: f p ( x ) ≡ f ( x p )   (   m o d     p ) f^p(x) \equiv f(x^p)~(\bmod~p) fp(x)≡f(xp) (mod p)。

由二项式定理和 L u c a s Lucas Lucas 定理可以得出: ( a + b ) p ≡ a p + b p   (   m o d     p ) (a + b)^p \equiv a^p + b^p~(\bmod~p) (a+b)p≡ap+bp (mod p)。

那么由数学归纳法,我们设 f ( x ) f(x) f(x) 度数为 k k k 且 f ( x ) = g ( x ) + a k x k f(x) = g(x) + a_kx^k f(x)=g(x)+ak​xk,同时上述结论对于度数为 k − 1 k - 1 k−1 的多项式成立。

则有 f p ( x ) ≡ g p ( x ) + a k p x k p ≡ g ( x p ) + a k x k p ≡ f ( x p )   (   m o d     p ) f^p(x) \equiv g^p(x) + a_k^px^{kp} \equiv g(x^p) + a_kx^{kp}\equiv f(x^p)~(\bmod~p) fp(x)≡gp(x)+akp​xkp≡g(xp)+ak​xkp≡f(xp) (mod p)。(这里可以使用欧拉定理)

在这道题中,由于 n < p n < p n<p,所以在模 p p p 意义下有 f p ( x ) ≡ f ( x p )   (   m o d     x n ) f^p(x)\equiv f(x^p) ~(\bmod~x^n) fp(x)≡f(xp) (mod xn)。

由因为 [ x 0 ] f ( x ) = 1 [x^0]f(x) = 1 [x0]f(x)=1,所以在模 p p p 意义下 f p ( x ) ≡ 1   (   m o d   x n ) f^p(x) \equiv 1~(\bmod x^n) fp(x)≡1 (modxn)。


代码:

inline int Read_Get_MOD()
{
	int res = 0; char c = getchar();
	while(!isdigit(c)) c = getchar();
	while(isdigit(c))
	{
		res = (10LL * res + (c - '0')) % MOD; 
		c = getchar();
	}
	return res;
}

inline void Get_Quick_Pow(int *f, int *g, int n, int k)
{
	Get_Ln(f, r, n);
	for(register int i = 0; i < p; i++) r[i] = 1LL * r[i] * k % MOD;
	return Get_Exp(r, g, n);
}

加强版不保证 [ x 0 ] f ( x ) = 1 [x^0]f(x) = 1 [x0]f(x)=1?

我们如果把所有数除去常数项,就可以仿照上述方法了。当然,不要忘记在最后把这个除掉的数乘回去(注意这个数是整数,根据费马小定理,它的指数需要对 φ ( 998244353 ) \varphi(998244353) φ(998244353) 取模)。

不过,当然会存在常数项为 0 0 0 的情况。这也很好解决,我们只要找到第一个 a t a_t at​(系数)不为 0 0 0 的 t t t,就可以套用如下公式:
f k ( x ) = x t k ∗ ( f ( x ) x t ) k f^k(x) = x^{tk} * (\frac{f(x)}{x^t})^k fk(x)=xtk∗(xtf(x)​)k
从本质上讲,这个操作就是降次之后升次。

注意这个式子中的 k k k 还是对 998244353 998244353 998244353 取模。

贴上代码:(由于常数过大,不吸氧只有 80 80 80 分,不过笔者实在是卡不动了。。。)

inline void Read_Get_MOD(int &k1, int &k2, int &k3)
{
	k1 = k2 = k3 = 0;
	char c = getchar();
	while(!isdigit(c)) c = getchar();
	while(isdigit(c))
	{
		k1 = (10LL * k1 + (c - '0')) % MOD; 
		k2 = (10LL * k2 + (c - '0')) % (MOD - 1);
		if(k3 < n) k3 = 10 * k3 + (c - '0'); 
		c = getchar();
	}
	return;
}

inline void Get_Quick_Pow(int *f, int *g, int n, int k1, int k2)
{
	int st = 0;
	while(st < n && f[st] == 0) ++st;
	if(st == n || 1LL * st * k1 > n)
	{
		for(register int i = 0; i < n; i++) putchar('0'), putchar(' ');
		putchar('\n');
		return;
	}
	int inv = Pow(f[st], MOD - 2), mul = Pow(f[st], k2);
	for(register int i = 0; i < n; i++) f[i] = 1LL * f[i + st] * inv % MOD;
	Get_Ln(f, r, n);
	for(register int i = 0; i < p; i++) r[i] = 1LL * r[i] * k1 % MOD;
	Get_Exp(r, g, n);
	int tk = st * k1;
	for(register int i = 0; i < tk; i++) putchar('0'), putchar(' ');
	for(register int i = tk; i < n; i++) printf("%lld ", 1LL * g[i - tk] * mul % MOD);
	putchar('\n');
	return;
}

inline void Run()
{
    int k1, k2, k3;
	scanf("%d", &n);
	Read_Get_MOD(k1, k2, k3);
	for(register int i = 0; i < n; i++) scanf("%d ", f + i);
	if(f[0] == 0 && k3 >= n)
	{
		for(register int i = 0; i < n; i++) putchar('0'), putchar(' ');
		putchar('\n');
		return;
	}
	Get_Quick_Pow(f, g, n, k1, k2);
	return;	
}

最后,附上这篇文章的总代码。

除快速幂的加强版需要吸氧之外,上文提到的所有模板题均可通过,仅需改写 Run()函数即可。

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<cctype>
#include<ctime>
#include<algorithm>

using namespace std;

struct Complex
{
	double r, i;
	
	Complex(double _r = 0.0, double _i = 0.0) : r(_r), i(_i) {}
	
	Complex operator + (const Complex &t) const { return Complex(r + t.r, i + t.i); }
	Complex operator - (const Complex &t) const { return Complex(r - t.r, i - t.i); }
	Complex operator * (const Complex &t) const { return Complex(r * t.r - i * t.i, r * t.i + i * t.r); }
};

namespace Quadratic_Residue
{
	#define MOD 998244353
	
	int si;

	struct Complex_MOD
	{
		int a, b;

		Complex_MOD operator * (const Complex_MOD &t) const
		{
			Complex_MOD res;
			res.a = (1LL * a * t.a % MOD + 1LL * b * t.b % MOD * si % MOD) % MOD;
			res.b = (1LL * a * t.b % MOD + 1LL * b * t.a % MOD) % MOD;
			return res;
		}
	};

	inline int Pow_Real(int a, int b)
	{
		int res = 1;
		while(b)
		{
			if(b & 1) res = 1LL * res * a % MOD;
			a = 1LL * a * a % MOD;
			b >>= 1;
		}
		return res;
	}

	inline int Pow_Complex(Complex_MOD val, int t)
	{
		Complex_MOD res;
		res.a = 1, res.b = 0;
		while(t)
		{
			if(t & 1) res = res * val;
			val = val * val;
			t >>= 1;
		}
		return res.a;
	}

	inline int Solve(int n)
	{
		if(n == 0) return 0;
		srand((unsigned)(time(NULL)));
		int p1, p2;
		while(true)
		{
			p1 = 1LL * rand() * rand() % MOD;
			p2 = (1LL * p1 * p1 % MOD - n + MOD) % MOD;
			if(Pow_Real(p2, (MOD - 1) / 2) != 1) break;
		}
		si = p2;
		Complex_MOD val;
		val.a = p1, val.b = 1;
		int ans = Pow_Complex(val, (MOD + 1) / 2);
		if(MOD - ans < ans) ans = MOD - ans;
		return ans;
	}

	#undef MOD
}

namespace Polynomial
{
	#define MAXN 5000005
	#define MOD 998244353
	#define Min_G 3
	#define Inv_Min_G 332748118

	int n, f[MAXN], g[MAXN], t[MAXN], h[MAXN], s[MAXN], r[MAXN], u[MAXN];
	int p, q, rev[MAXN], seq[3][MAXN];
	const double PI = acos(-1.0);

	inline int Get_MOD(int k) { if(k >= MOD) k -= MOD; return k; }

	inline void Read_Get_MOD(int &k1, int &k2, int &k3)
	{
		k1 = k2 = k3 = 0;
		char c = getchar();
		while(!isdigit(c)) c = getchar();
		while(isdigit(c))
		{
			k1 = (10LL * k1 + (c - '0')) % MOD; 
			k2 = (10LL * k2 + (c - '0')) % (MOD - 1);
			if(k3 < n) k3 = 10 * k3 + (c - '0'); 
			c = getchar();
		}
		return;
	}

	inline int Pow(int a, int b)
	{
		int res = 1;
		while(b)
		{
			if(b & 1) res = 1LL * res * a % MOD;
			a = 1LL * a * a % MOD;
			b >>= 1;
		}
		return res;
	}

	inline void Init(int n)
	{
		p = 1, q = -1;
		while(p < n) p <<= 1, ++q;  
		for(register int i = 0; i < p; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << q);
		return;
	}

	inline void Get_Operation_Sequence(int n, int *seq)
	{
		seq[0] = 0;
		while(n != 1)
		{
			seq[++seq[0]] = n;
			n = (n + 1) >> 1;
		}
		seq[++seq[0]] = 1;
		return;
	}

	inline void Get_Derivative(int *f, int *g, int n)
	{
		for(register int i = 1; i < n; i++)
			g[i - 1] = 1LL * i * f[i] % MOD;
		return g[n - 1] = 0, void();
	}

	inline void Get_Integral(int *f, int *g, int n)
	{
		for(register int i = n - 1; i >= 1; i--)
			g[i] = 1LL * f[i - 1] * Pow(i, MOD - 2) % MOD;
		return g[0] = 0, void();
	}

	inline void FFT(Complex *f, int n, double t = 1.0)
	{
		for(register int i = 0; i < n; i++) if(i < rev[i]) swap(f[i], f[rev[i]]);
		for(register int len = 2; len <= n; len <<= 1)
		{
	   	    int k = len >> 1;
			Complex _w(cos(PI / k), t * sin(PI / k));
			for(register int i = 0; i < n; i += len)
			{
				Complex w(1.0, 0.0);
				for(register int j = 0; j < k; j++)
				{
					Complex u = f[i + j], v = w * f[i + j + k];
					f[i + j] = u + v;
					f[i + j + k] = u - v;
           	     	w = w * _w;
				}
			}
		}
  	  	if(t == -1) for(register int i = 0; i < n; i++) f[i].r /= n, f[i].i /= n;
		return;
	}

	inline void NTT(int *f, int n, bool Inverse = false)
	{
		for(register int i = 0; i < n; i++) if(i < rev[i]) swap(f[i], f[rev[i]]);
		for(register int len = 2; len <= n; len <<= 1)
		{
			int k = (len >> 1), g = Pow(Inverse ? Inv_Min_G : Min_G, (MOD - 1) / len);
			for(register int i = 0; i < n; i += len)
			{
				int w = 1;
				for(register int j = 0; j < k; j++)
				{
					int u = f[i + j], v = 1LL * w * f[i + j + k] % MOD;
					f[i + j] = Get_MOD(u + v);
					f[i + j + k] = Get_MOD(u - v + MOD); 
					w = 1LL * w * g % MOD;
				}
			}
		}
		if(Inverse) 
		{
			int Inv_N = Pow(n, MOD - 2);
			for(register int i = 0; i < n; i++) f[i] = 1LL * f[i] * Inv_N % MOD;
		}
		return;
	}

	inline void Get_Inverse_Element(int *f, int *g, int n)
	{
		Get_Operation_Sequence(n, seq[0]);
		for(register int k = seq[0][0]; k >= 1; k--)
		{
			int m = seq[0][k];
			if(m == 1)
			{
				g[0] = Pow(f[0], MOD - 2);
				continue;
			}
			Init(m << 1);
			memcpy(t, f, sizeof(int) * m);
			fill(t + m, t + p, 0);
			NTT(t, p), NTT(g, p);
			for(register int i = 0; i < p; i++) g[i] = 1LL * g[i] * Get_MOD(2LL - 1LL * t[i] * g[i] % MOD + MOD) % MOD;
			NTT(g, p, true), fill(g + m, g + p, 0);
			continue;
		}
	}
	
	inline void Multiply(int *f, int n, int *g, int m, int *v)
	{
		Init(n + m - 1);
		memcpy(h, f, sizeof(int) * n), fill(h + n, h + p, 0);
		memcpy(s, g, sizeof(int) * m), fill(s + m, s + p, 0);
		NTT(h, p), NTT(s, p);
		for(register int i = 0; i < p; i++) v[i] = 1LL * h[i] * s[i] % MOD;
		return NTT(v, p, true), fill(v + n + m - 1, v + p, 0), void();
	}

	inline void Divide(int *f, int n, int *g, int m, int *v)
	{
		memcpy(s, g, sizeof(int) * m);
		reverse(s, s + m);
		Get_Inverse_Element(s, r, n - m + 1);
		memcpy(t, f, sizeof(int) * n);
		reverse(t, t + n);
		Multiply(t, n, r, n - m + 1, v);
		return reverse(v, v + n - m + 1);
	}

	inline void Get_Remainder(int *f, int n, int *g, int m, int *v, int *x)
	{
		Multiply(g, m, v, n - m + 1, x);
		for(register int i = 0; i < m; i++) x[i] = Get_MOD(f[i] - x[i] + MOD);
		return;
	}

	inline void Get_Sqrt(int *f, int *g, int n)
	{
		Get_Operation_Sequence(n, seq[1]);
		for(register int k = seq[1][0]; k >= 1; k--)
		{
			int m = seq[1][k];
			if(m == 1)
			{
				g[0] = Quadratic_Residue :: Solve(f[0]);
				continue;
			}
			Init(m << 1);
			memset(h, 0, sizeof(int) * p);
			Get_Inverse_Element(g, h, m);
			memcpy(s, f, sizeof(int) * m);
			fill(s + m, s + p, 0);
			NTT(g, p), NTT(h, p), NTT(s, p);
			int Inv2 = Pow(2, MOD - 2);
			for(register int i = 0; i < p; i++) g[i] = 1LL * Inv2 * Get_MOD(g[i] + 1LL * h[i] * s[i] % MOD) % MOD;
			NTT(g, p, true), fill(g + m, g + p, 0);
			continue;
		}
		return;
	}

	inline void Get_Ln(int *f, int *g, int n)
	{
		Get_Inverse_Element(f, g, n);
		memset(t, 0, sizeof(int) * p);
		Get_Derivative(f, t, n);
		NTT(g, p), NTT(t, p);
		for(register int i = 0; i < p; i++) g[i] = 1LL * g[i] * t[i] % MOD;
		return NTT(g, p, true), Get_Integral(g, g, n), void();
	}

	inline void Get_Exp(int *f, int *g, int n)
	{
		Get_Operation_Sequence(n, seq[2]);
		for(register int k = seq[2][0]; k >= 1; k--)
		{
			int m = seq[2][k];
			if(m == 1)
			{
				g[0] = 1;
				continue;
			}
			Get_Ln(g, h, m);
			Init(m << 1);
			fill(h + m, h + p, 0);
			memcpy(s, f, sizeof(int) * m);
			fill(s + m, s + p, 0);
			NTT(g, p), NTT(h, p), NTT(s, p);
			for(register int i = 0; i < p; i++) g[i] = 1LL * g[i] * Get_MOD(Get_MOD(1LL - h[i] + MOD) + s[i]) % MOD;
			NTT(g, p, true), fill(g + m, g + p, 0), memset(h, 0, sizeof(int) * p), void();
		}
	}

	inline void Get_Quick_Pow(int *f, int *g, int n, int k1, int k2)
	{
		int st = 0;
		while(st < n && f[st] == 0) ++st;
		if(st == n || 1LL * st * k1 > n)
		{
			for(register int i = 0; i < n; i++) putchar('0'), putchar(' ');
			putchar('\n');
			return;
		}
		int inv = Pow(f[st], MOD - 2), mul = Pow(f[st], k2);
		for(register int i = 0; i < n; i++) f[i] = 1LL * f[i + st] * inv % MOD;
		Get_Ln(f, r, n);
		for(register int i = 0; i < p; i++) r[i] = 1LL * r[i] * k1 % MOD;
		Get_Exp(r, g, n);
		int tk = st * k1;
		for(register int i = 0; i < tk; i++) putchar('0'), putchar(' ');
		for(register int i = tk; i < n; i++) printf("%lld ", 1LL * g[i - tk] * mul % MOD);
		putchar('\n');
		return;
	}

	inline void Run()
	{
		/*
		 * You wanna do something here?
		 */
	}

	#undef MAXN
	#undef MOD
	#undef Min_G
	#undef Inv_Min_G
}

int main()
{
	Polynomial :: Run();
	return 0;
}

T o   B e   C o n t i n u e d ⋯ ⋯ \mathit{To~Be~Continued\cdots\cdots} To Be Continued⋯⋯

NTT笔记和多项式全家桶 NTT和多项式全家桶模板 阅读详情

相关推荐

多项式全家桶——炫酷多项式变换

文章目录FFT常数优化1——IDFT常数优化2MTT常数优化多项式求逆常数优化多项式取对数牛顿迭代多项式求指数多项式开方拉格朗日反演 FFT 相信大家FFT已经掌握的很熟练了。 参考这一篇博客:浅谈算法——从多项式乘法到FFT 常数优化1——IDFT 若已知多项式A(x)A(x)A(x)的点值表示 &amp;amp;lt;(ωn0,A(ωn0)),(ωn1,A(ωn1)),⋯&amp;amp;ThinSpace;...

Canopus 1405

【学习笔记】多项式全家桶

终于明白为什么叫多项式全家桶了

cqbzlydd的博客 689

Windows下快速搭建MSYS2开发环境(含GCC与MinGW配置)

本文详细介绍了在Windows系统下快速搭建MSYS2开发环境的完整流程。通过使用pacman包管理器,可以便捷地安装GCC编译器与MinGW-w64工具链,并配置系统环境变量。文章还涵盖了从基础安装、系统更新到编译开源项目的实战指南,帮助开发者高效构建原生Windows程序,显著提升C/C++开发效率。

weixin_29213827的博客 538

多项式知识小全+全家桶学习小结

废话 大概用了两三个星期来搞多项式和生成函数,人都要被虐傻了qwq…… 下面是学到的所有知识的小结,应该是很适合初学者食用的教程,不过一定要按顺序看。 多项式全家桶 快速傅利叶变换FFT 快速数论变换NTT 多项式求逆 学接下来的东西需要的前置知识:微积分的一点初步知识,建议在这里学,看前 444 集大概就够了?泰勒展开与牛顿迭代,以及关于 eee 的一些知识。 多项式ln 多项式exp 多项式开根 多项式除法 多项式多点求值 任意模数NTT 在学的过程中同时也在膜拜师兄的优秀板子,所以每一篇的码风不尽相同

a_forever_dream的博客 1178

攻防世界 misc题GFSJ1129-【您看我还有机会吗?】

我们发现了隐藏内容,一开始我以为那个英文就是flag,提交后发现不对,然后我想只有一张图片然后图片上的隐写内容又不是flag,那必定还有什么文件被隐藏了,于是我们打开VMware并启动Ubuntu,使用binwalk分析一下有没有隐藏文件,我们要在Terminal中分析,如下。我们发现里面有一个flag.txt文件,我们尝试提取里面内容,但发现又要密码,我们想起了之前图片上的隐藏内容,我们将那段英文输入(包括空格和符号,在VMware中输入密码不会有显示,你看到的就像没输入一样),然后我们看到如下。

大数据专业,关注 AI 安全与数学建模。记录 Web 安全、对抗攻击、隐私保护等安全实践,以及从经典模型到智能方法的建模思考。用数学思维理解 AI,用 AI 方法反哺安全与建模。 1093

【FnOS飞牛云 | NAS存储】旧电脑不吃灰,手把手教你搭建完全属于自己的私人网盘

打造属于自己的NAS私人网盘,亲测有效,值得一试

木子Linux的博客世界 9413

【学习笔记】多项式全家桶(包含全套证明)

目录0x00.多项式基本概述0x01.多项式的基础概念0x02. 多项式的度0x03. 多项式的乘法0x04. 多项式的逆元0x10.拉格朗日插值0x20.多项式乘法0x21.快速傅里叶变换(FFT)0x22.快速数论变换(NTT)0x23.快速沃尔什变换(FWT)0x24.快速莫比乌斯变换(FMT)0x30.多项式求逆0x40.多项式开方0x50.多项式除法 / 取模0x60.多项式牛顿迭代0x70.多项式多点求值 / 快速插值0x80.多项式三角函数0x90.多项式反...

繁凡さん的博客 1万+

【vsomeip3 教程】SOME/IP中间件去抖策略全解析

本文探讨了在SOME/IP中间件中实现Debounce(去抖动)机制的必要性和实现原理。文章首先分析了中间件层进行Debounce的四大动机:多订阅者共享收益、语义显式化、协议噪声屏蔽和入口处背压控制。接着详细阐述了Debounce的底层原理,包括变化检测机制(ignore和mask的应用)、时间门控策略(interval的两种时间基准)以及超时补发机制,并通过表格清晰展示了不同参数组合的效果和适用场景。最后,文章提出了工程落地的具体方法,包括策略选型、ignore/mask的设计方法论以及分层验证调试的实

探索C++编程的奥秘,分享深入的技术见解和实践,旨在激发读者创造力与解决问题的思维。 232

多项式全家桶板子

短的 int inv[maxn]; int ksm(int x,int y){int re=1;for(;(y&1?re=1ll*re*x%mod:0),y;y>>=1,x=1ll*x*x%mod);return re;} #define INV(x) ksm(x,mod-2) struct NTT{ vector<int> w[30];NTT(){ inv[1]=1;for(int i=2;i<=maxn-10;i++)inv[i]=1ll*(mod-mod/i

a_forever_dream的博客 556

Tesla v100 4张显卡组装机安装vllm和ollama在Ubuntu系统上的安装---AI大模型系统从零开始0032

本文提供Ubuntu22.04系统下vLLM的完整安装指南,适配4×32G Tesla显卡的单机4卡张量并行部署。教程包含:1)硬件要求(Ubuntu22.04、NVIDIA驱动≥535、CUDA12.1+);2)NVIDIA驱动自动安装方法;3)Python3.10虚拟环境配置;4)vLLM官方预编译包一键安装;5)单机4卡启动Qwen3.6-27B模型的完整参数说明(含FP8量化、多进程并行等关键配置)。文末附分布式部署的Ray集群方案参考链接,适用于多机8卡场景。

添柴程序猿的专栏 176

STM32 IIC温湿度传感器(STH45)实验

精度 ΔRH = ±1.0 %RH, ΔT = ±0.1 °C• 平均电流:0.4 μA,空闲电流:80 nA• I2C FM、CRC 校验和、乘法。I2C 地址。• 专利保护选项 [1]、PTFE 膜和可拆卸保护盖• 工作范围:0 …100 %相对湿度,−40…125 摄氏度• 在冷凝环境中功能齐全• 动力加热器,真正的NIST可追溯性• JEDEC JESD47 认证 • 符合 ISO 17025 : 2017 的传感器专用校准证书,3 点温度校准。

baidu_34971492的博客 3072
上一篇: 线性空间和OI中的线性基
下一篇: 斯特林的试炼
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值