多项式(2):拉格朗日插值相关
之前我为什么会把 gf 和拉格朗日插值放到一起杂谈啊(捂脸,很奇怪啊
我在康复训练,这篇总结里面应该不会涉及太高妙的内容 qwq,大佬们觉得太水请掠过
大概也是杂谈向的文章吧。
0. 一些碎碎念
对于某些问题我们可以考虑用多项式刻画,系数表示的多项式加法和乘法很容易,但是对于乘法需要用 \(O(nm)\) 的复杂度,非常劣。然而用点值表示的方式,对于一个 \(n\) 次多项式,我们取 \((n+1)\) 个点值,那么就可以唯一确定这个多项式(因为可以待定系数建立 \((n+1)\) 个线性方程组),于是我们发现这个多项式和这 \((n+1)\) 个点值形成双射。同时,在这个点值体系下,乘法操作只需要对应点值相乘即可。
采用这个系数表示和点值表示的双射,我们可以处理非常多的问题。其中一大类就是多项式乘法,带入合适的点值,加速插值的过程和还原系数的过程,即 FFT 的过程。另一大类就是利用拉格朗日插值法。拉插常用的手法存在于:对于一类 dp 之类的问题,发现关于某一个维度居然是一个低次多项式,并且这个维度很大;或者建立生成函数,但是直接求解需要多项式做乘法,开销太大,于是采用拉插的方式,带入一些点值插出来然后再算系数。总之是一个很容易直观感受出来的东西。
这篇文章主要会讲拉插。
1. 拉格朗日插值
对于 \(n\) 个点值 \((x_1, y_1), (x_2, y_2), \dots, (x_n, y_n)\),那么经过这些点的 \((n - 1)\) 次多项式为:
不难看到这个式子中存在着一个“消除贡献”的逻辑。很明显我们可以将最终的结果写成一个 \(\sum\limits_{i = 1}^n y_i G_i(k)\) 的形式,\(G_i(k)\) 为一个 \((n - 1)\) 次多项式。考虑它要满足的约束,当 \(k = x_i\) 时,要求 \(G_i(k) = 1\),当 \(k = x_j \not = x_i\) 时,要求 \(G_i(k) = 0\)。于是我们想到构造出 \(G_i(k) = \prod\limits_{j \not = i}\dfrac{k - x_j}{x_i - x_j}\),作为这个 \(G_i(k)\)。
一些小小的拓展。
1.1 求解系数
拉插求系数怎么求?
首先构造出 \(G(x) = \prod\limits_{i = 1}^n (x - x_i)\),直接暴力求解这部分的多项式系数是 \(O(n^2)\) 的。然后对于每个位置上变成了多项式除以一个 \(x-c\)。类似于多项式求逆等手法,这部分可以直接推导:对于 \(\dfrac{F(x)}{x - c}\) 设为 \(G(x)\),那么:
\(G(x)(x - c) = F(x)\) 即 \(\sum\limits_{i = 0}^{n - 1}g_ix^i(x - c) = -cg_0 + \sum\limits_{i = 1}^n (g_{i - 1} - cg_i)x^i = \sum\limits_{i = 0}^n f_ix^i\),即 \(g_0 = -\dfrac{f_0}{c}, g_i = \dfrac{g_{i - 1} - f_i}{c}\)。然后就可以直接带进去了。
1.2 值域连续
如果点值值域连续可以预处理一些阶乘做到 \(O(n)\) 插值。
2. 应用
2.1 优化 dp
P8290
我被卡常了。
首先考虑暴力,最大值与最小值的差很难做,考虑钦定范围在 \([L, R]\) 内其中 \(R - L = k\)。这样会算重,常见的套路是钦定端点被选择,比如这里我们再减去 \([L+1, R]\) 的情况即可。那么每个点能选择的就是 \([l_i, r_i]\cap [L, R]\) 这个区间。第一问直接 dp,第二问要做一个换根,对于一个区间可以 \(O(n)\) 求。最终时间复杂度是 \(O(nV)\) 的。现在考虑优化。首先如果我们将 \(l_i - 1, l_i, r_i, r_i+1\) 这些可能的端点离散化,那么两个端点之间的情况显然是相同的,要么然完全包含,要么然是与 \(l_i, r_i\) 形成一次函数,总共乘起来是 \((n+1)\) 次的函数,最后再做前缀和就是 \((n+2)\) 次函数,要枚举前 \((n+3)\) 项算前缀和。拉差即可,时间复杂度 \(O(n^3)\)。
安徽省集 2025 D1T3
这个题目显然是一个奶龙的背包 dp 模型,所以我们来简单分析一下。\(f[i, j]\) 为横坐标为 \(i\),纵坐标为 \(j\) 的权值综合,那么显然有 \(f[i, j] = f[i, j + 1] +\sum\limits_{z = 1}^i j^{2z - 1}f[i - z, j - z]\)
所以,这些计数类背包有没有更好的刻画形式?比如这道题根据后面我就知道这是一个多项式可以拉插了,这就很牛,有没有更棒的本质形式呢?
更详细的分析我暂时不想做,因为我太菜了,不能咕咕咕。
然后考虑部分分的启发:启发我们简单的把坐标系给移动一下,毕竟理论来说小鸟是可以无限坠落的,但是这显然又飞不到 \([L, R]\) 内。
-
对于函数 \(f[i, j]\),求 \(j\) 范围
\(j \in [l - i, h + i]\)。于是 \(h - l\) 很小的时候我们还是可以直接暴力 dp 的,把原点平移一下即可。 -
通过瞪眼法我们发现,\(f[i, j]\) 是一个关于 \(j\) 的多项式。
这就很牛,但是我还没有详细的学习它的分析,这就很唐。于是可以怎么办呢?通过瞪眼法我们发现其实次数不会超过 \(2n - 1\),因此我们可以仅仅求出 \(j\) 在 \(2n - 1\) 这个范围内的一些东西就行了……
这样说还是很笼统,我一开始写就挂在这里,因为对于 \(i\) 定义域是 \([l - i, h + i]\),但是我没有考虑好这件事情,所以就 gg 了。\(j\) 的范围只要是 \(2i - 1\) 的,所以我们可以直接暴力 dp 求出 \([h - i, h +i]\) 内的值即可。这样就保证在定义域里面,插值就没问题了。
说得好混乱啊 QAQ
2.2 快速求解数学式
这部分似乎比较笼统?
下面三道例题分别对应着:看上去就是多项式的东西;感受上是多项式,具体证明不容易的东西;乍一看不是多项式,但是分析一下立刻变成多项式的东西
P3270
当时应该是省选之前和 yzy 一起看的这题,我俩当时怎么都不会啊,暴力都不会。
为了方便下问认为 \(n \gets n - 1, R_i = n - R_i + 1\)。
注意到直接约束“恰好 \(K\) 个人全部科目小于等于 B 的”很难做,所以考虑容斥,钦定有 \(x\) 个人被吊打了就很容易了。这是一个明显的二项式反演的形式,\(g(x)\) 为钦定 \(x\) 个人被碾压了,那么就有:
瓶颈在于快速计算 \(\sum\limits_{t=1}^{U_i}t^{R_i}(U_i - t)^{n - R_i}\),拉插即可。
CF622F
求解 \(\sum\limits_{i = 1}^n i^d\),\(d\) 为常数。
比较容易直观感受出来这是一个关于 \(n\) 的 \((d+1)\) 次多项式,我不会严格证明。然后我们带入 \((d+1)\) 个值域连续的点值算就行了,瓶颈在求 \(n^d\),是 \(O(d\log d)\) 的。
AT_abc208_f
这个形式很像网格行走,我们可以看成:在 \((0, x)\) 位置拥有 \(x^k\) 的权值,这个权值会被贡献点到达 \((n, m)\) 点的路径条数次数,也就是 \(\binom{n + m - 1 - x}{n - x}\)。
于是答案就是:
试着用第二类斯特林数肘了肘,但是没肘动 ,化出来一个形式非常猎奇的东西。然后我就不会了。
对于一个很神秘的式子可以考虑拉插的!\(x^k\) 是一个关于 \(x\) 的 \(k\) 次多项式,\((n + m - 1 - x)^{\underline{m - 1}}\) 是一个关于 \(x\) 的 \((m - 1)\) 次多项式,乘起来就是 \((k + m - 1)\) 次了,加上前缀和就是 \(O(k+m)\) 次。
容易 \(O(km)\) 求出此多项式在 \(1\sim (k+ m+1)\) 上的值,对着 \(n\) 插值即可。
特别的,因为这道题 \(m\) 可能为 \(0\),所以要特判!
反思:对于 \(\binom{n}{m}\),可以看作是 \(\dfrac{n^{\underline {m}}}{m!}\),于是立即得到这是一个关于 \(n\) 的 \(m\) 次多项式。不过也许乍一看难以发现这一点,所以还是要主义的。
2.3 维护多项式乘法
对于一类问题(比如利用生成函数刻画),需要利用到多项式乘法,但是又因为进行次数过多(比如做矩阵行列式),所以不能暴力做。对于生成函数,有意义的只是系数,所以考虑带入点值,最后再回代。
【省选模拟测试】生成树
给定一个大小为 \(n\) 的树,每个边有颜色:1,2,3。问有多少种生成树颜色 2 不超过 \(a\) 且颜色 3 不超过 \(b\)。\(n\le 40\)。
容易想到做一个二元的 gf 来维护每种生成树树边颜色。
直接做是维护多项式 \(G(x, y) = \sum\limits_{i = 0}^{n -1} \sum\limits_{j = 0}^{n - 1} a_{i, j}x^iy^j\)。多项式有 \(O(n^2)\) 项,直接进行操作是 \(O(n^4)\) 的,加上行列式就是 \(O(n^7)\) 的。没办法通过。
注意到直接进行多项式操作复杂度无法避免的升高,对于二元多项式也没有什么好的处理手法。但是可以通过将系数表示变成点值表示来加快,最后用拉插插回去。具体而言将 \(x, y\in[1, n]\) 这 \(O(n^2)\) 组带入,然后就算。这样就得到了 \(O(n^2)\) 组 \(G(x, y)\) 的取值。当 \(y\) 固定时,\(G(x) = \sum\limits_{j = 0}^{n - 1}y^j \sum\limits_{i = 0}^{n - 1}a_{i, j}x^i\),拉插求出系数 \(h_{j, x} =\sum\limits_{i = 0}^{n} a_{i, j}x^i\)。通过再一次拉插对于一个 \(j\) 求出一竖列 \(a_{i, j}\)。
时间复杂度 \(O(n^5)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 40, M = 1e5, Mod = 1e9 + 7;
void upd(int &x, int y) {
x = ((x + y >= Mod) ? (x + y - Mod) : (x + y));
}
int qpow(int n, int m) {
int res = 1;
while(m) {
if(m & 1) res = 1ll * res * n % Mod;
n = 1ll * n * n % Mod;
m >>= 1;
}
return res;
}
int n, m, qx, qy;
int X[M + 3], Y[M + 3], Z[M + 3];
int mat[N + 3][N + 3], g[N + 3][N + 3];
int sol() {
int res = 1;
for(int i = 1; i < n; i++) {
for(int j = i + 1; j < n; j++) {
while(mat[i][i]) {
int div = mat[j][i] / mat[i][i];
for(int k = i; k < n; k++)
mat[j][k] = (mat[j][k] - 1ll * div * mat[i][k] % Mod + Mod) % Mod;
swap(mat[j], mat[i]), res = Mod - res;
}
swap(mat[j], mat[i]), res = Mod - res;
}
}
for(int i = 1; i < n; i++)
res = 1ll * res * mat[i][i] % Mod;
return res;
}
int work(int x, int y) {
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
mat[i][j] = 0;
for(int i = 1; i <= m; i++) {
int tmp;
if(Z[i] == 1) tmp = 1;
else if(Z[i] == 2) tmp = x;
else tmp = y;
upd(mat[X[i]][X[i]], tmp);
upd(mat[Y[i]][Y[i]], tmp);
upd(mat[X[i]][Y[i]], Mod - tmp);
upd(mat[Y[i]][X[i]], Mod - tmp);
}
return sol();
}
int tmpg[N + 3][N + 3], tmph[N + 3];
void largv(int n, int *x, int *y, int *c) {
for(int i = 0; i <= n; i++)
for(int j = 0; j <= n; j++)
tmpg[i][j] = 0;
tmpg[0][0] = 1;
for(int i = 1; i <= n; i++) {
for(int j = 0; j <= n; j++) {
upd(tmpg[i][j], 1ll * tmpg[i - 1][j] * (Mod - x[i]) % Mod);
if(j > 0)
upd(tmpg[i][j], tmpg[i - 1][j - 1]);
}
}
for(int i = 0; i <= n; i++) tmph[i] = c[i] = 0;
for(int i = 1; i <= n; i++) {
int invn = qpow(x[i], Mod - 2), mul = 1;
for(int j = 1; j <= n; j++)
if(i != j) mul = 1ll * mul * (x[i] - x[j] + Mod) % Mod;
mul = qpow(mul, Mod - 2);
tmph[0] = 1ll * (Mod - tmpg[n][0]) * invn % Mod;
for(int j = 1; j < n; j++)
tmph[j] = 1ll * (tmph[j - 1] - tmpg[n][j] + Mod) % Mod * invn % Mod;
for(int j = 0; j < n; j++)
upd(c[j], 1ll * y[i] * tmph[j] % Mod * mul % Mod);
}
}
int calc(int x, int n, int *h) {
int res = 1, sum = 0;
for(int i = 0; i < n; i++)
upd(sum, 1ll * res * h[i] % Mod),
res = 1ll * res * x % Mod;
return sum;
}
int h[N + 10][N + 10], xx[N + 10], xy[N + 10];
int aa[N + 10][N + 10], a[N + 10][N + 10], hh[N + 10][N + 10];
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> qx >> qy;
for(int i = 1; i <= m; i++)
cin >> X[i] >> Y[i] >> Z[i];
for(int x = 1; x <= n; x++) {
for(int y = 1; y <= n; y++)
g[x][y] = work(x, y);
}
for(int x = 1; x <= n; x++) {
for(int y = 1; y <= n; y++)
xx[y] = y, xy[y] = g[x][y];
largv(n, xx, xy, h[x]);
}
for(int j = 0; j < n; j++) {
for(int x = 1; x <= n; x++)
xx[x] = x, hh[j][x] = h[x][j];
largv(n, xx, hh[j], aa[j]);
}
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
a[i][j] = aa[j][i];
int sum = 0;
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
if(i <= qx && j <= qy)
upd(sum, a[i][j]);
}
}
cout << sum << '\n';
}
Gym102978A
给一个 \(n\times m\) 的网格填数,要求:
- \(\forall i \in [1, n], j \in [1, m], a_{i, j} \in [1, k]\)
- \(\forall i\in [1, n], j\in [1, m), a_{i, j} \le a_{i, j + 1}\)
- \(\forall i\in [1, n), j\in [1, m], a_{i, j} \le a_{i + 1, j}\)
对于给定的 \(x, y\),有 \(a_{x, y} =v\)
这道题的重点其实不在于拉插的运用,而是在于非常厉害的建模,我不太清楚这个建模有什么很本质的东西,如果大神们有兴趣教教我的话麻烦评论一下,谢谢啦 qwq
我们考虑对于 \(\le x\) 的部分划分成 01,然后我们发现 \(0\) 部分总是对于划分出 \(x\) 的 0 部分被包含在 \((x+1)\) 里面。为了刻画这条线,我们将格子转化为格点,然后可以看成从 \((n, 0)\) 走到 \((0, m)\),每次一步可以向上或者向右走,要求这些路径只可以重合但是不能交叉。
考虑如何刻画这件事情,将这个路径向右向下平移 \(x\) 个单位,变成从 \((n + x, x)\) 走到 \((x, m + x)\),每次可以向上向右移动,最后要求这些路径互不相交。这是一个 LGV 引理解决的问题。这个建模是这道题的核心,我之前没有见过这样的建模,如果有大佬可以教我更本质的看法欢迎评论 qwq
现在我们考虑如何维护 \(a_{x, y} = v\) 的约束,则说明这个格点左上方 \((x - 1, y - 1)\) 这个位置上方,经过它的路径(允许穿过)有 \((v - 1)\) 条。我们采用一个 gf 刻画,设 \((a+bx)\) 作为 lgv 矩阵里面的元,\(x\) 的次数刻画有多少条路径穿过了这个点。这个 \(a, b\) 可以用 dp 计算。
因为直接用这个多项式计算行列式非常慢。最高次是 \(k\),所以带入 \(O(k)\) 个点值,然后插值求 \((v-1)\) 次系数即可。
CF1874E Jellyfish and Hack
类似的技巧我已经见过三次了,只自己做出来过一次,我是不是该跳了???
容易想到,划分出来两部分“离散化”掉,然后设 \(f[i, j]\) 为 \(i\) 个数的排列最终和为 \(j\),那么转移就是:
注意到这个东西是一个二维的卷积,而且模数非常坏不能 ntt,其实 ntt 了也跑不过去。
这里面两部分卷积如果设成二元的生成函数那么设计 \(O(n^3)\) 次,最后肯定不行,而且只涉及最后一行的 \(f[n]\),所以对于每一行设一个 gf。
设 \(F_i(x) = \sum\limits_{j = 0}f[i, j]x^j = \sum\limits_{k = 1}^i x^iF_k(x)F_{i - k}(x)\)。于是发现这是一个 \(O(n^2)\) 次函数,取 \(O(n^2)\) 个最后一行的点值,最后拉插取系数即可。

浙公网安备 33010602011771号