多项式大学习
多项式基础
多项式 FFT / NTT
以下只适合学过的人看
FFT 就简单背一下范德蒙德矩阵的结论、分治的方法以及逆变换的改动,就可以默写出代码了。
NTT 懒得推了。\(g\) 替换 \(w\) 就行。
半在线卷积
是的孩子们牛顿迭代已经被抛弃了
分治 NTT
给定 \(F\) 求 \(G\) 满足:
系数模 \(998244353\)。
推理:
\(n = 0\) 时:
\(n > 0\) 时:
分治地做,先算左边的所有 \(g\),再算左边对右边的贡献,再算右边的 \(g\)。
贡献是一个卷积的形式,可以 NTT。
时间复杂度:
由(类似)主定理知:
多叉分治优化
可以做 \(B\) 叉的分治以减少 DFT / IDFT 的次数以达到优化的目的。
多叉时依次算下去,每块累加点乘的值一并 IDFT 即可。
我也不知道为啥,但是反正 \(B = \log n\) 时有:
实际上为了保持长度是 \(2\) 的幂,一般取 \(B = 16\)。
模板汇总
为了模板化,封装了多项式结构体。
为了处理区间长度简便,默认在主程序里面把多项式长度调到 \(n = 2^k\) 的形式。
多项式乘法
只放最简单的 NTT 了,竞赛常用属于是。
const int mod = 998244353;
const int gmod = 3;
struct Poly {
int n;
vector<long long> a;
void redeg(int n_re) {
a.resize(n = n_re, 0);
}
int deg() {
return a.size();
}
void output(int output_deg) {
for(int i = 0; i < output_deg; i++) printf("%d ", a[i]);
puts("");
}
};
long long calc_pow(long long x, int k) { // calculate x ^ k % mod
long long res = 1;
for(; k; k >>= 1, x = x * x % mod) if(k & 1) res = res * x % mod;
return res;
}
void NTT(Poly& F, int op) {
int n = F.deg();
long long ginv = calc_pow(gmod, mod - 2);
// butterfly transform
vector<int> rev(n, 0);
for(int i = 1; i < n; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) * (n >> 1));
for(int i = 0; i < n; i++) if(rev[i] < i) swap(F.a[i], F.a[rev[i]]);
// NTT
for(int k = 1; k < n; k <<= 1) { // length
long long wn = calc_pow(op == 1 ? gmod : ginv, (mod - 1) / (k << 1));
for(int i = 0; i < n; i += (k << 1)) {
long long w = 1, x, y;
for(int j = i; j < i + k; j++, w = w * wn % mod) {
x = F.a[j], y = w * F.a[j + k] % mod;
F.a[j] = (x + y) % mod, F.a[j + k] = (x + mod - y) % mod;
}
}
}
// inv
if(op == -1) {
long long ninv = calc_pow(n, mod - 2);
for(int i = 0; i < n; i++) F.a[i] = F.a[i] * ninv % mod;
}
}
void Mul(Poly& F, Poly& G, Poly& H) {
int deg = H.deg();
Poly pF = F, pG = G;
NTT(pF, 1), NTT(pG, 1);
for(int i = 0; i < deg; i++) H.a[i] = pF.a[i] * pG.a[i] % mod;
NTT(H, -1);
}
多项式乘法逆
给定 \(F\) 求 \(G\) 满足:
考虑每一项有:
使用半在线卷积即可。
边界等注意事项之后不再赘述,读者可自行类比判断。
const int DAC_min = 128;
const int B = 16;
void Inv_DAC(Poly& F, Poly& G, int l, int r) { // [l, r)
if(r - l <= DAC_min) {
for(int i = l; i < r; i++) {
if(i == 0) G.a[i] = calc_pow(F.a[i], mod - 2);
else G.a[i] = 1ll * (mod - G.a[0]) * G.a[i] % mod; // mod - G.a[0] = - 1 / F.a[0]
for(int j = 1; j < r - i; j++) G.a[i + j] = (G.a[i + j] + G.a[i] * F.a[j]) % mod;
}
return ;
}
int len = (r - l) / B;
Poly pF[B], pG[B];
for(int i = 0; i < B; i++) pF[i].redeg(len << 1), pG[i].redeg(len << 1);
for(int i = 1; i < B; i++) { // calculate F (points)
for(int j = 0; j < len << 1; j++) pF[i].a[j] = F.a[(i - 1) * len + j];
NTT(pF[i], 1);
}
for(int i = 0; i < B; i++) { // calculate G (points)
NTT(pG[i], -1);
for(int j = 0; j < len; j++) G.a[l + i * len + j] = (G.a[l + i * len + j] + pG[i].a[len + j]) % mod;
Inv_DAC(F, G, l + i * len, l + (i + 1) * len);
pG[i].redeg(len << 1);
for(int j = 0; j < len; j++) pG[i].a[j] = G.a[l + i * len + j], pG[i].a[j + len] = 0;
NTT(pG[i], 1);
for(int j = i + 1; j < B; j++) { // calculate from i to j
for(int k = 0; k < len << 1; k++) pG[j].a[k] = (pG[j].a[k] + pG[i].a[k] * pF[j - i].a[k]) % mod;
}
}
for(int i = 0; i < B; i++) pF[i].redeg(0), pG[i].redeg(0);
}
void Inv(Poly& F, Poly& G, int deg) {
Inv_DAC(F, G, 0, deg);
}
多项式对数
给定 \(F\) 求 \(G\) 满足:
考虑求个导。
只需要求导、求逆、乘法和积分就做完了。
void Ln(Poly& F, Poly& G, int deg) {
Poly dF, Fn, dG;
dF.redeg(deg << 1), Fn.redeg(deg << 1), dG.redeg(deg << 1);
for(int i = 1; i < deg; i++) dF.a[i - 1] = F.a[i] * i % mod; dF.a[deg - 1] = 0;
for(int i = deg; i < deg << 1; i++) dF.a[i] = 0, Fn.a[i] = 0;
Inv(F, Fn, deg);
Mul(dF, Fn, dG);
for(int i = deg - 2; i >= 0; i--) G.a[i + 1] = dG.a[i] * calc_pow(i + 1, mod - 2) % mod; G.a[0] = 0;
dF.redeg(0), Fn.redeg(0), dG.redeg(0);
}
多项式指数
给定 \(F\) 求 \(G\) 满足:
考虑求个导。
对每一项:(这里看第 \(n - 1\) 项)
这是一个半在线卷积。
void Exp_DAC(Poly& F, Poly& G, int l, int r) {
if(r - l <= DAC_min) {
for(int i = l; i < r; i++) {
if(i == 0) G.a[i] = 1;
else G.a[i] = G.a[i] * inv[i] % mod;
for(int j = 1; j < r - i; j++) G.a[i + j] = (G.a[i + j] + G.a[i] * F.a[j] % mod * j) % mod;
}
return ;
}
int len = (r - l) / B;
Poly pF[B], pG[B];
for(int i = 0; i < B; i++) pF[i].redeg(len << 1), pG[i].redeg(len << 1);
for(int i = 1; i < B; i++) { // calculate pF
for(int j = 0; j < len << 1; j++) pF[i].a[j] = F.a[(i - 1) * len + j] * ((i - 1) * len + j) % mod;
NTT(pF[i], 1);
}
for(int i = 0; i < B; i++) { // calculate pG
NTT(pG[i], -1);
for(int j = 0; j < len; j++) G.a[l + i * len + j] = (G.a[l + i * len + j] + pG[i].a[len + j]) % mod;
Exp_DAC(F, G, l + i * len, l + (i + 1) * len);
for(int j = 0; j < len << 1; j++) pG[i].a[j] = j >= len ? 0 : G.a[l + i * len + j];
NTT(pG[i], 1);
for(int j = i + 1; j < B; j++) {
for(int k = 0; k < len << 1; k++) pG[j].a[k] = (pG[j].a[k] + pG[i].a[k] * pF[j - i].a[k]) % mod;
}
}
}
void Exp(Poly& F, Poly& G, int deg) {
Exp_DAC(F, G, 0, deg);
}
多项式快速幂
给定 \(F\) 求 \(G\) 满足:
考虑换底(先取对数再取指数),有:
只需对数、数乘和指数。
void Power(Poly& F, Poly& G, int deg, int k) {
Poly H;
H.redeg(deg);
Ln(F, H, deg);
for(int i = 0; i < deg; i++) H.a[i] = H.a[i] * k % mod;
Exp(H, G, deg);
}
例题
残缺的字符串
先考虑没有通配符。KMP 秒了
但是要为通配符铺路,我们换个视角。
不妨定义两个等长串的距离:记 \(S\) 的坐标为 \((C(S_0),C(S_1),\cdots,C(S_{n - 1}))\),其中 \(n\) 为串长,\(C(a) = 1,C(b) = 2\),依此类推。\(S\) 与 \(T\) 之间的距离就是坐标距离的平方(便于计算)。
那么两个等长串 \(S = (f_0,f_1, \cdots,f_{n - 1})\) 和 \(T = (g_0,g_1, \cdots,g_{n - 1})\) 相等的充要条件是:
带入 \(A,B\) 并展开这个式子:(\(B_i = B[i,\cdots,i + |A| - 1]\))
前两项容易求。后面的是一个差卷积,取 \(h_i = f_{n - 1 - i} (i < n),h_i = 0(i \ge n)\) 变换得
其中补全若干项是合理的,因为 \(h\) 的高位是 0。
那么就可以 \(O(n \log n)\) 计算所有的距离了。
有通配符时:
定义 \(C(*) = 0\)。
改距离为:
即可。
展开后对每一项做卷积就行。
P3711 仓鼠的数学题
实在看不懂,直接记结论算了。
重新定义 \(S_k(x) = \sum_{i = 0} ^ {x - 1} i^k\)。有结论:
其中伯努利数 \(B_i\) 的指数生成函数为 \(\dfrac{x}{e^x - 1}\)。那么容易通过求逆求出 \(B_i\)。
证明是把这个 \(x\) 换成 \(n\),再考虑 \(S_k(n)\) 的指数生成函数。
那么就有:
\(\sum_{k = i - 1} ^ {n} \dfrac{a_k}{k!}\dfrac{B_{k + 1 - i}}{(k + 1 - i)!}\) 拿差卷积算即可。
最后要 \(x \rightarrow x + 1\),令上面求出的多项式为 \(F(x)\),转换后为 \(G(x)\)。
那么又是一个差卷积。
P5488 差分与前缀和
这题相比之下就很简单了。
前缀和一次就是乘以 \(\dfrac{1}{1 - x}\),具体证明直接展开容易看出。
差分又是前缀和的逆运算,故答案为 \(f(x)\dfrac{1}{(1-x)^k}\) 或 \(f(x)(1-x)^k\)。
只需要多项式 \(\ln, \exp\) 以及数乘。
P4841 [集训队作业2013] 城市规划
这题用容斥就反而麻烦了,注意不要想歪就行了。
记 \(n\) 节点简单连通图数量为 \(f_n\),\(n\) 节点简单图数量为 \(g_n\)。
明显 \(g_n = 2^{\binom{n}{2}}\)。
考虑求 \(f\) 与 \(g\) 的关系。枚举 \(g\) 中 \(n\) 号节点所在连通块的大小,有:
拆开式子并定义 \(f_0 = 1\),有:
也即:
解得
由 EGF \(\exp\) 的意义也可导出。
P5915 冬至
两种方法都想了,没推到最后...
令人感到巧合的是此题也是同时能用矩阵递推和波斯坦茉莉做。
P7844「dWoi R2」FFT / 狒狒贴
求 NTT 做 \(k\) 次后的结果。有结论:
NTT 做两次相当于每个位置乘以 \(n\),然后翻转 \(f_1,\cdots,f_n\)。也有用这个性质来写 NTT 板子的,好处是不用求原根的逆元。
证明考虑直接算 NTT 矩阵的乘积就行。
那么NTT 做四次相当于每个位置乘以 \(n^2\),这些一起搞定,再做 \(k \bmod 4\) 次 NTT 即可。
P4389 付公主的背包
其生成函数为:
关键是怎么快速求出来。
容易想到求逆可以最后一起做,然后分治来乘。但这样时间其实不对。
考虑求个对数,加起来后最后 \(\exp\)。
可以统计 \(v\) 相同的一并贡献,只需在 \(v\) 的倍数次数上贡献,那么这是 \(O(m \log m)\) 的。
最后来一个 \(\exp\) 即可。也是 \(O(n \log n)\)(牛顿迭代)或 \(O(\dfrac{n \log ^ 2 n}{\log \log n})\)(半在线卷积)。
P5748 集合划分计数
好像叫贝尔数 \(bell(n)\)。
考虑 \(n\) 个数组成一个非空集合的方案数的 EGF 显然为 \(e^x - 1\)。
由 EGF 的 \(\exp\) 的意义得答案为 \([x^n]\exp (\exp(x) - 1)\)。
P7092 计数题
P7435 简单的排列计数
P5641 【CSGRound2】开拓者的卓识
这个 \(sum_{k, l, r}\) 的贡献肯定是每个 \(i \in [l, r]\) 的 \(a_i\) 乘以某个位置相关系数。考虑 \(a_i\) 的系数:
相当于求区间序列 \([l_0, r_0], [l_1, r_1], \cdots, [l_k, r_k]\) 满足 \([l_{i + 1}, r_{i + 1}] \subseteq [l_i, r_i], [l_0, r_0] = [l, r], [l_k, r_k] = [i, i]\) 的方案数。这是球盒问题的变种,于是答案为:
拆开式子直接卷积就行。
P9164 「INOH」Round 1 - 狂气
理论上甚至可以多叉分治。

浙公网安备 33010602011771号