多项式
多项式
定义
定义 \(f(x) = \sum_{k = 0} ^ m a_k x ^ k\) 为一个 \(m\) 次(\(m + 1\) 阶)的 多项式,其中 \(m \in [0, +\infty)\)。
如果一个和式可以按列相加,则称其为 级数。注意,该和式可以有无穷多项,即 \(\sum_{k = 0} ^ \infty a_k x ^ k\)。
如果对于每一项的指数 \(i\) 都满足 \(i \in \mathbb{N}\),则称这种级数为 形式幂级数。所谓形式幂级数,其实就是广义的多项式。
注意,形式幂级数与幂级数不同,我们并不关系幂级数是否收敛和是否有确定的取值。
在下文中,如果不加特殊说明,幂级数均指代形式幂级数。
定义一个 多项式环 为若干个多项式组成的集合 \(S[x]\),其中对于任意的两个多项式 \(P(x), Q(x) \in S[x]\),均满足:
- 加法封闭性:\(P(x), Q(x) \in S[x] \Rightarrow P(x) + Q(x) \in S[x]\)。
- 乘法封闭性:\(P(x), Q(x) \in S[x] \Rightarrow P(x) \times Q(x) \in S[x]\)。
- 分配律:\(P(x) \times (Q(x) + R(x)) = P(x) \times Q(x) + P(x) \times R(x)\)。
- 其他性质,详细请参考 基础群论。
最经典的例子是 \(\mathbb{R}[x]\),代表系数是实数的多项式组成的集合。
推广:定义 形式幂级数环 为若干个幂级数组成的集合 \(\mathbb{R}[[x]]\)。
多项式乘法(卷积)
乘法是多项式操作中最重要的操作,得益于 \(\text{FFT}\) 算法,我们可以在 \(O(n \log n)\) 的时间复杂度内完成多项式的乘法。
对于两个多项式 \(f(x) = \sum_{i = 0} ^ n a_i x ^ i\) 和 \(g(x) = \sum_{i = 0} ^ m b_i x ^ i\),规定其乘法为:
推广:对于两个幂级数 \(f(x), g(x)\),定义其卷积为:
有关高效计算卷积的相关内容可以参考 FFT 与 NTT。
多项式求导
关于求导的前置知识可以参考 基础微积分
对幂级数求导只具有形式意义,定义幂级数的形式导数为:
对 \(n\) 阶多项式求导需要在模 \(x ^ n\) 的意义下进行。
注:对 \(n\) 阶多项式求导会失去 \([x ^ 0]f(x)\) 的数值,使得有效位数少一位。
多项式积分
关于积分的前置知识可以参考 基础微积分
对幂级数积分只具有形式意义,定义幂级数的形式积分为:
对 \(n\) 阶多项式积分需要在模 \(x ^ n\) 的意义下进行。
注:对 \(n\) 阶多项式积分一般使用 \(C = 0\) 填充 \([x ^ 0]f(x)\),使得有效位数多一位。
因此,对于一个多项式 \(f(x)\),当且仅当 \([x ^ 0] f(x) = 0\),有:
多项式求逆
在普通生成函数中,我们曾经推导出:
这启发我们,多项式的乘法逆元并不一定是多项式,而是幂级数。
因此,对于 \(f(x) = \sum_{i = 0} ^ n a_i x ^ i\),定义其 乘法逆元 为 \(f ^ {-1}(x) = \sum_{i = 0} ^ \infty b_i x ^ i\),满足:\(f(x) \times f ^ {-1}(x) = 1\)
不妨展开上式:
注意:当 \(a_0 = 0\) 时,原式不存在逆元。
一般在算法竞赛中,我们会求 \(f(x)\) 在模 \(x ^ n\) 意义下的乘法逆元 \(g(x)\),即 \(f(x) \times g(x) \equiv 1 \pmod{x ^ n}\),目的是把 \(f ^ {-1}(x)\) 限制在至多 \(n\) 阶。
在实现时我们可以把 \(n\) 填充到 \(2\) 的整数次幂,以简化对折半的处理,也使得递推实现更加方便。
求解逆元可以使用递归的思想,而实现时采用递推实现。
假设我们已经求得了 \(f(x) \times g'(x) \equiv 1 \pmod{x ^ \frac{n}{2}}\),而我们希望求出 \(f(x) \times g(x) \equiv 1 \pmod{x ^ n}\),则有如下的变换:
则原式可以通过迭代实现求解 \(f(x)\) 的乘法逆元,时间复杂度为 \(T(n) = T(\frac{n}{2}) + O(n \log n) = O(n \log n)\)。
对于使用迭代法实现的函数,在调用时需要注意不能传入自身作为参数,同时需要注意先清空数组再进行迭代
多项式对数函数
对多项式进行对数/指数函数变换都会得到无穷项的幂级数,因此下文的计算默认在模 \(x ^ n\) 意义下。
对于 \(f(x) = \sum_{i = 0} ^ n a_i x ^ i\),\(\ln(f(x))\) 存在必须满足 \(a_0 = 1\)。
对 \(\ln(f(x))\) 作如下变换:
因此我们可以对 \(f(x)\) 求导,求逆,卷积,再积分,时间复杂度为 \(O(n \log n)\)。
多项式指数函数
对于 \(f(x) = \sum_{i = 0} ^ n a_i x ^ i\),\(g(x) = \exp(f(x))\) 存在必须满足 \(a_0 = 0\)。
求解 \(\exp(F(x))\) 可以使用牛顿迭代法。
牛顿迭代法
原理讲解待补充
推广:
如何使用牛顿迭代法求 \(g(x) = \exp(f(x))\)?首先先对式子做一些变换:
不妨设 \(g_0(x) \equiv \exp(F(x)) \pmod {x ^ \frac{n}{2}}\) 为上一轮迭代的值,则有:
注意,对 \(h(g_0(x))\) 求导时,我们认为自变量是 \(g_0\) 这个函数本身,而不是 \(x\),也就是说,我们把 \(f(x)\) 当作了一个常量。
多项式快速幂
使用多项式 \(\ln\) 和 \(\exp\) 可以容易将多项式幂的问题转化为对系数点乘的问题,需要注意多项式不存在扩展欧拉定理,对指数取模直接对质数本身取模即可。
多项式开根
多项式开根可以转化为多项式 \(\frac{1}{2}\) 次方的问题,对有理数取模即可。
多项式三角函数
多项式 \(\sin\) 和 \(\cos\) 虽然算法竞赛中鲜有运用,但还是记录一下。
使用欧拉公式可知:
简单变形可得:
因此可以推导出 \(\cos(x)\) 和 \(\cos(x)\) 关于 \(e ^ {ix}\) 和 \(e ^ {-ix}\) 的反演:
其中,\(i\) 在复数意义下相当于四次单位根,因此 \(i \equiv g ^ \frac{p - 1}{4} \pmod p\),其中 \(p\) 是 \(\text{NTT}\) 使用的模数,形如 \(2 ^ m \times n + 1\)。
使用多项式 \(\exp\) 即可求解上式。
一个简单的 \(\text{trick}\):\(e ^ {-ix}\) 可以由 \(e ^ {ix}\) 求逆来计算,减小常数。
多项式反三角函数
多项式 \(\arcsin\) 等反三角函数更是鲜有应用。
求解多项式反三角函数的方法是对原式求导再积分:
则原式得以求解。
求解其他反三角函数的方法与该方法类似,这里不再赘述。
多项式复合
泰勒级数
对于函数 \(f(x)\),如果它在一点 \(x = a\) 处有充分阶导数,我们可以使用一个级数来逼近它。
根据导数的定义,我们可以作如下的变换:
迭代上述过程,可得:
将 \(h\) 代换为 \(x - a\),可得:
一般情况下,我们会对函数 \(f(x)\) 在 \(x = 0\) 处展开,所得的泰勒级数也称 麦克劳林级数。
常用结论:
模板
多项式模板代码,汇总了部分常用的功能。
#include <bits/extc++.h>
#define inline __always_inline
const int MaxA = 1 << 21, MaxN = 1e5 + 5, mod = 998244353, G[2] = {3, (mod + 1) / 3};
inline int qpow(int x, int n)
{
int ans = 1;
for (; n; n >>= 1, x = 1l * x * x % mod)
if (n & 1) ans = 1l * ans * x % mod;
return ans;
}
int lim, l, w[2][MaxA], r[MaxA], inv[MaxN]; // NTT 数域内的多项式次数实际最高为 lim - 1 次。
void init_inv()
{
inv[1] = 1;
for (int i = 2; i < MaxN; i++) inv[i] = mod - 1l * mod / i * inv[mod % i] % mod;
}
void init_NTT(int n) // NTT 数域内的多项式次数期望最高为 n - 1 次。
{
for (lim = 1, l = 0; lim < n; lim <<= 1, l++);
for (int i = 0; i < lim; i++)
r[i] = r[i >> 1] >> 1 | (i & 1) << l - 1; // 预处理位逆序
for (int h = 1; h < lim; h <<= 1) // 预处理 2 ^ n 次单位根
w[0][h] = qpow(G[0], (mod - 1) / (h << 1)),
w[1][h] = qpow(G[1], (mod - 1) / (h << 1));
}
struct poly_t
{
int a[MaxA];
poly_t() { memset(a, 0, sizeof(a)); }
inline auto &operator[](int p) { return a[p]; }
inline auto &operator[](int p) const { return a[p]; }
int operator()(int x) const
{
int ans = 0;
for (int i = 0; i < lim; i--) ans = (1l * ans * x + a[i]) % mod;
return ans;
}
poly_t &operator+=(const poly_t &f)
{
for (int i = 0; i < lim; i++) a[i] = (a[i] + f[i]) % mod;
return *this;
}
poly_t &operator*=(int x)
{
for (int i = 0; i < lim; i++) a[i] = 1l * a[i] * x % mod;
return *this;
}
poly_t &operator*=(const poly_t &f)
{
for (int i = 0; i < lim; i++) a[i] = 1l * a[i] * f[i] % mod;
return *this;
}
poly_t &Diff(const poly_t &f, int n)
{
for (int i = 1; i < n; i++) a[i - 1] = 1l * f[i] * i % mod;
a[n - 1] = 0; return *this;
}
poly_t &Int(const poly_t &f, int n)
{
for (int i = n - 1; i; i--) a[i] = 1l * f[i - 1] * inv[i] % mod;
a[0] = 0; return *this;
}
poly_t &NTT(int t)
{
for (int i = 0; i < lim; i++)
if (i < r[i]) std::swap(a[i], a[r[i]]);
for (int h = 1; h < lim; h <<= 1) // 枚举半步长
for (int i = 0; i < lim; i += h << 1) // 枚举起始位置 i
for (int j = 0, s = 1; j < h; j++, s = 1l * s * w[t][h] % mod) // s = w ^ j
{
int u = a[i | j], v = 1l * s * a[i | h | j] % mod; // 使用位运算简化代码
a[i | j] = (u + v) % mod, a[i | h | j] = (u - v + mod) % mod; // 蝴蝶操作
}
if (t) for (int i = 0, inv = qpow(lim, mod - 2); i < lim; i++) // 如果是 NTT 逆变换,不要忘记对系数 / lim
a[i] = 1l * a[i] * inv % mod;
return *this;
}
poly_t &Inv(const poly_t &f, int n)
{
assert(&f != this), memset(a, 0, sizeof(a)); // ! important
poly_t g; a[0] = qpow(f[0], mod - 2);
for (int m = 2; m < n << 1; m <<= 1)
{
memcpy(g.a, f.a, m << 2);
init_NTT(m << 1), NTT(0), g.NTT(0); // ! 必须扩大 NTT 数域以保证精度
for (int i = 0; i < lim; i++)
a[i] = (2 - 1l * g[i] * a[i] % mod + mod) * a[i] % mod;
NTT(1);
memset(a + m, 0, m << 2); // 尽管最初扩大了一倍 NTT 数域,但实际可用的部分仍然只有 [0, m) 部分
}
return *this;
}
poly_t &ln(const poly_t &f, int n)
{
poly_t g, h; init_NTT(n << 1);
return Int((g.Diff(f, n).NTT(0) *= h.Inv(f, n).NTT(0)).NTT(1), n);
}
poly_t &exp(const poly_t &f, int n)
{
assert(&f != this), memset(a, 0, sizeof(a)); // ! important
poly_t g; a[0] = 1;
for (int m = 2; m < n << 1; m <<= 1)
{
g.ln(*this, m)[0]--; // 不要忘记对常数项加 1
for (int i = 0; i < m; i++) g[i] = (f[i] - g[i] + mod) % mod;
init_NTT(m << 1), (NTT(0) *= g.NTT(0)).NTT(1);
}
return *this;
}
poly_t &pow(const poly_t &f, int k, int n) { return exp(poly_t(ln(f, n) *= k), n); }
};

浙公网安备 33010602011771号