给你 n+1 个点,求穿过它们的 n 次多项式。于是你想到了待定系数高斯消元,不过这 O(n3) 的太烂了。于是我们引入拉格朗日插值法,通过构造法整出多项式。
拉格朗日插值法
记这 n+1 个点为
(x0,y0),(x1,y1),(x2,y2),…,(xn,yn)
定义拉格朗日基本多项式 ℓi(x)
ℓj(x)=i=j∏xj−xix−xi
很容易发现一个性质:
∀i=j:ℓj(xi)=0
ℓj(xj)=1
于是我们得到拉格朗日插值多项式为:
L(x)=j=0∑nyjℓj(x)=j=0∑nyji=j∏xj−xix−xi
这显然满足 L(xi)=yi 。
如果要求出每一项系数,需要多项式乘法,复杂度 O(n2logn) ,若只是求 L(k) 复杂度 O(n2) 。若希望多点求值,见 重心拉格朗日插值法 。
实现
【模板】拉格朗日插值
求 L(k)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include <bits/stdc++.h> using namespace std; typedef long long ll; const int N = 2005; const int mod = 998244353; int n, k; ll x[N], y[N]; ll fpow(ll a, ll b, ll p) { ll r = 1; for ( ; b; b >>= 1, a = (a * a) % p) if (b & 1) r = (r * a) % p; return r; } ll lagrange(int n, ll k, ll x[], ll y[]) { ll r = 0; for (int j = 1; j <= n; ++j) { ll P1 = 1, P2 = 1; for (int i = 1; i <= n; ++i) if (i != j) { P1 = (P1 * (k - x[i] + mod)) % mod; P2 = (P2 * (x[j] - x[i] + mod)) % mod; } r = (r + y[j] * P1 % mod * fpow(P2, mod - 2, mod) % mod) % mod; } return r; } int main() { scanf("%d%d", &n, &k); for (int i = 1; i <= n; ++i) scanf("%lld%lld", &x[i], &y[i]); printf("%lld\n", lagrange(n, k, x, y)); return 0; }
|
当 x 的值连续
当 xi 的取值连续时,可以把复杂度优化到 O(n) 。令 xi=i ,那么:
L(k)=j=0∑nyii=j∏j−ik−i
考虑快速求 ∏i=jj−ik−i 。
对于分子预处理前缀积、后缀积:
pi=j=0∏i−1k−jsi=j=i+1∏nk−j
分母就是阶乘,同样预处理,但需要注意分母的符号, n−i 为奇数时分母取负。
L(k)=j=0∑n(−1)n−jyjj!(n−j)!pjsj
预处理阶乘逆元,时间复杂度的瓶颈就不会在求逆元上。
实现
CF622F,求 ∑i=1nik(n≤109,k≤106) ,我们熟知 ∑i=1nik=f(n) 其中 f(n) 是一个 k+1 次的多项式,那么我们只要暴力算出 n=0,1,2,3,…,k+1 的值,再用拉格朗日插值算 f(n) 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <bits/stdc++.h> using namespace std; typedef long long ll; const int N = 1000010; const int mod = 1e9 + 7; int n, k; ll fpow(ll a, ll b, ll p = mod) { ll r = 1; for ( ; b; b >>= 1, a = (a * a) % p) if (b & 1) r = (r * a) % p; return r; } ll a[N], s[N], p[N], fac[N], inv[N], ifac[N]; void init(int n) { fac[0] = fac[1] = inv[0] = inv[1] = ifac[0] = ifac[1] = 1; for (int i = 2; i <= n; ++i) fac[i] = fac[i - 1] * i % mod; for (int i = 2; i <= n; ++i) inv[i] = (mod - mod / i) * inv[mod % i] % mod; for (int i = 2; i <= n; ++i) ifac[i] = ifac[i - 1] * inv[i] % mod; } ll lagrange(int n, ll k, ll y[]) { init(n); p[0] = 1; for (int i = 0; i < n; ++i) p[i + 1] = p[i] * (k - i + mod) % mod; s[n] = 1; for (int i = n; i >= 1; --i) s[i - 1] = s[i] * (k - i + mod) % mod; ll r = 0; for (int j = 0; j <= n; ++j) { ll tmp = p[j] * s[j] % mod * ifac[j] % mod * ifac[n - j] % mod; r = (r + (ll)(((n - j) & 1) ? (mod - 1) : 1) * y[j] % mod * tmp % mod) % mod; } return r; } int main() { cin >> n >> k; for (int i = 1; i <= k + 1; ++i) a[i] = (a[i - 1] + fpow(i, k)) % mod; cout << lagrange(k + 1, n, a) << endl; return 0; }
|
重心拉格朗日插值法
若每次多加一个点重新求插值,每次重求 O(n2) 的复杂度太烂了。于是我们改进一下:
令:
ℓ(x)=i=0∏n(x−xi)
那么拉格朗日基本多项式重新写为:
ℓj(x)=x−xjℓ(x)∏i=j(xj−xi)1
定义重心权:
wj=∏i=j(xj−xi)1
则:
L(x)=ℓ(x)j=0∑nx−xjwjyj
一开始算出所有 wj 复杂度 O(n2) ,之后插一个点只要修改 wj 即可,复杂度 O(n) 。计算一个 L(k) 是 O(n) (注意瓶颈在求逆,对 n 个数求逆可以做到 O(n+logp) 见:数论基础 - 线性求任意-n-个数逆元 。