多项式插值算法 学习笔记

Published on

概述

多项式插值算法用于寻找一个恰好通过给定数据点的多项式。

问题形式

给定 $k + 1$ 个互异数据点 $\left(x_i, y_i\right)$,找到一个 $n$ 次多项式 $f\left(x\right)$,使得对于任意的 $0 \leq i \leq k$,有 $f\left(x_i\right) = y_i$。在某些情况下,我们只需求出 $f\left(a\right)$ 的值,而不需要求出 $f\left(x\right)$ 的每个系数。

拉格朗日(Lagrange)插值法

定义 拉格朗日基本多项式(Lagrange basis polynomial) 为:

$$\ell_j\left(x\right) = \prod\limits_{0 \leq m \leq k, m \neq j} \frac{x - x_m}{x_j - x_m}$$

如果使用 克罗内克(Kronecker) $\delta$ 函数 进行描述:$\ell_j\left(x_m\right) = \delta_{jm}$

拉格朗日插值公式是如下的线性组合:

$$L\left(x\right) = \sum\limits_{j = 0}^ky_j\ell_j\left(x\right)$$

由于每个 拉格朗日基本多项式 的次数都不超过 $k$,所以 $L\left(x\right)$ 的次数也不超过 $k$。
而对于每个 $m$:$L\left(x_m\right) = \sum\limits_{j = 0}^ky_j\delta_{mj} = y_m$

同时,插值多项式是唯一的:
假设有一个 $k$ 次多项式 $M\left(x\right)$ 是插值的答案。那么 $M\left(x\right) - L\left(x\right)$ 在 $k + 1$ 个不同点 $\left\{x_0, x_1, \ldots, x_k\right\}$ 上都是 $0$,但是当一个 $k$ 次多项式具有 $k + 1$ 个零点时,可以确定,该多项式的值恒等于 $0$。因此:$M\left(x\right) - L\left(x\right) = 0$ 或者说 $M\left(x\right) = L\left(x\right)$。

原理

这里给出我的理解方式。

假设现在有三个目标点

我们构造三个函数:

注意看,每个函数都恰好经过一个不同的目标点 和 两个零点,这些零点的 $x$ 坐标均与一个目标点相同。
例如这根红线,经过的 $F$ 点的 $x$ 坐标与目标点 $A$ 的横坐标都是 $4$;经过的 $D$ 点的 $x$ 坐标与目标点 $B$ 的横坐标都是 $7$;而又经过了目标点 $C$。

这样,当我们把这三个函数相加:

每个目标点就刚好被穿过。


此时再观察拉格朗日基本多项式:

$$\ell_j\left(x\right) = \prod\limits_{0 \leq m \leq k, m \neq j} \frac{x - x_m}{x_j - x_m}$$

只有 $\ell_j\left(x_j\right) = 1$,其它 $\ell_j\left(x_i\right)$ 的值都为 $0$。

而最后拉格朗日插值公式:

$$L\left(x\right) = \sum\limits_{j = 0}^ky_j\ell_j\left(x\right)$$

又 给它乘上了一个 $y_j$,这就是的该函数刚好经过一个目标点。

重心拉格朗日插值法(Barycentric form)

如果需要动态加入点,那么普通的拉格朗日插值就需要整个重新计算一遍。重心拉格朗日插值法只需要线性时间即可完成动态点加入。

我们再来看看拉格朗日插值公式

$$ \begin{aligned} L\left(x\right) &= \sum\limits_{j = 0}^ky_j\prod\limits_{0 \leq m \leq k, m \neq j}\frac{x - x_m}{x_j - x_m} \\ &= \left(\prod\limits_{j = 0}^k\left(x - x_j\right)\right)\sum\limits_{j = 0}^k\frac1{x - x_j} \cdot \left(\prod\limits_{0 \leq m \leq k, m \neq j}\frac{y_j}{x_j - x_m}\right) \end{aligned} $$

因此,每次加入一个新点,我们值需要计算 $\prod\limits_{0 \leq m \leq k, m \neq j}\frac{y_j}{x_j - x_m}$ 即可。

牛顿(Newton)插值法

牛顿插值法也只需要线性时间即可完成动态点加入。

定义 牛顿基本多项式(Newton basis polynomials) 为:

$$n_j\left(x\right) = \prod\limits_{i = 0}^{j - 1}\left(x - x_i\right)$$

牛顿插值公式是如下的线性组合:

$$N\left(x\right) = \sum\limits_{j = 0}^ka_jn_j\left(x\right)$$

其中,$a_n = \left[y_0, y_1, \ldots, y_n\right]$,而 $\left[y_0, y_1, \ldots, y_n\right]$ 表示 差商(Divided differences)

差商

$$ \begin{aligned} \left[y_0\right] &= y_0 \\ \left[y_0, y_1\right] &= \frac{\left[y_1\right] - \left[y_0\right]}{x_1 - x_0} \\ \left[y_0, y_1, y_2\right] &= \frac{\left[y_1, y_2\right] - \left[y_0, y_1\right]}{x_2 - x_0} \\ \left[y_0, y_1, y_2, y_3\right] &= \frac{\left[y_1, y_2, y_3\right] - \left[y_0, y_1, y_2\right]}{x_3 - x_0} \\ \left[y_0, \ldots, y_k\right] &= \frac{\left[y_1, \ldots, y_k\right] - \left[y_0, \ldots, y_{k - 1}\right]}{x_k - x_0} \end{aligned} $$

注意到 $k$ 阶差商就是 两个 $k - 1$ 阶差商 的差商。

原理

假设现在只有两个点 $\left(x_0, y_0\right)$ 和 $\left(x_1, y_1\right)$。求经过这两个点的函数 $g\left(x\right)$。

设 $g\left(x\right) = y_0 + c_1 \left(x - x_0\right)$,代入 $g\left(x_1\right) = y_1$ 即可解出 $c_1$。

我们让 $c_1$ 乘上 $\left(x - x_0\right)$ 的原因是保证 $g\left(x_0\right) = y_0$。


假设现在有三个点 $\left(x_0, y_0\right), \left(x_1, y_1\right)$ 和 $\left(x_2, y_2\right)$。求经过这两个点的函数 $h\left(x\right)$。

设 $h\left(x\right) = g\left(x\right) + c_2 \left(x - x_0\right)\left(x - x_1\right)$,代入 $h\left(x_2\right) = y_2$ 即可解出 $c_2$。


此时再观察牛顿多项式:

$$N\left(x\right) = \left[y_0\right] + \left[y_0, y_1\right]\left(x - x_0\right) + \cdots + \left[y_0, \ldots, y_k\right]\left(x - x_0\right)\left(x - x_1\right) \cdots \left(x - x_{k - 1}\right)$$

其实 $c_i$ 的解就是对应的差商。

代码

网上找了一圈没看见一个写牛顿插值动态插入的,只好自己写一个了。

代码大家应该都看得懂的。

dvdiff 表示差商,X 是 $x_i$,n 表示 $\prod\limits_{i = 0}^k\left(x - x_i\right)$,N 表示插出来的各项系数,从高到低的。

大家可以当个模板使用,注意使用前先 setmod 一下。当然有需要的话可以把这个改成实数域的,也很好改。

class Newton_Poly_Int_Builder {
  vector<vector<tp>> dvdiff; // dvdiff[a][b] -> [a, a + 1, ..., a + b];
  vector<tp> X, n, N;
  tp mod;
  
 public:
  void clear() {
    X.clear();
    dvdiff.clear();
    n.clear();
    N.clear();
  }
  
  void setmod(tp x) {
    mod = x;
    clear();
  }
  
  tp positive(tp x) {
    if (x < 0) return x + mod;
    else if (x >= mod) return x - mod;
    return x;
  }
  
  void insert(tp x, tp y) {
    dvdiff.push_back(vector<tp>(1, y));
    for (tp i = dvdiff.size() - 2; i >= 0; --i)
      dvdiff[i].push_back(positive(dvdiff[i + 1].back() - dvdiff[i].back()) * RCAL::inverse(positive(x - X[i]), mod) % mod);
    tp c = dvdiff.front().back();
    vector<tp> tmp = n;
    if (n.empty()) n = vector<tp>(1, 1);
    else {
      for (auto& i : tmp) i = positive(-i * X.back() % mod);
      n.push_back(0);
      for (tp i = 0; i < tmp.size(); ++i) n[i + 1] = (n[i + 1] + tmp[i]) % mod;
    }
    tmp = n;
    for (auto& i : tmp) i = positive(i * c % mod);
    N.swap(tmp);
    for (tp i = 0; i < tmp.size(); ++i) N[i + 1] = positive(N[i + 1] + tmp[i]);
    X.push_back(x);
  }
  
  tp query(tp x) {
    tp pw = 1, ret = 0;
    for (tp i = N.size() - 1; i >= 0; --i) {
      ret = (ret + N[i] * pw) % mod;
      pw = pw * x % mod;
    }
    return ret;
  }
} Newton_Poly_Int;

参考

https://en.wikipedia.org/wiki/Lagrange_polynomial
https://zh.wikipedia.org/wiki/%E6%8B%89%E6%A0%BC%E6%9C%97%E6%97%A5%E6%8F%92%E5%80%BC%E6%B3%95
https://en.wikipedia.org/wiki/Newton_polynomial
https://www.zhihu.com/question/22320408/answer/141973314
https://en.wikipedia.org/wiki/Divided_differences
https://www.zhihu.com/question/58333118


No comments

Post a comment