【学习笔记】多项式运算

文章目录

  • 前置知识
  • 多元多项式乘法
  • 牛顿迭代
  • 多点求值
  • 快速插值
  • 解线性方程组
  • 常数优化
  • 半在线卷积
  • 代码实现

前置知识

有用的:多项式乘法,别人的博客和别人的博客。

没用的:自适应辛普森,拉格朗日反演, Half-GCD \textit{Half-GCD} Half-GCD 和下降幂多项式。

多元多项式乘法

可见于 EntropyIncreaser \textsf{EntropyIncreaser} EntropyIncreaser 的博客。设第 i i i 维只用保留 ( n i − 1 ) (n_i{-}1) (ni1) 次项。

取占位数 χ ( i ) = ⌊ i n 1 ⌋ + ⌊ i n 1 n 2 ⌋ + ⋯ + ⌊ i ∏ n j ⌋ \chi(i)=\lfloor\frac{i}{n_1}\rfloor+\lfloor\frac{i}{n_1n_2}\rfloor+\cdots+\lfloor\frac{i}{\prod n_j}\rfloor χ(i)=n1i+n1n2i++nji,有 χ ( i + j ) − χ ( i ) − χ ( j ) ∈ [ 0 , k ) \chi(i{+}j)-\chi(i)-\chi(j)\in[0,k) χ(i+j)χ(i)χ(j)[0,k),因此乘 σ χ ( i ) \sigma^{\chi(i)} σχ(i) 后对 ( σ k − 1 ) (\sigma^k{-}1) (σk1) 取模可行。复杂度 O ( k N log ⁡ N ) \mathcal O(kN\log N) O(kNlogN),其中 k k k 是变元的数量,而 N = ∏ n i N=\prod n_i N=ni

不难发现,这是集合幂级数的推广。 Δ χ \Delta\chi Δχ 其实是进位次数,类似 Kummer \textrm{Kummer} Kummer 定理。

牛顿迭代

严谨地写,实际上我们是求解方程
G ( χ , x ) = 0 G(\chi,x)=0 G(χ,x)=0

的通解 χ = f ( x ) \chi=f(x) χ=f(x) 。在 χ 0 = f 0 ( x ) \chi_0=f_0(x) χ0=f0(x) 处,在 χ \chi χ 这一维上泰勒展开,得
G ( χ , x ) = G ( χ 0 , x ) + ( χ − χ 0 ) ⋅ G ′ ( χ 0 , x ) + R ( χ ) G(\chi,x)=G(\chi_0,x)+(\chi-\chi_0)\cdot G'(\chi_0,x)+R(\chi) G(χ,x)=G(χ0,x)+(χχ0)G(χ0,x)+R(χ)

这里的导数 G ′ G' G 都是在 χ \chi χ 上的偏导。其中有拉格朗日余项 R ( χ ) = ( χ − χ 0 ) 2 2 ! G ′ ′ [ x 0 + γ ( x − x 0 ) ]    ( 0 < γ < 1 ) R(\chi)={(\chi-\chi_0)^2\over 2!}G''[x_0+\gamma(x{-}x_0)]\;(0<\gamma<1) R(χ)=2!(χχ0)2G′′[x0+γ(xx0)](0<γ<1) 。此时,设 f 0 ( x ) ≡ f ( x ) ( m o d x n ) f_0(x)\equiv f(x)\pmod{x^n} f0(x)f(x)(modxn),代入 χ = f ( x ) ,    χ 0 = f 0 ( x ) \chi=f(x),\;\chi_0=f_0(x) χ=f(x),χ0=f0(x),再对 x 2 n x^{2n} x2n 取模得到
G ( f ( x ) , x ) ≡ G ( f 0 ( x ) , x ) + [ f ( x ) − f 0 ( x ) ] ⋅ G ′ ( f 0 ( x ) , x ) ( m o d x 2 n ) G(f(x),x)\equiv G(f_0(x),x)+[f(x)-f_0(x)]\cdot G'(f_0(x),x)\pmod{x^{2n}} G(f(x),x)G(f0(x),x)+[f(x)f0(x)]G(f0(x),x)(modx2n)

因为 x 2 n ∣ [ f ( x ) − f 0 ( x ) ] 2 ∣ R ( χ ) x^{2n}\mid[f(x)-f_0(x)]^2\mid R(\chi) x2n[f(x)f0(x)]2R(χ) 。此时解方程 G ( f ( x ) , x ) ≡ 0 ( m o d x 2 n ) G(f(x),x)\equiv 0\pmod{x^{2n}} G(f(x),x)0(modx2n)
f ( x ) ≡ f 0 ( x ) − G ( f 0 ( x ) , x ) G ′ ( f ( x ) , x ) ( m o d x 2 n ) f(x)\equiv f_0(x)-\frac{G(f_0(x),x)}{G'(f(x),x)}\pmod{x^{2n}} f(x)f0(x)G(f(x),x)G(f0(x),x)(modx2n)

只需解出常数 c c c 使得 G ( c , x ) ≡ 0 ( m o d x ) G(c,x)\equiv 0\pmod{x} G(c,x)0(modx),然后不断迭代,就能得到对 x 2 k x^{2^k} x2k 取模的根。

为了避免误解,举个例子,求 A ( x ) \sqrt{A(x)} A(x) 。记 G ( f ( x ) , x ) = f ( x ) 2 − A ( x ) G(f(x),x)=f(x)^2-A(x) G(f(x),x)=f(x)2A(x),那么 G ′ ( f ( x ) , x ) = 2 f ( x ) G'(f(x),x)=2f(x) G(f(x),x)=2f(x),因为其原型是 G ( χ , x ) = χ 2 − A ( x ) ⇒ ∂ G ( χ , x ) ∂ χ = 2 χ G(\chi,x)=\chi^2-A(x)\Rightarrow\frac{\partial G(\chi,x)}{\partial\chi}=2\chi G(χ,x)=χ2A(x)χG(χ,x)=2χ

多点求值

n n n 次多项式 f ( x ) f(x) f(x) k k k 个点值 f ( x i )    ( i ∈ [ 1 , k ] ) f(x_i)\;(i\in[1,k]) f(xi)(i[1,k]) 。当 k > n k>n k>n 时,将其拆解成 ⌈ k n ⌉ \lceil{ k\over n }\rceil nk 次多点求值,故假定 n ⩾ k n\geqslant k nk

f ( x )   m o d   ( x − x 1 ) f(x)\bmod(x-x_1) f(x)mod(xx1) 就是 f ( x 1 ) f(x_1) f(x1) 。那么 f ( x ) f(x) f(x) 取模 ∏ j ∈ S ( x − x j ) \prod_{j\in S}(x-x_j) jS(xxj) 就不会影响 f ( x j )    ( j ∈ S ) f(x_j)\;(j\in S) f(xj)(jS) 的取值。

于是分治。对于区间 l , r l,r l,r 可知 f ( x )   m o d   [ ∏ j = l r ( x − x j ) ] f(x)\bmod\big[\prod_{j=l}^{r}(x-x_j)\big] f(x)mod[j=lr(xxj)] 的结果,往左递归则继续取模,往右递归也可取模。复杂度 T ( k ) = 2 T ( k 2 ) + O ( k log ⁡ k ) = O ( k log ⁡ 2 k ) T(k)=2T({k\over 2})+\mathcal O(k\log k)=\mathcal O(k\log^2k) T(k)=2T(2k)+O(klogk)=O(klog2k) 。再考虑到第一步 f ( x )   m o d   [ ∏ j = 1 k ( x − x j ) ] f(x)\bmod\big[\prod_{j=1}^{k}(x-x_j)\big] f(x)mod[j=1k(xxj)] 需要 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) ,总复杂度为
O ( n log ⁡ n + k log ⁡ 2 k ) \mathcal O(n\log n+k\log^2 k) O(nlogn+klog2k)

2022/1/25   update \texttt{2022/1/25 update} 2022/1/25 update:学了个使用 转置原理 的新方法。

f ( x ) = ∑ i = 0 n − 1 a i x i f(x)=\sum_{i=0}^{n-1}a_ix^i f(x)=i=0n1aixi 系数构成列向量 v \bf v v 满足 v i = a i {\bf v}_i=a_i vi=ai 。多点求值等价于乘矩阵 A \mathscr A A 满足 A r c = x r   c {\mathscr A}_{rc}=x_r^{\thinspace c} Arc=xrc 。考虑其转置 A T r c = x c   r {\mathscr A^{\sf T}}_{rc}=x_c^{\thinspace r} ATrc=xcr 。此时验算 A T v {\mathscr A^{\sf T}}\bf v ATv 的第 r r r 项为 ∑ i = 0 n − 1 x i   r a i \sum_{i=0}^{n-1}x_{i}^{\thinspace r}a_i i=0n1xirai

对该数列建立生成函数,则 F ( λ ) = ∑ i = 1 n a i 1 − x i λ F(\lambda)=\sum_{i=1}^{n}\frac{a_i}{1-x_i\lambda} F(λ)=i=1n1xiλai,考虑每个 a i a_i ai F ( λ ) F(\lambda) F(λ) 各项系数的贡献易得。说老实话,看到这个式子之前我还真不会算

考虑该转置问题的求解方法:分治,维护分子和分母多项式。分母是固定的 ∏ ( 1 − x i λ ) \prod(1-x_i\lambda) (1xiλ),可以预先求出来。问题变为,在分治结构上(可将其理解为线段树)求解分子多项式。但是,自变量并不是多项式——自变量应当是 多项式的系数,因为原本的 A \mathscr A A 就是作用于系数的。

f x f_x fx 表示分子多项式的 系数序列(列向量),用 g x g_x gx 表示分母多项式的 卷积矩阵(卷积显然是线性变换,由于分母多项式是已知的,我们可以将其转化为卷积矩阵),记 l , r l,r l,r 为分治结构的左右儿子,则 A T v {\mathscr A^{\sf T}}\bf v ATv 的解法是 f x = g r f l + g l f r f_x=g_rf_l+g_lf_r fx=grfl+glfr

现在,对 A T {\mathscr A^{\sf T}} AT 作转置。这一步线性变换则变为
f l = g r   T f x f r = g l   T f x f_l=g_r^{\thinspace\sf T}f_x\quad f_r=g_l^{\thinspace\sf T}f_x fl=grTfxfr=glTfx

这个结果对于熟练者,可以一眼看出。因为矩阵相当于有向图邻接矩阵,转置相当于将有向图边反向;原来有 g r g_r gr 条路从 f l f_l fl f x f_x fx,现在就该反着到 f l f_l fl 了。

接下来,填个前文的坑——卷积对应的矩阵。我就不列举了,希望大家能用上面的方法,一眼看穿:卷积等价于,从 i i i j j j 的路径条数是 a j − i a_{j-i} aji 。所以卷积矩阵的转置,就是 j → i j\to i ji 路径条数是 a j − i a_{j-i} aji,相当于所谓 “减卷积”,下标做减法的卷积。

“减卷积” 可以翻转多项式后做普通卷积,或者沿用转置原理。只需先做 IDFT \textit{IDFT} IDFT,作乘法,再做 DFT \textit{DFT} DFT 。然而,常数优化 去掉蝴蝶变换 会导致它出问题,所以我不会这么做。

上述的整个过程,就是对 A T \mathscr A^{\sf T} AT 的转置,也就得到了 A \mathscr A A 。原本 A T v \mathscr A^{\sf T}\bf v ATv input \text{input} input 是序列 { a 0 , a 1 , … , a n − 1 } \{a_0,a_1,\dots,a_{n-1}\} {a0,a1,,an1},其 output \text{output} output f r o o t f_{\rm root} froot 的各项系数。那么转置后 input \text{input} input 变为 output \text{output} output,即 { a 0 , a 1 , … , a n − 1 } \{a_0,a_1,\dots,a_{n-1}\} {a0,a1,,an1} 直接视为 f r o o t f_{\rm root} froot 的各项系数即可。

注意 A T v \mathscr A^{\sf T}\bf v ATv 问题中,求出 f r o o t f_{\rm root} froot 后还除以了最终的分母多项式 β ( λ ) = ∏ ( 1 − x i λ ) \beta(\lambda)=\prod(1-x_i\lambda) β(λ)=(1xiλ),转置后就要先跟 β ( λ ) − 1 \beta(\lambda)^{-1} β(λ)1 作 “减卷积”。然后用上面的方法推到叶子节点上。最后叶子节点的值(度数为零的多项式的系数)就是答案。

快速插值

是对拉格朗日插值法的改进。考虑原本的等式
f ( x ) = ∑ i = 1 n y i ⋅ ∏ j ≠ i ( x − x j ) ∏ j ≠ i ( x i − x j ) f(x)=\sum_{i=1}^{n}y_i\cdot{\prod_{j\ne i}(x-x_j)\over \prod_{j\ne i}(x_i-x_j)} f(x)=i=1nyij=i(xixj)j=i(xxj)

首先把分母搞定。
∏ j ≠ i ( x i − x j ) = [ ∏ j = 1 n ( x − x j ) ] ′ ∣ x = x i \prod_{j\ne i}(x_i-x_j)=\left[\prod_{j=1}^{n}(x-x_j)\right]'\Bigg|_{x=x_i} j=i(xixj)=[j=1n(xxj)] x=xi

求出那个多项式,求导,然后 多点求值 得到 n n n 个位置的取值。现在我们只需要求
f ( x ) = ∑ i = 1 n y i g ′ ( x i ) ∏ j ≠ i ( x − x j ) f(x)=\sum_{i=1}^{n}{y_i\over g'(x_i)}\prod_{j\ne i}(x-x_j) f(x)=i=1ng(xi)yij=i(xxj)

右边那个 ∏ \prod 可以用一个前缀与一个后缀拼接而成。于是我们将插值法优化到了
O ( n log ⁡ 2 n ) − O ( n ) \mathcal O(n\log^2n)-\mathcal O(n) O(nlog2n)O(n)

这个记号的含义是, O ( n log ⁡ 2 n ) \mathcal O(n\log^2 n) O(nlog2n) 预处理,然后 O ( n ) \mathcal O(n) O(n) 查询。

如果我们要求出 f ( x ) f(x) f(x) 的多项式形式,就用 f ( x ) = ∑ i = 1 n y i g ′ ( x i ) ∏ j = 1 n ( x − x j ) x − x i f(x)=\sum_{i=1}^{n}\frac{y_i}{g'(x_i)}\frac{\prod_{j=1}^{n}(x-x_j)}{x-x_i} f(x)=i=1ng(xi)yixxij=1n(xxj),合并后面的分式可以分治后维护分子分母,复杂度 O ( n log ⁡ 2 n ) \mathcal O(n\log^2 n) O(nlog2n)

特别地,如果已有点值是 [ 0 , n ) [0,n) [0,n) 连续点值,显然不需要 多点求值 了,那么至少可以直接做到 O ( n ) \mathcal O(n) O(n) 算单点值。但是,如果目标是得到别处的连续点值,则存在基于卷积的 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) 做法:
f x = ∑ i = 0 n − 1 y i x − i ⋅ x ! ( x − n ) ! ⋅ ( − 1 ) n − i − 1 i ! ⋅ ( n − i − 1 ) ! = x ! ( x − n ) ! ∑ i = 0 n − 1 λ i x − i f_x=\sum_{i=0}^{n-1}\frac{y_i}{x-i}\cdot\frac{x!}{(x{-}n)!}\cdot\frac{(-1)^{n-i-1}}{i!\cdot (n{-}i{-}1)!} =\frac{x!}{(x{-}n)!}\sum_{i=0}^{n-1}\frac{\lambda_i}{x-i} fx=i=0n1xiyi(xn)!x!i!(ni1)!(1)ni1=(xn)!x!i=0n1xiλi

其中与 x x x 无关的部分写为 λ i \lambda_i λi,容易预处理得。

解线性方程组

F i ( x ) = G i ( x ) + ∑ j = 1 n P i , j ( x ) F j ( x ) ( m o d x m ) F_i(x)=G_i(x)+\sum_{j=1}^{n} P_{i,j}(x)F_j(x)\pmod{x^m} Fi(x)=Gi(x)+j=1nPi,j(x)Fj(x)(modxm) 一类的方程。当你意识到,形式幂级数的指数只是形式时,你就想到矩阵的所谓下标也只是形式。二者都可作为主元。所以,必定可以写成 元素为矩阵 的多项式 F ( x ) = G ( x ) + P ( x ) F ( x ) F(x)=G(x)+P(x)F(x) F(x)=G(x)+P(x)F(x)

I x 0 − P ( x ) Ix^0-P(x) Ix0P(x) 存在逆元,用多项式求逆,复杂度 O ( n 3 m + n 2 m log ⁡ m ) \mathcal O(n^3m+n^2m\log m) O(n3m+n2mlogm)

[ x 0 ] P ( x ) = 0 [x^0]P(x)=0 [x0]P(x)=0 [ x κ ] G i ( x ) = 0    ( κ ≠ 0 ) [x^{\kappa}]G_i(x)=0\;(\kappa\ne 0) [xκ]Gi(x)=0(κ=0) 时,即给出的是 F ( x ) F(x) F(x) 的线性递推式时,另有一个有趣的做法:递归到某区间,需要计算内部贡献时,内部贡献是线性变换;只需要预处理长度为 n n n 的左右区间贡献系数。这会是一个多项式,与分治 NTT \textit{NTT} NTT 的递推方式相同;在 [ x 0 ] F ( x ) = I [x^0]F(x)=I [x0]F(x)=I 时,甚至就是 F ( x ) F(x) F(x) [ x 0 ] [x^0] [x0] [ x n − 1 ] [x^{n-1}] [xn1] 系数构成的 “子多项式”。

于是时间复杂度 O ( n 3 m + n 2 m log ⁡ m ) \mathcal O(n^3m+n^2m\log m) O(n3m+n2mlogm) 不变。

常数优化

可参考巨佬的博客。这里只摘录(有删改)最常见的求逆操作。

设求解 f f f 的逆元, g 0 = f − 1   m o d   x n g_0=f^{-1}\bmod x^n g0=f1modxn 。倍增到 g = f − 1   m o d   x 2 n g=f^{-1}\bmod x^{2n} g=f1modx2n 满足
g = ( 2 g 0 − g 0 ⋅ f ⋅ g 0 )   m o d   x 2 n g=(2g_0-g_0\cdot f\cdot g_0)\bmod x^{2n} g=(2g0g0fg0)modx2n

虽然系数属于非交换环的情况较少,但确实有(如矩阵)。但是左逆元总是等于右逆元,这是群的性质。

一般做法是,将 g 0 g_0 g0 f f f 全部做长度为 4 n 4n 4n FFT \textit{FFT} FFT,乘起来之后逆变换。假定复杂度瓶颈在于 FFT \textit{FFT} FFT,记为 3 E ( 4 n ) 3\mathsf E(4n) 3E(4n) 。下面将给出两种更优的做法。

f 0 = f   m o d   x 2 n f_0=f\bmod x^{2n} f0=fmodx2n,考虑 g = [ g 0 − ( g 0 f 0 − 1 ) ⋅ g 0 ]   m o d   x 2 n g=[g_0-(g_0f_0-1)\cdot g_0]\bmod x^{2n} g=[g0(g0f01)g0]modx2n,注意到 ( g 0 f 0 − 1 ) (g_0f_0-1) (g0f01) 只在 [ n , 3 n ) [n,3n) [n,3n) 次项处有值,因此可以做长度为 2 n 2n 2n 的循环卷积。再计算其与 g 0 g_0 g0 的乘积,亦可以用长度为 2 n 2n 2n 的循环卷积。

一次长度为 2 n 2n 2n 的循环卷积需要 3 E ( 2 n ) 3\mathsf E(2n) 3E(2n),但 g 0 g_0 g0 的正变换被计算了两次,所以实际只有 5 E ( 2 n ) 5\mathsf E(2n) 5E(2n)

再改进:注意到 ( g 0 f 0 − 1 ) ⋅ g 0 (g_0f_0-1)\cdot g_0 (g0f01)g0 只在 [ n , 4 n ) [n,4n) [n,4n) 次项处有值,因此考虑做 长度为 3 n 3n 3n 的循环卷积。这听上去需要 Bluestein \text{Bluestein} Bluestein 之类的东西?其实只要点值的数量够多即可。

考虑取 a a a 满足 a 2 n ≠ 1 a^{2n}\ne 1 a2n=1,然后求出取模 ( x 2 n − 1 ) ( x n − a n ) (x^{2n}-1)(x^n-a^n) (x2n1)(xnan) 的结果,即在 { ω 2 n   i    ∣    i ∈ [ 0 , 2 n ) } \{\omega_{2n}^{\thinspace i}\;|\;i\in[0,2n)\} {ω2nii[0,2n)} { a ω n   i    ∣    i ∈ [ 0 , n ) } \{a\omega_n^{\thinspace i}\;|\;i\in[0,n)\} {aωnii[0,n)} 处作点值转换。求值相当于求 f ( x ) f(x) f(x) 长为 2 n 2n 2n DFT \textit{DFT} DFT 数组和 f ( a x ) f(ax) f(ax) 长为 n n n DFT \textit{DFT} DFT 数组,插值则分别 IDFT \textit{IDFT} IDFT CRT \textit{CRT} CRT 合并即可。一般取 a = ω 4 n a=\omega_{4n} a=ω4n

对这个 CRT \textit{CRT} CRT 的直观理解:设
f ( x ) = a x n + b x 2 n + c x 3 n f(x)=ax^n+bx^{2n}+cx^{3n} f(x)=axn+bx2n+cx3n

我们已知的是
{ f ( x )   m o d   ( x 2 n − 1 ) = b + ( a + c ) x n f ( ω 4 n x )   m o d   ( x n − 1 ) = ω 4 a + ω 4 2 b + ω 4 3 c \begin{cases} f(x)\bmod(x^{2n}-1)=b+(a+c)x^{n}\\ f(\omega_{4n}x)\bmod(x^n-1)=\omega_4 a+\omega^2_4b+\omega_4^{3}c \end{cases} {f(x)mod(x2n1)=b+(a+c)xnf(ω4nx)mod(xn1)=ω4a+ω42b+ω43c

还原出 a a a 是容易的,毕竟 ω 4 3 = − ω 4 \omega_{4}^{3}=-\omega_4 ω43=ω4 。这样的复杂度是 3 E ( 2 n ) + 3 E ( n ) 3\mathsf E(2n)+3\mathsf E(n) 3E(2n)+3E(n)

半在线卷积

所谓半在线:要做卷积的东西在动态增长。

  • 最简单情形:求 G ( x ) G(x) G(x) 使得 [ x n ] G ( x ) = ω n [ x n ] F ( x ) G ( x ) [x^n]G(x)=\omega_n[x^n]F(x)G(x) [xn]G(x)=ωn[xn]F(x)G(x)

cdq \texttt{cdq} cdq 分治即可。不妨设 n = 2 k n=2^k n=2k,则只需先算出前 2 k − 1 2^{k-1} 2k1 项,然后与 F ( x ) F(x) F(x) 的前 2 k 2^k 2k 项作卷积,贡献到右侧。

注意到这样的 F ( x ) G ( x ) F(x)G(x) F(x)G(x) 只在 [ 0 ,    3 × 2 k − 1 ) [0,\;3\times 2^{k-1}) [0,3×2k1) 项有值,故可以做 2 k 2^{k} 2k 长度的循环卷积,这样只会污染前半部分的值。

又注意到我们总是对 F ( x ) F(x) F(x) 长为 2 k 2^k 2k 的前缀做 2 k 2^k 2k 长度的循环卷积,故可以预处理这些结果。这样应该是相当快的,至少 cmdblock \textsf{cmdblock} cmdblock 说这样实现 exp ⁡ \exp exp 比牛顿迭代快多了。

具体实现的时候,甚至可以避免递归(但我不清楚是否有常数上的优化)。

  • 更在线的情形:令 f n = ⨁ i = 0 n − 1 g i f_n=\bigoplus_{i=0}^{n-1}g_i fn=i=0n1gi,求 [ x n ] G ( x ) = ω n [ x n ] F ( x ) G ( x ) [x^n]G(x)=\omega_n[x^n]F(x)G(x) [xn]G(x)=ωn[xn]F(x)G(x) 的解。

因为 F ( x ) F(x) F(x) 未给定,当分治区间左端点 l = 0 l=0 l=0 时, F ( x ) F(x) F(x) 的前 2 k 2^k 2k 项还没算出来。只能做 F ( x ) F(x) F(x) 2 k − 1 2^{k-1} 2k1 项的卷积。

其后,可以认为 F ( x ) , G ( x ) F(x),G(x) F(x),G(x) 的项数同步增长,每次都将新增项的贡献加入即可(如同 最简单情形 G ( x ) G(x) G(x) 的新求出项去乘 F ( x ) F(x) F(x) 的前缀, F ( x ) F(x) F(x) 的新求出项也要去乘 G ( x ) G(x) G(x) 的前缀)。

  • 最真实的半在线卷积:给定 F ( x ) F(x) F(x),求 [ x n ] G ( x ) = ω n [ x n ] F ( x ) G ( x ) 2 [x^n]G(x)=\omega_n[x^n]F(x)G(x)^2 [xn]G(x)=ωn[xn]F(x)G(x)2

G ( x ) G(x) G(x) 多求出一项后, G ( x ) 2 G(x)^2 G(x)2 并不能 O ( 1 ) \mathcal O(1) O(1) 增长。与上面 更在线的情况 形成对比(前缀异或和 O ( 1 ) \mathcal O(1) O(1) 即得)。

因此我们要同步计算 G ( x ) 2 G(x)^2 G(x)2,也是半在线卷积。

  • 更快速的半在线卷积。

c d q \tt cdq cdq 分治时,分成 log ⁡ n \log n logn 个块,则递归层数是 log ⁡ log ⁡ n n = log ⁡ n log ⁡ log ⁡ n \log_{\log n}n=\frac{\log n}{\log\log n} loglognn=loglognlogn 。每层的 FFT \textit{FFT} FFT 总复杂度还是 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn),而卷积需要块之间两两暴力乘法,复杂度 O ( n log ⁡ n ⋅ log ⁡ 2 n ) = O ( n log ⁡ n ) \mathcal O({n\over\log n}\cdot\log^2n)=\mathcal O(n\log n) O(lognnlog2n)=O(nlogn),因此总复杂度 O ( n log ⁡ n log ⁡ log ⁡ n ) \mathcal O({n\log n\over\log\log n}) O(loglognnlogn)

代码实现

不是很全面,但应该比较好用了。

const int MOD = 998244353, LOGMOD = 30;
inline void modAddUp(int &x, const int &y){
	if((x += y) >= MOD) x -= MOD; // faster?
}
inline llong qkpow(llong b, int q){
	llong a = 1;
	for(; q; q>>=1,b=b*b%MOD) if(q&1) a = a*b%MOD;
	return a;
}
const int MAXN = 800005;
int g[LOGMOD], inv[MAXN];
inline void prepareNtt(const int &n){
	rep0(i,(inv[1]=1)<<1,1<<n) inv[i] = int(
		llong(MOD-MOD/i)*inv[MOD%i]%MOD);
	int p = MOD-1, x = 0;
	while(!(p&1)) p >>= 1, ++ x;
	for(g[x]=int(qkpow(3,p)); x; --x)
		g[x-1] = int(llong(g[x])*g[x]%MOD);
}
void ntt(int a[], int n){
	for(int w=1<<n>>1,x=n; x; w>>=1,--x)
	for(int *p=a; p!=a+(1<<n); p+=(w<<1))
	for(int *i=p,*j=p+w,v=1; i!=p+w; ++i,++j,v=int(llong(v)*g[x]%MOD)){
		const llong t = llong((*i)+MOD-(*j))*v%MOD;
		modAddUp(*i,*j), *j = int(t);
	}
}
void dntt(int a[], int n){
	for(int w=1,x=1; x<=n; w<<=1,++x)
	for(int *p=a; p!=a+(1<<n); p+=(w<<1))
	for(int *i=p,*j=p+w,v=1; i!=p+w; ++i,++j,v=int(llong(v)*g[x]%MOD)){
		const int t = int(llong(*j)*v%MOD);
		modAddUp(*j=*i,MOD-t), modAddUp(*i,t);
	}
	std::reverse(a+1,a+(1<<n));
	const int inv2n = MOD-((MOD-1)>>n);
	for(int *i=a; i!=a+(1<<n); ++i)
		*i = int(llong(*i)*inv2n%MOD);
}
inline int getNttLen(int n){
	return 32-__builtin_clz(n);
}
inline void array_mul(int a[], const int b[], int n){
	for(; n; --n,++a,++b) *a = int(llong(*a)*(*b)%MOD);
}
int tmp[MAXN]; // be careful
void getInv(const int a[], int n, int f[]){
	f[0] = int(qkpow(a[0],MOD-2)), f[1] = 0;
	for(int len=1; len<=n; ++len){
		memcpy(tmp,a,(1<<len)<<2);
		memset(tmp+(1<<len),0,(1<<len)<<2);
		memset(f+(1<<len),0,(1<<len)<<2);
		ntt(tmp,len+1), ntt(f,len+1);
		rep0(i,0,2<<len) f[i] = int((2-llong(
			f[i])*tmp[i]%MOD+MOD)*f[i]%MOD);
		dntt(f,len+1); // get inversion
		memset(f+(1<<len),0,(1<<len)<<2);
	}
}
void getLn(const int a[], int n, int ln[]){
	getInv(a,n,ln); ntt(ln,n+1);
	rep0(i,1,1<<n) tmp[i-1] = int(llong(i)*a[i]%MOD);
	memset(tmp+(1<<n),0,(1<<n)<<2), tmp[(1<<n)-1] = 0;
	ntt(tmp,n+1); array_mul(tmp,ln,2<<n);
	dntt(tmp,n+1); ln[0] = 0; rep0(i,1,1<<n)
		ln[i] = int(llong(inv[i])*tmp[i-1]%MOD);
}
int tmp2[MAXN]; // need more buffer
void getExp(const int a[], int n, int exp[]){
	exp[0] = 1, exp[1] = 0; rep(len,1,n){
		getLn(exp,len,tmp2); tmp2[0] = tmp2[0] ? tmp2[0]-1 : MOD-1;
		rep0(i,0,1<<len) tmp2[i] = (MOD+a[i]-tmp2[i])%MOD;
		memset(tmp2+(1<<len),0,(1<<len)<<2);
		memset(exp+(1<<len),0,(1<<len)<<2);
		ntt(tmp2,len+1), ntt(exp,len+1), array_mul(exp,tmp2,2<<len);
		dntt(exp,len+1), memset(exp+(1<<len),0,(1<<len)<<2);
	}
}

还有基于 v e c t o r \tt vector vector 的多点求值以及多项式求逆(含常数优化)实现。但不得不说,确实慢。

#include 
#include  // Almighty XJX yyds!!
#include  // Who can tell me why I'm so weak!
#include  // rainybunny root of the evil.
#include 
#include 
using llong = long long;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
# define rep0(i,a,b) for(int i=(a); i!=(b); ++i)
inline int readint(){
    int a = 0, c = getchar(), f = 1;
    for(; !isdigit(c); c=getchar()) if(c == '-') f = -f;
    for(; isdigit(c); c=getchar()) a = a*10+(c^48);
    return a*f;
}

const int MOD = 998244353, LOGMOD = 24;
inline llong qkpow(llong b, int q){
    llong a = 1;
    for(; q; q>>=1,b=b*b%MOD) if(q&1) a = a*b%MOD;
    return a;
}
inline void modAddUp(int& x, const int& y){
    if((x += y) >= MOD) x -= MOD;
}

const int MAXN = 1<<17;
struct Poly : std::vector<int>{
    static int g[LOGMOD];
    static void prepare(){
        int p = MOD-1, x = 0;
        while(!(p&1)) p >>= 1, ++ x;
        for(g[x]=int(qkpow(3,p)); x; --x)
            g[x-1] = int(llong(g[x])*g[x]%MOD);
    }
    static void ntt(Poly& a, const int& n){
        for(int w=1<<n>>1,x=n; x; w>>=1,--x){
            static int v[MAXN]; v[0] = 1; // pre-compute
            rep0(i,1,w) v[i] = int(llong(g[x])*v[i-1]%MOD);
            for(auto p=a.begin(); p!=a.end(); p+=(w<<1)){
                const int* nowv = v;
                for(auto i=p,j=p+w; i!=p+w; ++i,++j,++nowv){
                    llong t = llong((*i)-(*j)+MOD)*(*nowv)%MOD;
                    modAddUp(*i,*j); *j = int(t);
                }
            }
        }
    }
    static void dntt(Poly& a, const int& n){
        for(int w=1,x=1; x<=n; w<<=1,++x){
            static int v[MAXN]; v[0] = 1; // pre-compute
            rep0(i,1,w) v[i] = int(llong(g[x])*v[i-1]%MOD);
            for(auto p=a.begin(); p!=a.end(); p+=(w<<1)){
                const int* nowv = v;
                for(auto i=p,j=p+w; i!=p+w; ++i,++j,++nowv){
                    int t = int(llong(*j)*(*nowv)%MOD);
                    modAddUp(*j=*i,MOD-t); modAddUp(*i,t);
                }
            }
        }
        std::reverse(a.begin()+1,a.end());
        const llong inv2n = MOD-((MOD-1)>>n);
        rep0(i,0,1<<n) a[i] = int(a[i]*inv2n%MOD);
    }
    inline void trim(){ while(back() == 0) pop_back(); }
    template < class Itr > // mostly Poly::iterator
    inline static void array_mul(Itr a, Itr b, int n){
        for(; n; ++a,++b,--n) *a = int(llong(*a)*(*b)%MOD);
    }
    template < class IntegerType >
    inline static int nttLen(const IntegerType& n){
        if(n == 0) return 0; // 2^0 > 0
        return 32-__builtin_clz(unsigned(n));
    }
    Poly operator * (const Poly& b) const{
        const int n = nttLen(size()+b.size()-2);
        Poly c = *this; c.resize(1<<n,0); ntt(c,n);
        Poly d = b; d.resize(1<<n,0); ntt(d,n);
        array_mul(c.begin(),d.begin(),1<<n); dntt(c,n);
        c.resize(size()+b.size()-1); return c;
    }
    /** @note @p a shall be of length 1 << @p n */
    static Poly inv(const Poly& a, const int& len){
        Poly b; b.resize(1<<len,0); b[0] = 1;
        rep0(n,0,len){ // log2 of current length
            Poly tmp; tmp.resize(2<<n);
            rep0(i,0,2<<n) tmp[i] = a[i];
            Poly tmpb; tmpb.resize(2<<n,0);
            rep0(i,0,1<<n) tmpb[i] = b[i];
            ntt(tmp,n+1), ntt(tmpb,n+1);
            array_mul(tmp.begin(),tmpb.begin(),2<<n);
            dntt(tmp,n+1); rep0(i,0,1<<n) tmp[i] = 0;
            ntt(tmp,n+1); // (g0*f0-1) to multiply g0
            array_mul(tmp.begin(),tmpb.begin(),2<<n);
            dntt(tmp,n+1); rep0(i,1<<n,2<<n)
                if(tmp[i]) b[i] = MOD-tmp[i];
        }
        return b; // exactly that length
    }
};
int Poly::g[LOGMOD]; // actually global
Poly minus_convolution(const Poly& a, const Poly& b){
    Poly c = b; std::reverse(c.begin(),c.end());
    c = c*a; const int shift = int(b.size())-1;
    rep0(i,shift,int(c.size())) c[i-shift] = c[i];
    c.resize(c.size()-shift); return c;
}

Poly mom[MAXN<<1], ans[MAXN<<1];
int a[MAXN], b[MAXN];
int main(){
    Poly::prepare();
    int n = readint()+1, m = readint();
    rep0(i,0,n) a[i] = readint();
    rep0(i,0,m) b[i] = readint();
    if(n < m) n = m; // be bigger
    const int ass = Poly::nttLen(n-1);
    n = 1<<ass; // won't change the answer
    rep0(i,0,n){
        if(!b[i]){
            mom[i^n].resize(1,1);
            continue; // short
        }
        mom[i^n].resize(2); mom[i^n][0] = 1;
        mom[i^n][1] = MOD-b[i];
    }
    drep(i,n-1,1) mom[i] = mom[i<<1]*mom[i<<1|1];
    ans[1].resize(n); // set to be f_{root}
    rep0(i,0,n) ans[1][i] = a[i];
    mom[1].resize(1<<ass,0); // it's Okay
    Poly beta = Poly::inv(mom[1],ass);
    ans[1] = minus_convolution(ans[1],beta);
    for(int i=1,siz=n<<1; i!=n; ++i){
        if((i&-i) == i) siz >>= 1; // shrink
        ans[i].resize(siz); // how long is needed
        ans[i<<1] = minus_convolution(ans[i],mom[i<<1|1]);
        ans[i<<1|1] = minus_convolution(ans[i],mom[i<<1]);
    }
    rep0(i,0,m) printf("%d\n",ans[i^n][0]);
    return 0;
}

你可能感兴趣的:(数学,#,多项式/生成函数,C++,线性代数)