【算法竞赛 数学】拉格朗日插值法

【算法竞赛 - 数学】拉格朗日插值法 · purinliang/MyCareer Wiki

模板

版本1: \(x\) 取从 \(1\) 开始的连续正整数,并对需要的数字进行预处理缓存。

根据 \(x[] = 1, 2, 3, \cdots, k\) 时的值 \(y[]\)
插值计算不超过 \(k-1\) 次的多项式 \(L\)\(x_0\) 处的取值 \(L(x_0)\)

时间复杂度 \(O(k)\) ,空间复杂度 \(O(MAXK)\)

\(P1[i]\)\(x - i\) 的前缀积,
\(P2[i]\)\(x - i\) 的后缀积,
\(Q[i]\) 是阶乘 \(i!\) 的逆元。

注意:
1. \(k\) 为点的数量, \(MAXK\) 是否足够大?
2. \(y[]\) 是否传入负数?
3. \(int\) 是否可能溢出?
4. \(MOD\) 必须是大于其他所有数字的质数,否则逆元会不存在。

卡常:
1. \(q\) 只与 \(k\) 有关,对于固定的 \(k\) ,可以预处理后多次使用,但是真没什么必要。

使用:
1. 直接调用 int lagrange (int x0, int *y, int k) 即可,其会自动初始化需要的数组,并且可以多次调用。

namespace Lagrange1 {

const int MAXK = 1e6 + 5;
static int P1[MAXK], P2[MAXK], Q[MAXK];

ll qpow (ll x, ll n) {
    ll res = 1;
    for (; n; n >>= 1) {
        if (n & 1) res = res * x % MOD;
        x = x * x % MOD;
    }
    return res;
}

void init_P (int x0, int k) {
    P1[0] = 1;
    for (int i = 1; i <= k; ++i)
        P1[i] = 1LL * P1[i - 1] * (x0 - i) % MOD;
    P2[k + 1] = 1;
    for (int i = k; i >= 1; --i)
        P2[i] = 1LL * P2[i + 1] * (x0 - i) % MOD;
}

void init_Q (int k) {
    if (Q[k] != 0) return;
    Q[k] = 1;
    for (int i = 1; i <= k; ++i)
        Q[k] = 1LL * Q[k] * i % MOD;
    Q[k] = qpow (Q[k], MOD - 2);
    for (int i = k; i >= 1; --i)
        Q[i - 1] = 1LL * Q[i] * i % MOD;
}

int lagrange (int x0, int *y, int k) {
    if (x0 <= k) return y[x0];
    init_P (x0, k);
    init_Q (k);
    int Lx0 = 0;
    for (int i = 1; i <= k; ++i) {
        ll p = 1LL * P1[i - 1] * P2[i + 1] % MOD;
        ll q = 1LL * Q[i - 1] * Q[k - i] % MOD;
        if ( (k - i) & 1) q = MOD - q;
        Lx0 = (Lx0 + p * q % MOD * y[i] % MOD) % MOD;
    }
    return Lx0;
}

};

using Lagrange1::lagrange;

版本2: \(x\) 取从 \(1\) 开始的连续正整数,但是不预处理。

时间复杂度 \(O(k\log{M})\) ,空间复杂度 \(O(1)\)

使用:
1. 直接调用 int lagrange (int x0, int *y, int k) 即可,并且可以多次调用。

namespace Lagrange2 {

ll qpow (ll x, ll n) {
    ll res = 1;
    for (; n; n >>= 1) {
        if (n & 1) res = res * x % MOD;
        x = x * x % MOD;
    }
    return res;
}

ll inv (ll x) {
    return qpow (x, MOD - 2);
}

int lagrange (int x0, int *y, int k) {
    if (x0 <= k)
        return y[x0];
    ll Lx0 = 0, P = 1, Q1 = 1, Q2 = 1;
    for (int i = 1; i <= k; ++i) {
        P = P * (x0 - i) % MOD;
        if (i < k) Q2 = Q2 * (MOD - i) % MOD;
    }
    for (int i = 1; i <= k; ++i) {
        ll p = P * inv (x0 - i) % MOD;
        ll q = inv (Q1 * Q2 % MOD);
        Lx0 = (Lx0 + p * q % MOD * y[i]) % MOD;
        Q1 = Q1 * i % MOD;
        Q2 = Q2 * inv (MOD - k + i) % MOD;
    }
    return Lx0;
}

};

using Lagrange2::lagrange;

版本3:对于给定的取值 \(x[]\) 进行求解。

时间复杂度 \(O(k^2)\) ,空间复杂度 \(O(1)\)

使用:
1. 直接调用 int lagrange (int x0, int *x, int *y, int k) 即可,并且可以多次调用。

namespace Lagrange3 {

ll qpow (ll x, ll n) {
    ll res = 1;
    for (; n; n >>= 1) {
        if (n & 1) res = res * x % MOD;
        x = x * x % MOD;
    }
    return res;
}

ll inv (ll x) {
    return qpow (x, MOD - 2);
}

int lagrange (int x0, int *x, int *y, int k) {
    if (x0 <= k)
        return y[x0];
    ll Lx0 = 0, P = 1;
    for (int i = 1; i <= k; ++i)
        P = P * (x0 - x[i] + MOD) % MOD;
    for (int i = 1; i <= k; ++i) {
        ll p = P * inv (x0 - x[i] + MOD) % MOD, q = 1;
        for (int j = 1; j <= k; ++j) {
            if (j == i) continue;
            q = q * (x[i] - x[j] + MOD) % MOD;
        }
        Lx0 = (Lx0 + p * inv (q) % MOD * y[i]) % MOD;
    }
    return Lx0;
}

};

using Lagrange3::lagrange;

原理

对某个多项式函数,已知有

\[l_i(x_0)=\prod\limits_{j=1}^{k} [i\neq j]\frac{(x_0-x_j)}{(x_i-x_j)} \]

\[L(x_0)=\sum\limits_{i=1}^{k} y_il_i(x_0) \]

在算法竞赛中,通常要求在模 \(MOD\) 的意义下进行,且 \(MOD\) 是某个较大的质数。

TODO


细节

拉格朗日插值法的 \(k\) 的取值很容易让人迷惑,本文中所有的公式、代码都用 \(k-1\) 表示多项式的最高次数,也就是不超过 \(k-1\) 次的多项式

  1. 对一个不超过 \(k-1\) 次的多项式插值,至少需要 \(k\) 个点。
  2. 一个不超过 \(k-2\) 次多项式的部分和,通常是一个不超过 \(k-1\) 次的多项式。

上面的式子中,给出某个 \(x\) ,然后 \(O(k^2+k\log M)\) 计算出 \(f(x)\) 的值,慢得离谱,慎用。


拓展

选取等差数列进行插值

例如:求 \(\sum\limits_{i=1}^ni^{k}\) 的值,众所周知,这是一个 \(k+1\) 次多项式,至少需要 \(k+2\) 个点。

这个值有很多种求法,有 \(O(n\log k)\) 的朴素解法(枚举+快速幂),有 \(O(k^2)\) 的系数递推法,使用上述的适用范围广泛的拉格朗日插值法,也可以做到 \(O(k^2)\) ,算法的瓶颈在于计算插值多项式的分母部分。

这里可以选取一些特殊点来加速计算插值多项式的分母部分。例如通过选取一个等差数列,那么这个分母的临项会变得非常有规律。简单起见可以取正整数。

观察分子部分,对每个 \(i\) 来说,分子部分乘上 \((x-x_i)\) 后,都变为公共的 \(\prod\limits_{j=0}^{k}(x-x_j)\) ,那么在使用的时候再把这个 \((x-x_i)\) 除掉就行,复杂度为 \(O(k)\) 预处理,\(O(\log M)\) 单次使用,复杂度为 \(O(k\log M)\)。算法的复杂度瓶颈在于计算分母部分。

观察分母部分,在选取 \(x_i=i\) 后,分母变为两个自然数的前缀积之积,符号是正负交替。更准确地说,第 \(i\) 项的值是: \([\prod\limits_{j=0}^{i-1}(i-j)][\prod\limits_{j=i+1}^{k}(i-j)]=(\prod\limits_{j=1}^{i}j)(\prod\limits_{j=1}^{k-i}j)(-1)^{k-i}\)

由于算法竞赛一般是在模 \(M\) 意义下进行,算上求解逆元时间复杂度应为 \(O(k\log{M})\)

注: \(\sum_{i=1}^ni^{k}\) 的暴力求解其实可以是 \(O(n)\) 的,是使用线性筛去筛出每个数的 \(k\) 次方,然后线性递推出前缀和,不过这实际上是在画蛇添足,因为快速幂的常数实际上很小,而且 \(k\) 本身不大。

卡常:对同一个多项式多次求值时,插值完成后可以保存其分母(毒瘤)。

重心拉格朗日插值法

观察上面这个式子:

\[l_i(x_0)=\prod\limits_{j=1}^{k} [i\neq j]\frac{(x_0-x_j)}{(x_i-x_j)} \]

\[L(x_0)=\sum\limits_{i=1}^{k} y_il_i(x_0) \]

若记 \(g=\prod\limits_{j=1}^{k} (x_0-x_j)\) ,代入上式得到:

\[ L(x_0)=\sum\limits_{i=1}^{k} y_i\prod\limits_{j=1}^{k} [i\neq j]\frac{(x_0-x_j)}{(x_i-x_j)} \\ = g\sum\limits_{i=1}^{k} \prod\limits_{j=1}^{k} [i\neq j]\frac{y_i}{(x_0-x_i)(x_i-x_j)} \]

若记 \(t_i=\prod\limits_{j=1}^{k} [i\neq j]\frac{y_i}{(x_i-x_j)}\) ,代入上式得到

\[L(x_0)= g\sum\limits_{i=1}^{k} \frac{t_i}{(x_0-x_i)} \]

那么增加一个点或者删除一个点,只需要重新计算 \(g\) ,和所有的新的 \(t\) 即可,对于已有的值只需要乘上或者除以变动的差值,对于新的值也是直接求解即可。

TODO:给出代码实现并通过版本3能通过的题目,因为其理论复杂度接近。


验证

https://www.luogu.com.cn/problem/P4781 (使用版本3)
https://codeforces.com/contest/622/problem/F (使用版本1或者版本2)

引申阅读

重心拉格朗日插值法、牛顿插值法、差分法、高斯消元法、龙格现象

https://www.luogu.com.cn/problem/solution/P4781

原文地址:https://www.cnblogs.com/purinliang/p/13670493.html