Chương 7·Bài 71

Lagrange Interpolation

Một đa thức bậc không quá n−1n - 1 được xác định bởi giá trị tại nn điểm phân biệt. Nội suy Lagrange tính ngay giá trị của nó tại điểm khác mà không cần tìm hệ số, O(n2)O(n^2) với điểm bất kỳ và O(n)O(n) khi các điểm là 0,1,…,n0, 1, \dots, n. Kỹ thuật mạnh nhất của bài này không nằm ở công thức mà ở chỗ nhận ra đáp án là đa thức theo một tham số lớn (tổng lũy thừa, số cách gán nhãn theo giới hạn DD), tính nó ở vài giá trị nhỏ bằng DP, rồi nội suy ra giá trị lớn.

Đa thức cơ sở

Với các nút phân biệt x1,…,xnx_1, \dots, x_n và giá trị yiy_i, đặt

Li(x)=∏j≠ix−xjxi−xj.L_i(x) = \prod_{j \ne i} \frac{x - x_j}{x_i - x_j}.

LiL_i có bậc n−1n - 1, bằng 1 tại xix_i và bằng 0 tại mọi nút khác, nên f(x)=∑iyiLi(x)f(x) = \sum_i y_i L_i(x) đi qua đủ nn điểm. Hai đa thức bậc nhỏ hơn nn trùng nhau tại nn điểm thì hiệu của chúng có nn nghiệm nên bằng 0: đa thức nội suy là duy nhất.

Theo mô-đun nguyên tố pp, mọi phép chia thành nhân nghịch đảo, chỉ cần các nút phân biệt theo mô-đun pp. Với mỗi ii, nhân tử số và mẫu số riêng rồi lấy một nghịch đảo, tổng O(n2+nlog⁡p)O(n^2 + n \log p).

Trên: đa thức đi qua các điểm đã cho. Dưới: đa thức cơ sở Lᵢ, bằng 1 tại nút i và 0 tại các nút khác. Đổi y hoặc i để xem.

Ví dụGiá Trị Qua Các Điểm

Đề. n≤2000n \le 2000 điểm (xi,yi)(x_i, y_i) với xix_i phân biệt, tính f(k)f(k) theo mô-đun 998244353998244353 với ff là đa thức bậc nhỏ hơn nn qua các điểm đó.

Lời giải. Áp thẳng công thức nội suy tại điểm kk:

f(k)=∑iyi⋅∏j≠i(k−xj)∏j≠i(xi−xj).f(k) = \sum_i y_i \cdot \frac{\prod_{j \ne i}(k - x_j)}{\prod_{j \ne i}(x_i - x_j)}.

Với mỗi ii, nhân tử số và mẫu số riêng trong một vòng O(n)O(n), rồi lấy một nghịch đảo của mẫu số.

num bắt đầu bằng y[i] để nhân thẳng vào tử số; den là tích các hiệu xi−xjx_i - x_j. Hiệu có thể âm, mà phép % của C++ giữ dấu, nên đưa về [0,p)[0, p) trước khi nhân. Các xix_i phân biệt nên mẫu số khác 0 và có nghịch đảo.

C++
for (int i = 0; i < n; i++) {
    ll num = y[i], den = 1;
    for (int j = 0; j < n; j++)
        if (j != i) {
            num = num * ((k - x[j]) % MOD + MOD) % MOD;
            den = den * ((x[i] - x[j]) % MOD + MOD) % MOD;
        }
    ans = (ans + num * power(den, MOD - 2)) % MOD;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll MOD = 998244353;
ll n, k, x[2005], y[2005];

ll power(ll a, ll b) {
    ll r = 1;
    a %= MOD;
    while (b) {
        if (b & 1) r = r * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return r;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> k;
    for (int i = 0; i < n; i++) cin >> x[i] >> y[i];
    ll ans = 0;
    for (int i = 0; i < n; i++) {
        ll num = y[i], den = 1;
        for (int j = 0; j < n; j++)
            if (j != i) {
                num = num * ((k - x[j]) % MOD + MOD) % MOD;
                den = den * ((x[i] - x[j]) % MOD + MOD) % MOD;
            }
        ans = (ans + num * power(den, MOD - 2)) % MOD;
    }
    cout << ans << '\n';
}
2 4
1 5
2 7

O(n2+nlog⁡p)O(n^2 + n \log p), khoảng 4⋅1064 \cdot 10^6 phép nhân với n=2000n = 2000.

Nút liên tiếp trong O(n)

Khi các nút là 0,1,…,n0, 1, \dots, n, mẫu số có dạng đóng: ∏j≠i(i−j)=i!⋅(n−i)!⋅(−1)n−i\prod_{j \ne i} (i - j) = i! \cdot (n - i)! \cdot (-1)^{n - i}. Tử số ∏j≠i(T−j)\prod_{j \ne i}(T - j) là tích tiền tố ∏j<i(T−j)\prod_{j < i}(T - j) nhân tích hậu tố ∏j>i(T−j)\prod_{j > i}(T - j). Tính sẵn giai thừa và nghịch đảo giai thừa, mỗi số hạng O(1)O(1), tổng O(n)O(n).

Nếu T≤nT \le n thì trả ngay P(T)P(T), vì khi đó tử số của mọi cơ sở trừ một đều bằng 0.

Hỏi nhanh

Đa thức bậc 2 có P(0)=1P(0) = 1, P(1)=4P(1) = 4, P(2)=9P(2) = 9. Nội suy cho P(3)P(3) bằng bao nhiêu?

Ví dụBảng Giá Trị Liên Tiếp

Đề. Đa thức bậc không quá N≤105N \le 10^5, biết P(0),…,P(N)P(0), \dots, P(N) theo mô-đun 109+710^9 + 7. Tính P(T)P(T) với T≤109T \le 10^9.

Lời giải. Nút là 0,1,…,N0, 1, \dots, N liên tiếp. Mẫu số của cơ sở ii là ∏j≠i(i−j)=i!⋅(N−i)!⋅(−1)N−i\prod_{j \ne i}(i - j) = i! \cdot (N - i)! \cdot (-1)^{N - i}: các j<ij < i cho tích i!i!, các j>ij > i cho (N−i)!(N - i)! với N−iN - i dấu trừ. Tử số ∏j≠i(T−j)\prod_{j \ne i}(T - j) là tích tiền tố ∏j<i\prod_{j < i} nhân tích hậu tố ∏j>i\prod_{j > i}. Tính sẵn hai mảng tích và bảng nghịch đảo giai thừa thì mỗi số hạng O(1)O(1).

Bước 1: trường hợp có sẵn. T≤NT \le N thì trả ngay a[t]. Trường hợp này cũng tránh việc tử số có một thừa số bằng 0.

Bước 2: giai thừa nghịch đảo. Một lần lũy thừa cho ifac[n], rồi lùi dần: 1(i−1)!=ii!\frac{1}{(i-1)!} = \frac{i}{i!}.

C++
fac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
ifac[n] = power(fac[n], MOD - 2);
for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;

Bước 3: tích tiền tố và hậu tố. pre[i] =∏j<i(T−j)= \prod_{j < i}(T - j), suf[i] =∏j≥i(T−j)= \prod_{j \ge i}(T - j); tử số của cơ sở ii là pre[i] * suf[i + 1].

C++
pre[0] = 1;
for (int i = 1; i <= n + 1; i++) pre[i] = pre[i - 1] * ((t - (i - 1)) % MOD) % MOD;
suf[n + 1] = 1;
for (int i = n; i >= 0; i--) suf[i] = suf[i + 1] * ((t - i) % MOD) % MOD;

Bước 4: cộng các số hạng. Dấu (−1)N−i(-1)^{N - i} xử lý bằng cách cộng hay trừ.

C++
for (int i = 0; i <= n; i++) {
    ll term = a[i] * pre[i] % MOD * suf[i + 1] % MOD * ifac[i] % MOD * ifac[n - i] % MOD;
    if ((n - i) % 2) ans = (ans - term + MOD) % MOD;
    else ans = (ans + term) % MOD;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll MOD = 1e9 + 7;
const int N = 1e5 + 5;
ll n, t, a[N], pre[N], suf[N], fac[N], ifac[N];

ll power(ll x, ll b) {
    ll r = 1;
    x %= MOD;
    while (b) {
        if (b & 1) r = r * x % MOD;
        x = x * x % MOD;
        b >>= 1;
    }
    return r;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (int i = 0; i <= n; i++) cin >> a[i];
    cin >> t;
    if (t <= n) {
        cout << a[t] << '\n';
        return 0;
    }
    fac[0] = 1;
    for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
    ifac[n] = power(fac[n], MOD - 2);
    for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;
    pre[0] = 1;
    for (int i = 1; i <= n + 1; i++) pre[i] = pre[i - 1] * ((t - (i - 1)) % MOD) % MOD;
    suf[n + 1] = 1;
    for (int i = n; i >= 0; i--) suf[i] = suf[i + 1] * ((t - i) % MOD) % MOD;
    ll ans = 0;
    for (int i = 0; i <= n; i++) {
        ll term = a[i] * pre[i] % MOD * suf[i + 1] % MOD * ifac[i] % MOD * ifac[n - i] % MOD;
        if ((n - i) % 2) ans = (ans - term + MOD) % MOD;
        else ans = (ans + term) % MOD;
    }
    cout << ans << '\n';
}
2
1 4 9
3

O(N+log⁡p)O(N + \log p).

Tổng lũy thừa là đa thức

S(n)=∑i=1nikS(n) = \sum_{i=1}^{n} i^k là đa thức bậc k+1k + 1 theo nn: hiệu S(n)−S(n−1)=nkS(n) - S(n - 1) = n^k có bậc kk, và một dãy có hiệu bậc kk thì chính nó có bậc k+1k + 1 (như tích phân tăng bậc). Vậy chỉ cần S(0),S(1),…,S(k+1)S(0), S(1), \dots, S(k + 1), rồi nội suy tại nn bằng cách nút liên tiếp, tổng O(klog⁡k)O(k \log k) cho việc tính các lũy thừa và O(k)O(k) cho nội suy. Không cần công thức Faulhaber hay số Bernoulli.

Cách nhận ra “là đa thức” nói chung: lấy hiệu liên tiếp nhiều lần; nếu đáp án bậc dd thì hiệu lần thứ d+1d + 1 bằng 0. Viết brute force in thử vài giá trị là kiểm được nhanh.

Ví dụTổng Các Lũy Thừa

Đề. Tính 1k+2k+⋯+nk1^k + 2^k + \dots + n^k theo mô-đun 109+710^9 + 7 với n≤109n \le 10^9, k≤106k \le 10^6.

Lời giải. S(n)=∑i=1nikS(n) = \sum_{i=1}^n i^k là đa thức bậc k+1k + 1 theo nn, nên xác định bởi k+2k + 2 giá trị S(0),…,S(k+1)S(0), \dots, S(k + 1). Tính các giá trị đó bằng cộng dồn iki^k, rồi nội suy tại nn bằng đúng khung nút liên tiếp của ví dụ trước với m=k+1m = k + 1 thay cho NN.

Bước 1: các giá trị tại nút. s[i] cộng dồn power(i, k); đây là phần chậm nhất. n <= m thì s[n] đã là đáp án.

C++
int m = k + 1;
for (int i = 1; i <= m; i++) s[i] = (s[i - 1] + power(i, k)) % MOD;
if (n <= m) {
    cout << s[n] << '\n';
    return 0;
}

Bước 2: nội suy chép nguyên khung của ví dụ trước: pre, suf, giai thừa nghịch đảo và dấu (−1)m−i(-1)^{m - i}.

Kiểm nhanh với mẫu: k=1k = 1, n=4n = 4 ra 1+2+3+4=101 + 2 + 3 + 4 = 10.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll MOD = 1e9 + 7;
const int N = 1e6 + 5;
ll n, k, s[N], pre[N], suf[N], fac[N], ifac[N];

ll power(ll x, ll b) {
    ll r = 1;
    x %= MOD;
    while (b) {
        if (b & 1) r = r * x % MOD;
        x = x * x % MOD;
        b >>= 1;
    }
    return r;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> k;
    int m = k + 1;
    for (int i = 1; i <= m; i++) s[i] = (s[i - 1] + power(i, k)) % MOD;
    if (n <= m) {
        cout << s[n] << '\n';
        return 0;
    }
    fac[0] = 1;
    for (int i = 1; i <= m; i++) fac[i] = fac[i - 1] * i % MOD;
    ifac[m] = power(fac[m], MOD - 2);
    for (int i = m; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;
    pre[0] = 1;
    for (int i = 1; i <= m + 1; i++) pre[i] = pre[i - 1] * ((n - (i - 1)) % MOD) % MOD;
    suf[m + 1] = 1;
    for (int i = m; i >= 0; i--) suf[i] = suf[i + 1] * ((n - i) % MOD) % MOD;
    ll ans = 0;
    for (int i = 0; i <= m; i++) {
        ll term = s[i] * pre[i] % MOD * suf[i + 1] % MOD * ifac[i] % MOD * ifac[m - i] % MOD;
        if ((m - i) % 2) ans = (ans - term + MOD) % MOD;
        else ans = (ans + term) % MOD;
    }
    cout << ans << '\n';
}
4 1

O(klog⁡k)O(k \log k) cho các lũy thừa, O(k)O(k) cho nội suy.

DP có tham số lớn

Có những DP mà kết quả, xét như hàm của một tham số lớn DD, là đa thức bậc nhỏ. Ví dụ đếm cách gán nhãn trong [1,D][1, D] cho cây sao cho con không lớn hơn cha: gọi Gv(x)G_v(x) là số cách gán cây con vv với nhãn của vv không vượt xx. Lá có Gv(x)=xG_v(x) = x, bậc 1. Ở đỉnh trong, số cách khi nhãn của vv đúng bằng yy là ∏cGc(y)\prod_c G_c(y), có bậc szv−1sz_v - 1; cộng dồn theo y≤xy \le x tăng bậc lên 1. Vậy GvG_v có bậc szvsz_v, và GgoˆˊcG_{\text{gốc}} có bậc nn.

Tính GG cho x=0..nx = 0..n bằng DP O(n2)O(n^2), rồi nội suy Ggoˆˊc(D)G_{\text{gốc}}(D) với DD tới 10910^9. Cùng mẹo cho DP mà một chiều là “giá trị tối đa DD”: chạy DP với DD nhỏ, chứng minh bậc, nội suy.

Hỏi nhanh

Cây là một đường 3 đỉnh (gốc, con, cháu), nhãn trong [1,D][1, D], con không lớn hơn cha. Kết quả là đa thức bậc mấy theo DD?

Ví dụNhãn Không Tăng Trên Cây

Đề. Cây N≤3000N \le 3000 đỉnh, cha của vv là pv<vp_v < v. Đếm cách gán nhãn trong [1,D][1, D], D≤109D \le 10^9, sao cho nhãn con không lớn hơn nhãn cha, theo mô-đun 109+710^9 + 7.

Lời giải. Như phân tích ở trên, Gv(x)G_v(x) là số cách gán cây con của vv khi nhãn của vv không vượt xx, có bậc szvsz_v theo xx; gốc cho đa thức bậc NN. Tính GG tại x=0..Nx = 0..N bằng quy hoạch động rồi nội suy tại DD bằng khung nút liên tiếp. Vì pv<vp_v < v, duyệt vv từ NN về 1 thì mọi con được xử lý trước cha.

Bước 1: nhân các con. f[v][x] ban đầu là 1 với mọi xx. Khi con cc xong, f[par[c]][x] nhân thêm Gc(x)G_c(x). Tới lượt vv, mọi con đã xong, nên f[v][y] =∏cGc(y)= \prod_c G_c(y): số cách khi nhãn của vv đúng bằng yy.

Bước 2: đổi thành tổng tiền tố. Đặt f[v][0] = 0 (nhãn phải từ 1), rồi cộng dồn: giờ f[v][x] =∑y≤x∏cGc(y)=Gv(x)= \sum_{y \le x} \prod_c G_c(y) = G_v(x).

C++
for (int v = n; v >= 1; v--) {
    f[v][0] = 0;
    for (int x = 1; x <= n; x++) f[v][x] = (f[v][x - 1] + f[v][x]) % MOD;
    if (v > 1)
        for (int x = 0; x <= n; x++) f[par[v]][x] = (ll)f[par[v]][x] * f[v][x] % MOD;
}

Bước 3: nội suy. d <= n thì f[1][d] đã là đáp án; ngược lại nội suy f[1][0..n] tại dd. Bảng f là 3001×30013001 \times 3001 số int, khoảng 36 MB.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll MOD = 1e9 + 7;
const int N = 3005;
int n, par[N], f[N][N];
ll d, pre[N], suf[N], fac[N], ifac[N];

ll power(ll x, ll b) {
    ll r = 1;
    x %= MOD;
    while (b) {
        if (b & 1) r = r * x % MOD;
        x = x * x % MOD;
        b >>= 1;
    }
    return r;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> d;
    for (int v = 2; v <= n; v++) cin >> par[v];
    for (int v = 1; v <= n; v++)
        for (int x = 0; x <= n; x++) f[v][x] = 1;
    for (int v = n; v >= 1; v--) {
        f[v][0] = 0;
        for (int x = 1; x <= n; x++) f[v][x] = (f[v][x - 1] + f[v][x]) % MOD;
        if (v > 1)
            for (int x = 0; x <= n; x++) f[par[v]][x] = (ll)f[par[v]][x] * f[v][x] % MOD;
    }
    if (d <= n) {
        cout << f[1][d] << '\n';
        return 0;
    }
    fac[0] = 1;
    for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
    ifac[n] = power(fac[n], MOD - 2);
    for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;
    pre[0] = 1;
    for (int i = 1; i <= n + 1; i++) pre[i] = pre[i - 1] * ((d - (i - 1)) % MOD) % MOD;
    suf[n + 1] = 1;
    for (int i = n; i >= 0; i--) suf[i] = suf[i + 1] * ((d - i) % MOD) % MOD;
    ll ans = 0;
    for (int i = 0; i <= n; i++) {
        ll term = f[1][i] * pre[i] % MOD * suf[i + 1] % MOD * ifac[i] % MOD * ifac[n - i] % MOD;
        if ((n - i) % 2) ans = (ans - term + MOD) % MOD;
        else ans = (ans + term) % MOD;
    }
    cout << ans << '\n';
}
1 4

Quy hoạch động O(N2)O(N^2), nội suy O(N)O(N).

Khi đi thi

  • Tham số tới 10910^9 trở lên mà đáp án có vẻ “trơn” theo nó: in brute force vài giá trị, lấy hiệu liên tiếp để đoán bậc; nếu là đa thức bậc dd, tính d+1d + 1 giá trị rồi nội suy.
  • Chọn nút 0..d0..d để nội suy O(d)O(d); nút bất kỳ thì O(d2)O(d^2) với một nghịch đảo mỗi cơ sở.
  • Trả ngay giá trị khi điểm cần tính trùng một nút; cẩn thận dấu (−1)n−i(-1)^{n - i} và các thừa số âm theo mô-đun.

Tóm tắt

  • f(x)=∑yi∏j≠ix−xjxi−xjf(x) = \sum y_i \prod_{j \ne i} \frac{x - x_j}{x_i - x_j}, duy nhất với bậc nhỏ hơn số nút.
  • Nút 0..n0..n: mẫu là i!(n−i)!(−1)n−ii!(n-i)!(-1)^{n-i}, tử là tích tiền tố nhân hậu tố; O(n)O(n).
  • ∑i≤nik\sum_{i \le n} i^k là đa thức bậc k+1k + 1 theo nn.
  • DP mà mỗi đỉnh, mỗi bước cộng dồn làm tăng bậc 1: bậc kết quả bằng kích thước, tính ở giá trị nhỏ rồi nội suy.

Bài tập

1
Đa Thức Nhị Phân Trên Trườngcanuc80k7102Theo Fermat, 1−(x−i)p−11 - (x - i)^{p-1} bằng 1 tại x=ix = i và 0 ở chỗ khác; cộng các đa thức này với hệ số aia_i, khai triển nhị thức để lấy hệ số, O(p2)O(p^2).
LQDOJ
2
Siêu Lập Phương Ngẫu Nhiêncanuc80k7104Có (n−s+1)k(n - s + 1)^k siêu lập phương cạnh ss; tử và mẫu của kỳ vọng là tổng của đa thức theo ss, mỗi tổng là đa thức theo nn, tính ở giá trị nhỏ rồi nội suy.
LQDOJ
3
Tổng Tích Lũycanuc80k7105F(n,m)F(n, m) theo nn là đa thức bậc K+mK + m; tính các giá trị nhỏ bằng truy hồi rồi nội suy tại NN.
LQDOJ
4
Tích Của Các Dãy Phân Biệtcanuc80k7106Đáp án là n!n! nhân tổng tích của mọi tập nn số trong [1,k][1, k], một đa thức bậc 2n2n theo kk; DP với kk nhỏ rồi nội suy theo mô-đun pp.
LQDOJ
5
Truy Tìm Nghiệm Đa Thứccanuc80k7107 · Tương tácHỏi giá trị tại 11 điểm, giải hệ Vandermonde theo mô-đun để lấy hệ số, rồi thử mọi xx tìm nghiệm.
LQDOJ
6
Tổng Kép Lũy Thừacanuc80k7109SK(N)S_K(N) là đa thức bậc K+2K + 2; với mỗi số nguyên tố pp trong [L,R][L, R] nội suy riêng theo mô-đun pp.
LQDOJ
7
Khoảng Cách Giữa Các Câycanuc80k7114Ma trận Kirchhoff với cạnh cũ trọng số xx, cạnh mới trọng số 1: định thức là đa thức theo xx, hệ số xjx^j đếm cây giữ đúng jj cạnh cũ; tính tại NN điểm rồi nội suy hệ số.
LQDOJ
8
Dịch Một Đoạn Giá Trị Đa Thứccanuc80k7115Viết công thức nút liên tiếp cho F(M+k)F(M + k) thành một tích chập với dãy nghịch đảo 1M−N+i\frac{1}{M - N + i}, nhân bằng NTT.
LQDOJ

Đọc thêm

  • 插值OI WikiNội suy Lagrange, nút liên tiếp và độ phức tạp.
  • Lagrange polynomialWikipediaĐịnh nghĩa, tính duy nhất và dạng trọng tâm.