牛顿迭代法在多项式运算的应用
总算是快把 FFT 和生成函数的各种东西补了好多,膜拜策爷的论文和 Picks 的博客 QAQ
这篇文章大概就是说如何用牛顿迭代法来解满足 $G(F(z)) \equiv 0 \pmod {z^n}$ 的 $F(z)$
然后这东西可以比较方便地计算 $\sqrt{A(z)}$、$e^{A(z)}$,也就是多项式开根、求指数对数之类鬼畜的东西,在生成函数计数中十分有用
顺便一提,这里说的“多项式”实际上你可以直接理解为生成函数或者形式幂级数
Newton’s Method
好,现在问题是这样的,已经知道了一个函数 $G(z)$,求一个多项式 $F(z) \bmod {z^n}$,满足方程
\[G(F(z)) \equiv 0 \pmod {z^n}\]然后这个问题嘛…… 可以回忆一下多项式求逆的过程
首先 $n = 1$ 的时候,$G(F(z)) \equiv 0 \pmod z$,这是要单独求出来的
现在假设已经求出了
\[G(F_0(z)) \equiv 0 \pmod {z^{\lceil \frac{n}{2} \rceil}}\]考虑如何扩展到 $\bmod z^n$ 下,可以把 $G(F(z))$ 在 $F_0(z)$ 这里进行泰勒展开
\[\begin{eqnarray*} G(F(z)) & = & G(F_0(z)) \\ & + & \frac{G'(F_0(z))}{1!}\left(F(z) - F_0(z)\right) \\ & + & \frac{G''(F_0(z))}{2!}\left(F(z) - F_0(z)\right)^2 \\ & + & \cdots \end{eqnarray*}\]因为 $F(z)$ 和 $F_0(z)$ 的最后 $\lceil \frac{n}{2} \rceil$ 项相同,所以 $\left(F(z) - F_0(z)\right)^2$ 的最低的非 $0$ 项次数大于 $2\lceil \frac{n}{2} \rceil$,所以可以得到
\[G(F(z)) \equiv G(F_0(z)) + G'(F_0(z))\left(F(z) - F_0(z)\right) \pmod {z^n}\]然后因为 $ G(F(z)) \equiv 0 \pmod {z^n} $,可以得到
\[F(z) \equiv F_0(z) - \frac{G(F_0(z))}{G'(F_0(z))} \pmod {z^n}\]然后好像就完了?现在来看看它能干什么用好了
应用
多项式开方
这是给出 $A(z)$,求 $B(z)$,满足方程
\[B^2(z) \equiv A(z) \pmod {z^n}\]然后我们可以构造方程 $F^2(z) - A(z) = 0$,目的就是要求解 $F(z) \bmod z^n$
这时候 $G(F(z)) = F^2(z) - A(z)$,$G’(F(z)) = 2F(z)$,带到上面的迭代方程
\[\begin{eqnarray*} F(z) & \equiv & F_0(z) - \frac{F_0^2(z) - A(z)}{2F_0(z)} \\ & \equiv & \frac{F_0^2(z) + A(z)}{2F_0(z)} \pmod {z^n} \end{eqnarray*}\]然后就可以计算了,复杂度是 $\mathcal O(n\log n)$
当系数是在模意义下的时候,有点麻烦的其实是常数项的确定,如果 $[z^0]A(z) = 1$ 还好,否则你可能要计算二次剩余
例. [Codeforces #250 Div1 E] The Child And Binary Tree
有一个含有 $n$ 个元素的正整数集合 $S = { c_1, c_2, \cdots, c_n }$,我们称一个节点带权的有根二叉树是好的,当且仅当对于每个节点 $v$,$v$ 的权值在 $S$ 内,并且我们称这棵树的权值为所有节点的权值和
现在给出一个正整数 $m$,你要计算出对于所有正整数 $1 \leq s \leq m$,有多少不同的好的二叉树满足它的权值是 $s$
答案对 $998244353 (7 \times 17 \times 2^{23} + 1$,一个质数$)$ 取模,其中 $1 \leq n, m, c_i \leq 10^5$
题目链接:Codeforces-438E、BZOJ-3625
Example:比如说 $S = {1, 2}$,$s = 3$ 的时候一共有 $9$ 种方案
Solution:嗯…… 既然我放在了这里不用说肯定是和多项式开方有关系啦
直接用生成函数来考虑这个问题的话,对于一个节点来说,它的生成函数是
\[T(z) = \sum_{s \in S} z^s\]现在假设答案的生成函数是 $F(z)$,那么由于二叉树的递归性质,可以由左右子树 $F^2(z)$ 加上一个根节点 $T(z)$ 组合而成,并且还有一个空树我们也算作一种,那么可以得到方程
\[F(z) = 1 + T(z) F^2(z)\]解出来可以得到
\[F(z) = \frac{1 - \sqrt{1 - 4T(z)}}{2T(z)}\]然后剩下的就是多项式开方和多项式求逆了
如果从递推的角度来考虑的话,我们可以设权值为 $s$ 的方案有 $f_s$ 种,那么可以得到方程
\[f_s = \sum_{i \in S, s - i \geq 0}\sum_{j = 0}^{s - i} f_j f_{s - i - j}\]边界条件是 $f_0 = 1$,这样你会发现 $f_s$ 里面的求和是个卷积的形式,同样可以转换为生成函数
#include <cstdio>
#include <algorithm>
using std::swap;
using std::fill;
using std::copy;
typedef int value_t;
typedef long long calc_t;
const int MaxN = 1 << 19;
const value_t mod_base = 119, mod_exp = 23;
const value_t mod_v = (mod_base << mod_exp) + 1;
const value_t primitive_root = 3;
int epsilon_num;
value_t eps[MaxN], inv_eps[MaxN], inv2;
value_t dec(value_t x, value_t v) { x -= v; return x < 0 ? x + mod_v : x; }
value_t inc(value_t x, value_t v) { x += v; return x >= mod_v ? x - mod_v : x; }
value_t pow(value_t x, value_t p)
{
value_t v = 1;
for(; p; p >>= 1, x = (calc_t)x * x % mod_v)
if(p & 1) v = (calc_t)x * v % mod_v;
return v;
}
void init_eps(int num)
{
epsilon_num = num;
value_t base = pow(primitive_root, (mod_v - 1) / num);
value_t inv_base = pow(base, mod_v - 2);
eps[0] = inv_eps[0] = 1;
for(int i = 1; i != num; ++i)
{
eps[i] = (calc_t)eps[i - 1] * base % mod_v;
inv_eps[i] = (calc_t)inv_eps[i - 1] * inv_base % mod_v;
}
}
void transform(int n, value_t *x, value_t *w = eps)
{
for(int i = 0, j = 0; i != n; ++i)
{
if(i > j) swap(x[i], x[j]);
for(int l = n >> 1; (j ^= l) < l; l >>= 1);
}
for(int i = 2; i <= n; i <<= 1)
{
int m = i >> 1, t = epsilon_num / i;
for(int j = 0; j < n; j += i)
{
for(int p = 0, q = 0; p != m; ++p, q += t)
{
value_t z = (calc_t)x[j + m + p] * w[q] % mod_v;
x[j + m + p] = dec(x[j + p], z);
x[j + p] = inc(x[j + p], z);
}
}
}
}
void inverse_transform(int n, value_t *x)
{
transform(n, x, inv_eps);
value_t inv = pow(n, mod_v - 2);
for(int i = 0; i != n; ++i)
x[i] = (calc_t)x[i] * inv % mod_v;
}
void polynomial_inverse(int n, value_t *A, value_t *B)
{
static value_t T[MaxN];
if(n == 1)
{
B[0] = pow(A[0], mod_v - 2);
return;
}
int half = (n + 1) >> 1;
polynomial_inverse(half, A, B);
int p = 1;
for(; p < n << 1; p <<= 1);
fill(B + half, B + p, 0);
transform(p, B);
copy(A, A + n, T);
fill(T + n, T + p, 0);
transform(p, T);
for(int i = 0; i != p; ++i)
B[i] = (calc_t)B[i] * dec(2, (calc_t)T[i] * B[i] % mod_v) % mod_v;
inverse_transform(p, B);
}
void polynomial_sqrt(int n, value_t *A, value_t *B)
{
static value_t T[MaxN];
if(n == 1)
{
B[0] = 1; // sqrt A[0], here is 1
return;
}
int p = 1;
for(; p < n << 1; p <<= 1);
int half = (n + 1) >> 1;
polynomial_sqrt(half, A, B);
fill(B + half, B + n, 0);
polynomial_inverse(n, B, T);
fill(T + n, T + p, 0);
transform(p, T);
fill(B + half, B + p, 0);
transform(p >> 1, B);
for(int i = 0; i != p >> 1; ++i)
B[i] = (calc_t)B[i] * B[i] % mod_v;
inverse_transform(p >> 1, B);
for(int i = 0; i != n; ++i)
B[i] = (calc_t)inc(A[i], B[i]) * inv2 % mod_v;
transform(p, B);
for(int i = 0; i != p; ++i)
B[i] = (calc_t)B[i] * T[i] % mod_v;
inverse_transform(p, B);
}
value_t tmp[MaxN];
value_t A[MaxN], B[MaxN], C[MaxN], T[MaxN];
int main()
{
int n, m;
std::scanf("%d %d", &n, &m);
int min_v = ~0u >> 1;
for(int i = 0; i != n; ++i)
{
std::scanf("%d", tmp + i);
if(min_v > tmp[i]) min_v = tmp[i];
}
inv2 = mod_v - mod_v / 2;
int p = 1;
for(; p < (m + min_v + 1) << 1; p <<= 1);
init_eps(p);
A[0] = 1;
for(int i = 0; i != n; ++i)
{
int x = tmp[i];
T[x - min_v] = 2;
A[x] = mod_v - 4;
}
polynomial_inverse(m + min_v + 1, T, C);
polynomial_sqrt(m + min_v + 1, A, B);
B[0] = dec(1, B[0]);
for(int i = 1; i <= m + min_v; ++i)
B[i] = mod_v - B[i];
for(int i = 0; i <= m; ++i)
B[i] = B[i + min_v];
fill(B + m + 1, B + p, 0);
fill(C + m + 1, C + p, 0);
transform(p, B);
transform(p, C);
for(int i = 0; i != p; ++i)
B[i] = (calc_t)B[i] * C[i] % mod_v;
inverse_transform(p, B);
for(int i = 1; i <= m; ++i)
std::printf("%d\n", B[i]);
return 0;
}
多项式的对数和指数函数
啥…… 多项式的对数是什么?其实我们可以认为它是一个多项式和麦克劳林级数的复合,也就是给出一个多项式 $A(z) = \sum_{i \geq 1} a_iz^i$
\[\ln (1 - A(z)) = -\sum_{i \geq 1} \frac{A^i(z)}{i}\]指数函数同样可以这样定义
\(\exp(A(z)) = e^{A(z)} = \sum_{i \geq 0} \frac{A^i(z)}{i!}\)
对数的计算
对于一个多项式 $A(z) = 1 + \sum_{i \geq 1}a_iz^i $,现在要计算其对数 $\ln A(z)$
注意这里 $A(z)$ 的常数项是 $1$ 是因为上面的定义,因为到直接计算似乎很难计算,我们考虑对其求导后的结果
\[(\ln A(z))' = \frac{A'(z)}{A(z)}\]也就是说,我们要计算出 $A(z)$ 的逆元,这可以在 $\mathcal O(n\log n)$ 的时间内完成,那么
\[\ln A(z) = \int \frac{A'(z)}{A(z)}\]至于多项式的求导和积分,这是都是 $\mathcal O(n)$ 复杂度的,因此计算对数的时间是 $\mathcal O(n\log n)$
指数的计算
对于一个多项式 $A(z) = \sum_{i \geq 1}a_iz^i $,现在要计算其指数 $e^{A(z)}$
如果和对数计算一样直接求导,你会发现是不可行的,因为求导完还有指数函数自身,这时候就要利用刚刚所说的牛顿迭代,我们需要的方程实际上是
\[F(z) = e^{A(z)}\]变形后变成
\[\ln F(z) - A(z) = 0\]构造函数 $G(F(z)) = \ln F(z) - A(z)$,这时候 $G’(F(z)) = \frac{1}{F(z)}$,然后得到递推式
\[\begin{eqnarray*} F(z) & \equiv & F_0(z) - \frac{G(F_0(z))}{G'(F_0(z))} \\ & \equiv & F_0(z) \left (1 - \ln F_0(z) + A(z) \right ) \pmod {z^n}\end{eqnarray*}\]然后 $F(z)$ 的常数项是 $1$,最后复杂度是
\(T(n) = T(\frac{n}{2}) + \mathcal O(n\log n) = \mathcal O(n\log n)\)
void polynomial_logarithm(int n, value_t *A, value_t *B) { static value_t T[MaxN]; int p = 1; for(; p < n << 1; p <<= 1); polynomial_inverse(n, A, T); fill(T + n, T + p, 0); transform(p, T); // derivative copy(A, A + n, B); for(int i = 0; i < n - 1; ++i) B[i] = (calc_t)B[i + 1] * (i + 1) % mod_v; fill(B + n - 1, B + p, 0); transform(p, B); for(int i = 0; i != p; ++i) B[i] = (calc_t)B[i] * T[i] % mod_v; inverse_transform(p, B); // integral for(int i = n - 1; i; --i) B[i] = (calc_t)B[i - 1] * inv[i] % mod_v; B[0] = 0; } void polynomial_exponent(int n, value_t *A, value_t *B) { static value_t T[MaxN]; if(n == 1) { B[0] = 1; return; } int p = 1; for(; p < n << 1; p <<= 1); int half = (n + 1) >> 1; polynomial_exponent(half, A, B); fill(B + half, B + p, 0); polynomial_logarithm(n, B, T); for(int i = 0; i != n; ++i) T[i] = dec(A[i], T[i]); T[0] = inc(T[0], 1); transform(p, T); transform(p, B); for(int i = 0; i != p; ++i) B[i] = (calc_t)B[i] * T[i] % mod_v; inverse_transform(p, B); }
任意次幂的计算
给出一个多项式 $A(z)$,你现在要计算 $A^k(z), k \in \mathbb Q$
对于 $k \in \mathbb N$ 的部分,可以直接使用快速幂来计算,这样复杂度是 $\mathcal O(n\log n\log k)$
现在有了求指数和求对数的运算,那么
\[A^k(z) = e^{k \ln A(z)}\]这样就可以在 $\mathcal O(n\log n)$ 的时间内计算出 $A^k(z)$,包括开方等也可以这样来计算