Chương 7·Bài 71

Lagrange Interpolation

Tổng 1+2+⋯+n=n(n+1)/21 + 2 + \dots + n = n(n+1)/2 là một đa thức bậc 2 theo nn. Tổng bình phương là đa thức bậc 3. Vậy nếu biết tổng ở đủ vài giá trị nhỏ của nn, có suy ra được tổng ở n=109n = 10^9 không?

Được, nhờ một đa thức bậc không quá n−1n - 1 hoàn toàn xác định bởi giá trị tại nn điểm phân biệt. Nội suy Lagrange tính 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ỳ, O(n)O(n) khi các điểm là 0,1,…,n0, 1, \dots, n. Phần khó của bài 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).

Hình trên là đa thức đi qua các điểm đã cho; hình dưới là đa thức cơ sở LiL_i, bằng 1 tại nút ii và 0 tại các nút khác. Đổi yy hoặc ii để xem.

Đa thức nội suy và cơ sở LiL_i.

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;
#define 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

Độ phức tạp: 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

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;
#define 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

Độ phức tạp: O(N+log⁡p)O(N + \log p).

Tổng lũy thừa

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 nút liên tiếp. 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;
#define 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. Phần lũy thừa là chỗ tốn nhất, và làm nhanh hơn được vì iki^k là hàm nhân tính hoàn toàn: chỉ cần lũy thừa nhanh tại các số nguyên tố, rồi suy ra các số khác bằng một phép nhân qua sàng tuyến tính.

Tính i^k theo mô-đun cho i từ 1 tới một triệu, trung vị 5 lần chạy, i7-10850H, g++ -O2.lũy thừa nhanh từng số59 ms · gấp 3,3 lầnsàng tuyến tính, chỉ số nguyên tố18 ms
Tính i^k theo mô-đun cho i từ 1 tới một triệu, hai cách, đo trên i7-10850H, g++ -O2.

Sàng tuyến tính nhanh hơn lũy thừa nhanh từng số khoảng 3 lần. Quy tắc: cần iki^k cho mọi ii liên tiếp thì chỉ tính tại số nguyên tố.

DP 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 mọi DP có 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;
#define 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

Độ phức tạp: 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 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; là đa thức bậc dd thì 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à 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

Đọc thêm