
拉格朗日插值学习笔记
拉格朗日插值是众多插值算法中的一种,插值是通过一些点来求出过这些点的多项式函数的过程。
Table of Contents
算法思想
构造函数
给出 个点 ,要求出过这些点的 次多项式函数(也称该函数的度为 ),先考虑对每个点构造一个函数,第 个点的函数为 ,满足:
最后将这些函数组合为最终的函数:
接下来就是找到一种合适的形式来表示 的分段规定,可以让 含有一些因式,满足 时,其值为 , 时,其值为 。显然下面这种满足条件:
所以最终的 的解析式为:
求单个函数值的时间复杂度是 。
前后缀积优化
在一些题目中,插值所需的 是连续的,当 时,我们可以把公式写成这样:
特殊地,,。
这样单次插值的时间复杂度就是 。 当然,前后缀积优化还适用于 的情况,即 是一个公差不为 的等差数列。
重心拉格朗日插值
如果题面要求动态地加入插值点升高次数,上述方法不够优秀,从公式中可以发现,我们重复计算了许多东西,考虑把它们提取出来:
定义以下函数:
其中 也被称为重心权。
最终公式为:
显然只要 更新 , 求出 以及更新 。最后 求出新的 。 为求逆元的复杂度,一般为 , 为模数。
具体地,对于 , 都除以 ,而 只需扫一遍即可。
代码实现
普通拉格朗日插值
int interpolation(int deg, int k, int x[], int y[]) {
int res = 0;
for (int i = 1; i <= deg + 1; ++i) {
int num = y[i], den = 1;
for (int j = 1; j <= deg + 1; ++j) {
if (i == j)
continue;
num = 1ll * num * (k - x[j] + MOD) % MOD;
den = 1ll * den * (x[i] - x[j] + MOD) % MOD;
}
res = (res + 1ll * num * power(den, MOD - 2)) % MOD;
}
return res;
}
前后缀积优化
inline int sgnInv(int n) {
static int inv[2] = {power(1, MOD - 2), power(MOD - 1, MOD - 2)};
return inv[n % 2];
}
void preprocess(int deg) {
fac[0] = 1;
for (int i = 1; i <= deg + 1; ++i)
fac[i] = 1ll * fac[i - 1] * i % MOD;
inv[deg + 1] = power(fac[deg + 1], MOD - 2);
for (int i = deg; i >= 0; --i)
inv[i] = 1ll * inv[i + 1] * (i + 1) % MOD;
}
int interpolation(int deg, int k, int y[]) {
int res = 0;
pre[0] = suf[deg + 2] = 1;
for (int i = 1; i <= deg + 1; ++i)
pre[i] = 1ll * pre[i - 1] * (k - i + MOD) % MOD;
for (int i = deg + 1; i >= 1; --i)
suf[i] = 1ll * suf[i + 1] * (k - i + MOD) % MOD;
for (int i = 1; i <= deg + 1; ++i) {
int num = 1ll * y[i] * pre[i - 1] % MOD * suf[i + 1] % MOD;
int den = 1ll * inv[i - 1] * inv[deg - i + 1] % MOD * sgnInv(deg - i + 1) % MOD;
res = (res + 1ll * num * den) % MOD;
}
return res;
}
重心拉格朗日插值
int interpolationAdd(int nx, int ny) {
++deg;
x[deg + 1] = nx;
y[deg + 1] = ny;
c = 1ll * c * (k - nx + MOD) % MOD;
for (int i = 1; i <= deg; ++i)
w[i] = 1ll * w[i] * power(x[i] - x[deg + 1] + MOD, MOD - 2) % MOD;
for (int i = 1; i <= deg; ++i)
w[deg + 1] = 1ll * w[deg + 1] * (x[deg + 1] - x[i] + MOD) % MOD;
w[deg + 1] = power(w[deg + 1], MOD - 2);
int res = 0;
for (int i = 1; i <= deg + 1; ++i)
res = 1ll * y[i] * w[i] % MOD * power(k - x[i]) % MOD;
res = 1ll * res * c % MOD;
return res;
}
例题
Luogu P4593 教科书般的亵渎
给出一个单调递增的长度为 的数列,每次操作可以使数列的值全部减少 ,满足 恰好是操作开始前数列开头的值域连续段全部被减为小于等于 ,操作结束后,将该连续段删除,并获得 的分数。题面给出数列中没有出现的数,共 个。
,。
把原题面转化为上述模型后,我们需要求 。
这里给出一个结论:一个 次函数的前缀和为 次。
所以考虑先求出 的 ,插值得到 次的 。
至于题目中的减操作, 模拟每一段即可。若使用前后缀积优化,则时间复杂度为 。
#include <iostream>
#include <algorithm>
#include <utility>
#include <numeric>
#include <vector>
using namespace std;
using ll = long long;
constexpr int maxm = 50 + 10;
constexpr int mod = 1e9 + 7;
int t, m;
ll n, a[maxm];
int x[maxm], y[maxm];
int fac[maxm], inv[maxm], pre[maxm], suf[maxm];
int power(int x, int y) {
int res = 1;
for (; y; y /= 2) {
if (y % 2)
res = 1ll * res * x % mod;
x = 1ll * x * x % mod;
}
return res;
}
inline int sgnInv(int n) {
static int inv[2] = {power(1, mod - 2), power(mod - 1, mod - 2)};
return inv[n % 2];
}
void preprocess() {
fac[0] = 1;
for (int i = 1; i <= maxm - 1; ++i)
fac[i] = 1ll * fac[i - 1] * i % mod;
inv[maxm - 1] = power(fac[maxm - 1], mod - 2);
for (int i = maxm - 2; i >= 0; --i)
inv[i] = 1ll * inv[i + 1] * (i + 1) % mod;
}
int interpolation(ll x0, int deg, int x[], int y[]) {
int ans = 0;
pre[0] = suf[deg + 2] = 1;
for (int i = 1; i <= deg + 1; ++i)
pre[i] = 1ll * pre[i - 1] * (x0 - i + mod) % mod;
for (int i = deg + 1; i >= 1; --i)
suf[i] = 1ll * suf[i + 1] * (x0 - i + mod) % mod;
for (int i = 1; i <= deg + 1; ++i) {
int num = 1ll * y[i] * pre[i - 1] % mod * suf[i + 1] % mod;
int den = 1ll * inv[i - 1] * inv[deg - i + 1] % mod * sgnInv(deg - i + 1) % mod % mod;
ans = (1ll * ans + 1ll * num * den % mod) % mod;
}
return (ans + mod) % mod;
}
int calc(ll x0) {
if (x0 <= m + 3)
return y[x0];
return interpolation(x0, m + 2, x, y);
}
int solve() {
int ans = 0;
for (int i = 1; i <= m + 3; ++i) {
x[i] = i;
y[i] = (y[i - 1] + power(i, m + 1)) % mod;
}
for (int i = 1; i <= m; ++i) {
ans = (ans + calc(n)) % mod;
for (int j = i; j <= m; ++j)
ans = (ans - power(a[j], m + 1) + mod) % mod;
for (int j = i + 1; j <= m; ++j)
a[j] -= a[i];
n -= a[i];
}
if (n != 0)
ans = (ans + calc(n)) % mod;
return ans;
}
int main() {
ios::sync_with_stdio(false);
preprocess();
cin >> t;
while (t--) {
cin >> n >> m;
for (int i = 1; i <= m; ++i)
cin >> a[i];
sort(a + 1, a + 1 + m);
cout << solve() << endl;
}
return 0;
}
Luogu P3270 成绩比较
G 系共有 位同学, 门必修课。这 位同学的编号为 到 的整数,其中 B 神的编号为 号。这 门必修课编号为 到 的整数。一位同学在必修课上可以获得的分数是 到 中的一个整数。
如果在每门课上 A 获得的成绩均小于等于 B 获得的成绩,则称 A 被 B 碾压。在 B 神的说法中,G 系共有 位同学被他碾压(不包括他自己),而其他 位同学则没有被他碾压。D 神查到了 B 神每门必修课的排名。
这里的排名是指:如果 B 神某门课的排名为 ,则表示有且仅有 位同学这门课的分数大于 B 神的分数,有且仅有 位同学这门课的分数小于等于 B 神(不包括他自己)。
请你求出所有可能的成绩种类数。
,,,。
考虑 DP。先只考虑成绩的相对关系,即先不管每个人的成绩具体是多少。设 表示前 门课程 B 神能够碾压 个人的方案数:
这个式子意思是前 门课中,B 神可以碾压 人,但现在其中的 人这门课成绩比 B 神高,无法被碾压,有 种方案,为了保证 B 神排名为 ,除了刚才的 人,还要有 人排在 B 神前面,这些人来自未被碾压的 人中有 种方案。
由于每门课的排名固定互相独立,所以设 为第 门课的人的排名固定时成绩方案数,则有:
这里的算法是枚举 B 神的分数,然后统计每个人可能的分数的方案数。可以发现 是一个 次多项式,则 是一个 次多项式,插值解决。
最后定义 为考虑成绩的具体取值的方案数:
最后答案为 。时间复杂度 。
#include <iostream>
using namespace std;
constexpr int MAXN = 100 + 10;
constexpr int MOD = 1e9 + 7;
int n, m, k;
int u[MAXN], r[MAXN];
int c[MAXN][MAXN], d[MAXN], f[MAXN][MAXN];
int power(int x, int y) {
int res = 1;
for (; y; y /= 2) {
if (y % 2)
res = 1ll * res * x % MOD;
x = 1ll * x * x % MOD;
}
return res;
}
int interpolation(int deg, int k, int x[], int y[]) {
int res = 0;
for (int i = 1; i <= deg + 1; ++i) {
int num = y[i], den = 1;
for (int j = 1; j <= deg + 1; ++j) {
if (i == j)
continue;
num = 1ll * num * (k - x[j] + MOD) % MOD;
den = 1ll * den * (x[i] - x[j] + MOD) % MOD;
}
res = (res + 1ll * num * power(den, MOD - 2)) % MOD;
}
return res;
}
int calc(int i) {
int x[MAXN] = { 0 }, y[MAXN] = { 0 };
for (int j = 1; j <= min(u[i], n + 1); ++j) {
x[j] = j;
y[j] = (y[j - 1] + 1ll * power(j, n - r[i]) * power(u[i] - j, r[i] - 1)) % MOD;
}
if (u[i] <= n + 1)
return y[u[i]];
return interpolation(n, u[i], x, y);
}
int main() {
ios::sync_with_stdio(false);
cin >> n >> m >> k;
for (int i = 1; i <= m; ++i)
cin >> u[i];
for (int i = 1; i <= m; ++i)
cin >> r[i];
for (int i = 0; i <= n; ++i)
c[i][0] = c[i][i] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 1; j < i; ++j)
c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % MOD;
for (int i = 1; i <= m; ++i)
d[i] = calc(i);
f[0][n - 1] = 1;
for (int i = 1; i <= m; ++i) {
for (int j = 1; j < n; ++j) {
for (int k = j; k < n; ++k) {
if (k - j > r[i] - 1)
continue;
int cnt = 1ll * c[k][j] * c[n - k - 1][r[i] - 1 - (k - j)] % MOD;
int pre = 1ll * d[i] * f[i - 1][k] % MOD * cnt % MOD;
f[i][j] = (f[i][j] + pre) % MOD;
}
}
}
cout << f[m][k] << endl;
return 0;
}
Luogu P4463 calc
定义一个长度为 的正整数数列 的权值为 ,现在要求你求出所有满足以下条件的数列的权值并对质数 取模:
- ,。
- ,。
,,,。
设 为不考虑数的顺序情况下,在数列的前 个数都属于 的权值和,考虑选或不选 这个数,则有:
最后答案为 。可以想到需要插值,现在重点是要知道 的次数。
设 为 的次数.
- 首先可以知道 。
- 对于 ,仅考虑 ,则 ,而考虑 ,相当于做前缀和,则 。
- 对于 ,仅考虑 ,则 ,而考虑 ,相当于做前缀和,则 。
- etc.
最后得到 ,则需要处理出 个 。 注意这还没考虑数的顺序,对数列进行全排列,答案为 。
时间复杂度 。
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 500 + 10;
int n, k, p, f[MAXN][MAXN * 2];
int x[MAXN * 2], y[MAXN * 2];
void DP() {
for (int i = 0; i <= 2 * n; ++i)
f[0][i] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= 2 * n; ++j)
f[i][j] = (f[i][j - 1] + 1ll * j * f[i - 1][j - 1]) % p;
}
int power(int x, int y) {
int res = 1;
while (y) {
if (y % 2)
res = 1ll * res * x % p;
x = 1ll * x * x % p;
y /= 2;
}
return res % p;
}
int inv(int x) {
return power(x, p - 2);
}
int interpolation(int deg, int k, int x[], int y[]) {
int ans = 0;
for (int i = 1; i <= deg + 1; ++i) {
int num = y[i], den = 1;
for (int j = 1; j <= deg + 1; ++j) {
if (i == j)
continue;
num = 1ll * num * (k - x[j] + p) % p;
den = 1ll * den * (x[i] - x[j] + p) % p;
}
ans = (ans + 1ll num * inv(den) % p) % p;
}
return ans;
}
int main() {
cin >> k >> n >> p;
DP();
for (int i = 1; i <= 2 * n + 1; ++i) {
x[i] = i;
y[i] = f[n][i];
}
int ans = interpolation(2 * n, k % p, x, y);
for (int i = 1; i <= n; ++i)
ans = 1ll * ans * i % p;
cout << ans << endl;
return 0;
}