欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

拉格朗日插值法

程序员文章站 2022-05-18 07:56:07
...

拉格朗日插值法是一种多项式插值方法。简单来说,给定 nn 个点,它可以唯一地构造出经过这些点的 n1n-1 次函数。
如何实现呢?其实非常地暴力:记我们给定的点为 (ai,bi)(a_i,b_i),我们只需要构造出一些函数 fi(x)f_i(x) 使得 fi(x)f_i(x) 满足 f(ai)=bif(a_i)=b_if(aj)=0f(a_j)=0jij≠i 即可。
接下来介绍“开关函数” pi(x)p_i(x)pi(x)p_i(x) 满足当 x=aix=a_ipi(x)=1p_i(x)=1x=ajx=a_jjij\ne ipi(x)=0p_i(x)=0。这样 fi(x)=pi(x)bif_i(x)=p_i(x)b_i
如何实现上述功能呢?我们考虑一个简单的数学性质:i/i=0i/i=00/i=00/i=0i0i\ne0。这样我们只需要让 x=aix=a_i 时分子分母一定相等,xx 为其他的 aa 值时分母一定出现 00 即可。可以得到:pi=j=1,jinxajaiajp_i=\prod_{j=1,j\ne i}^{n}\frac{x-a_j}{a_i-a_j}。考虑这样的 pip_i 为什么拥有上述性质:当 x=aix=a_i 时,无论 jj 为何值,分子分母一定相等,即 pi=1p_i=1;当 x=amx=a_mmim\ne i 时,由于 jij\ne i,故必然存在 j=mj=m,此时分子为 00pi=0p_i=0
综上所述,我们可以给出一般地过点集 {(ai,bi)}\{(a_i,b_i)\} 的多项式插值公式:
F(n)=i=1nfi(n)=i=1nbij=1,jinnajaiaj \begin{array}{lll} F(n)&=&\sum_{i=1}^nf_i(n)\\ &=&\sum_{i=1}^nb_i\prod_{j=1,j\ne i}^n\frac{n-a_j}{a_i-a_j} \end{array}
注意其中 iijjnn 的区别。

例题

The sum of the K-th Powers

给出两个非负整数 nnkk,求
i=1nik\sum_{i=1}^ni^k
答案对 109+710^9+7 取模。
n109n\le10^9k106k\le10^6

题解

暴力计算显然实现复杂度无法承受。
F(n)=i=1nikF(n)=\sum_{i=1}^ni^k
首先分析当 kk 较小的情况。k=1k=1 时,F(n)=n(n+1)/2F(n)=n(n+1)/2k=2k=2 时,F(n)=n(n+1)(2n+1)/6F(n)=n(n+1)(2n+1)/6;依此类推,可以发现 F(n)F(n) 是关于 nnk+1k+1 次多项式。
既然如此,我们可以通过先暴力算出一部分多项式上的点,然后通过插值得出多项式关于 nn 的函数形式,再直接通过函数求值。
我们知道,确定一个 nn 次函数需要 n+1n+1 个点,故我们首先暴力算出 F(1),F(2),...,F(k+2)F(1),F(2),...,F(k+2)。然后,我们将这些点代入拉格朗日插值公式得
F(n)=i=1k+2F(i)j=1,jik+2njij F(n)=\sum_{i=1}^{k+2}F(i)\prod_{j=1,j\ne i}^{k+2}\frac{n-j}{i-j}
然后就到了愉快的拆式子时间了。
F(n)=i=1k+2F(i)(j=1k+2(nj))/(ni)(i1)!(k+2i)!(1)k+2iF(n)=\sum_{i=1}^{k+2}F(i)\frac{(\prod_{j=1}^{k+2}(n-j))/(n-i)}{(i-1)!(k+2-i)!(-1)^{k+2-i}}
这样只需要通过预处理得出分母中阶乘的逆元,并通过费马小定理求出 i1i-1 的逆元即可在 O(klogk)O(k\log k) 的时间内得出答案。

代码

#include <iostream>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const unsigned MOD = 1E9 + 7;
const unsigned MAXN = 1E6;
const unsigned N = MAXN + 10;
unsigned n, k;
unsigned inv[N];
unsigned f[N];
void init() {
    inv[0] = inv[1] = 1;
    for (int i = 2; i <= k + 2; i++) {
        inv[i] = (-ll(MOD / i) * inv[MOD % i] % MOD + MOD) % MOD;
    }
    for (int i = 2; i <= k + 2; i++) {
        inv[i] = ull(inv[i - 1]) * inv[i] % MOD;
    }
}
unsigned pow(unsigned a, unsigned b) {
    unsigned res = 1;
    while (b) {
        if (b & 1) {
            res = ull(res) * a % MOD;
        }
        a = ull(a) * a % MOD;
        b >>= 1;
    }
    return res;
}
main() {
    ios::sync_with_stdio(false);
    cin >> n >> k;
    init();
    for (unsigned i = 1; i <= k + 2; i++) {
        f[i] = (f[i - 1] + pow(i, k)) % MOD;
    }
    if (n <= k + 2) {
        cout << f[n];
        return 0;
    }
    unsigned t = 1;
    for (unsigned i = 1; i <= k + 2; i++) {
        t = ull(t) * (n - i) % MOD;
    }
    unsigned ans = 0;
    for (unsigned i = 1; i <= k + 2; i++) {
        ans = (ans + ((ll(ull(f[i]) * t % MOD * pow(n - i, MOD - 2) % MOD * inv[i - 1] % MOD * inv[k + 2 - i] % MOD) * (((k + 2 - i) & 1) ? -1 : 1)) + MOD) % MOD) % MOD;
    }
    cout << ans;
}
相关标签: 数学 插值