Wiki Thuật toán Toán học Nội suy Lagrange

Nội suy Lagrange

huunguyen huunguyen Updated Tháng sáu 1, 2026

Nội suy Lagrange (Lagrange interpolation) là kỹ thuật tìm đa thức P(x) bậc N1 duy nhất đi qua N điểm (x0,y0),,(xN1,yN1), rồi tính giá trị của nó tại một điểm x bất kỳ. Nó cho phép tính P(x) trong O(N2) ở trường hợp tổng quát, hoặc chỉ O(N) khi các mốc xi là dãy số nguyên liên tiếp — thay vì phải khôi phục toàn bộ hệ số đa thức tốn O(N2) rồi mới thay số.

Ứng dụng kinh điển nhất trong competitive programming: tính tổng i=1nik hoặc i=1nf(i) với f là đa thức, khi n lên tới 1018 — những bài mà vòng lặp trực tiếp O(n) là bất khả thi.

Ý tưởng / Trực giác

Vì sao chỉ cần N điểm là đủ? Một đa thức bậc N1 có đúng N hệ số a0,a1,,aN1. Mỗi điểm (xi,yi) cho ta một phương trình tuyến tính theo các hệ số đó. N điểm với các xi đôi một khác nhau cho N phương trình độc lập, nên đa thức được xác định duy nhất. Đây là lý do "biết N giá trị thì suy ra mọi giá trị khác".

Vì sao công thức Lagrange đúng? Thay vì giải hệ phương trình, Lagrange xây trực tiếp đa thức bằng cách ghép N "viên gạch" i(x), mỗi viên ứng với một điểm:

i(x)=jixxjxixj

Viên gạch i được thiết kế để có tính chất bộ lọc:

  • Tại x=xi: tử và mẫu giống hệt nhau nên i(xi)=1.
  • Tại x=xm với mi: trong tích có thừa số (xmxm)=0 nên i(xm)=0.

Nói cách khác i(xm)=[i=m]. Vì vậy

P(x)=i=0N1yi·i(x)

thỏa P(xm)=iyi·[i=m]=ym tại mọi mốc. Mỗi i là đa thức bậc N1, nên P cũng bậc N1 — đúng đa thức ta cần. Trực giác: yi chỉ được "kích hoạt" tại đúng điểm xi của nó, các điểm khác bị triệt tiêu về 0.

Vì sao tổng lũy thừa lại là đa thức? S(n)=i=1nik là đa thức bậc k+1 theo n (định lý Faulhaber). Do đó chỉ cần tính k+2 giá trị đầu tiên S(1),S(2),,S(k+2) bằng cộng dồn, rồi nội suy tại n khổng lồ. Ta không cần biết công thức đóng — chỉ cần "đủ điểm".

Ví dụ chạy tay

Cho 3 điểm (1,1), (2,8), (3,27) (chính là y=x3 tại x=1,2,3). Đa thức bậc 2 qua 3 điểm này thực ra là x3 thu hẹp — ta nội suy giá trị tại x=4 (kết quả đúng phải là... lưu ý x3 bậc 3 nên qua 3 điểm KHÔNG khôi phục được x3; đa thức bậc 2 qua 3 điểm này sẽ cho giá trị khác 64 tại x=4 — chính minh hoạ rằng số điểm phải bậc+1).

Bố trí các mốc và việc tính từng viên gạch i(4):

  i:        0        1        2
 x_i:       1        2        3
 y_i:       1        8       27
            |        |        |
   x = 4 ---+--------+--------+----> tính P(4)

Tính 0(4)=(42)(43)(12)(13)=2·1(1)(2)=22=1

Tính 1(4)=(41)(43)(21)(23)=3·1(1)(1)=31=3

Tính 2(4)=(41)(42)(31)(32)=3·2(2)(1)=62=3

Ghép lại:

P(4)=1·1+8·(3)+27·3=124+81=58

Bảng tổng hợp từng bước (ô đang xét là cột i(4)):

i xi yi i(4) đóng góp yi·i(4)
0 1 1 1 1
1 2 8 3 24
2 3 27 3 81
Σ 58

Kiểm chứng: đa thức bậc 2 qua 3 điểm trên là P(x)=6x211x+6; thật vậy P(1)=1,P(2)=8,P(3)=21... khoan, P(3)=6·933+6=27 ✓. Và P(4)=9644+6=58 ✓. Khớp với nội suy.

Cài đặt

Tổng quát — O(N2) với N điểm bất kỳ
const long long MOD = 1e9 + 7;

long long power(long long a, long long b, long long mod) {
    long long res = 1; a %= mod;
    if (a < 0) a += mod;                 // phòng a âm
    for (; b > 0; b >>= 1) {
        if (b & 1) res = res * a % mod;
        a = a * a % mod;
    }
    return res;
}
// Nghịch đảo modulo: chỉ đúng khi MOD là số nguyên tố (Fermat nhỏ)
long long inv(long long a) { return power(a, MOD - 2, MOD); }

// Nội suy giá trị P(x) từ N điểm (xi[i], yi[i])
long long lagrange(vector<long long>& xi, vector<long long>& yi, long long x) {
    int n = xi.size();
    long long ans = 0;
    for (int i = 0; i < n; i++) {
        long long num = yi[i] % MOD, den = 1;
        for (int j = 0; j < n; j++) {
            if (j == i) continue;
            // tử nhân (x - x_j), giữ trong [0, MOD)
            num = num * (((x - xi[j]) % MOD + MOD) % MOD) % MOD;
            // mẫu nhân (x_i - x_j)
            den = den * (((xi[i] - xi[j]) % MOD + MOD) % MOD) % MOD;
        }
        ans = (ans + num % MOD * inv(den)) % MOD; // chia = nhân nghịch đảo
    }
    return ans;
}
Nhanh — O(N) khi xi=0,1,2,,N1

Khi mốc liên tiếp, tích ji(xj) được tách thành prefix j<i(xj)suffix j>i(xj) tính một lần; còn mẫu ji(ij)=(1)N1ii!(N1i)! dùng giai thừa.

// Nội suy tại x từ N điểm (0, y[0]), (1, y[1]), ..., (N-1, y[N-1])
long long lagrange_consec(vector<long long>& y, long long x) {
    int n = y.size();
    x = ((x % MOD) + MOD) % MOD;

    // pre[i] = tích (x-0)(x-1)...(x-(i-1)) ; suf[i] = (x-i)...(x-(n-1))
    vector<long long> pre(n + 1, 1), suf(n + 1, 1);
    for (int i = 0; i < n; i++)
        pre[i + 1] = pre[i] * ((x - i + MOD) % MOD) % MOD;
    for (int i = n - 1; i >= 0; i--)
        suf[i] = suf[i + 1] * ((x - i + MOD) % MOD) % MOD;

    // tiền xử lý giai thừa và nghịch đảo giai thừa
    vector<long long> fact(n, 1), inv_fact(n, 1);
    for (int i = 1; i < n; i++) fact[i] = fact[i - 1] * i % MOD;
    inv_fact[n - 1] = power(fact[n - 1], MOD - 2, MOD);
    for (int i = n - 2; i >= 0; i--)
        inv_fact[i] = inv_fact[i + 1] * (i + 1) % MOD;

    long long ans = 0;
    for (int i = 0; i < n; i++) {
        long long num = pre[i] * suf[i + 1] % MOD;       // bỏ qua thừa số (x-i)
        long long den = inv_fact[i] * inv_fact[n - 1 - i] % MOD;
        if ((n - 1 - i) & 1) den = MOD - den;            // dấu (-1)^(n-1-i)
        ans = (ans + y[i] % MOD * num % MOD * den) % MOD;
    }
    return ans;
}
Ứng dụng: tổng lũy thừa i=1nik

S(n) là đa thức bậc k+1 nên cần k+2 điểm:

long long sum_of_powers(long long n, int k) {
    int pts = k + 2;                 // số điểm = bậc + 1
    vector<long long> y(pts);
    long long cur = 0;
    for (int i = 1; i <= pts; i++) { // y[i-1] = S(i) = tổng dồn
        cur = (cur + power(i, k, MOD)) % MOD;
        y[i - 1] = cur;
    }
    // mốc của ta là x=1..pts; lagrange_consec dùng mốc 0..pts-1,
    // nên nội suy tại (n-1): điểm 0 ứng S(1), điểm pts-1 ứng S(pts)
    if (n <= pts) return y[n - 1];
    return lagrange_consec(y, (n - 1) % MOD);
}

Độ phức tạp

Phương pháp Thời gian Bộ nhớ
Tổng quát (N điểm bất kỳ) O(N2) O(N)
xi liên tiếp O(N) O(N)

Vì sao O(N2) ở bản tổng quát? Với mỗi trong N điểm, tích chạy qua N1 điểm còn lại → N·(N1) phép nhân. Nếu mỗi vòng lại gọi inv (lũy thừa nhanh O(logMOD)) thì thành O(N2logMOD) — vẫn coi là O(N2)logMOD30 là hằng số nhỏ, nhưng nên gộp nghịch đảo bằng tích tiền tố nếu muốn chặt chẽ.

Vì sao O(N) ở bản liên tiếp? Prefix/suffix và giai thừa đều tính trong một lượt O(N); nghịch đảo giai thừa chỉ tốn một lần lũy thừa nhanh O(logMOD) rồi truy hồi ngược O(N). Vòng tổng cuối là O(N). Bộ nhớ O(N) cho các mảng pre, suf, fact, inv_fact.

⚠️ Lỗi thường gặp

  • Modulo âm khi tính (xxj): xxj có thể âm, a % MOD trong C++ trả số âm. Luôn dùng ((x - xj) % MOD + MOD) % MOD. Quên bước này → kết quả sai ngẫu nhiên, khó debug.
  • Nghịch đảo khi MOD không nguyên tố: inv(a) = power(a, MOD-2) (Fermat nhỏ) chỉ đúng khi MOD nguyên tố và gcd(a,MOD)=1. Nếu MOD hợp số (vd 264, hay modulo bài cho không nguyên tố) phải dùng nghịch đảo qua thuật toán Euclid mở rộng, hoặc đảm bảo phép chia là chia hết thật.
  • x trùng một mốc xi: khi đó tồn tại thừa số (xxi)=0 ở tử của các viên gạch khác và i(x)=1; công thức vẫn đúng nhưng nếu cài bản O(N) mà không xử lý, mẫu có thể dính 0. An toàn nhất: nếu x là số nguyên trong [0,N1] thì trả thẳng y[x].
  • Lệch mốc (off-by-one) ở tổng lũy thừa: S(i) ứng với i=1,2, nhưng lagrange_consec dùng mốc 0,1,. Phải nội suy tại (n1) chứ không phải n. Sai chỗ này cho kết quả lệch đúng một bậc, dễ qua test nhỏ nhưng sai test lớn.
  • Thiếu số điểm so với bậc: đa thức bậc d cần d+1 điểm. Tổng ik là bậc k+1 → cần k+2 điểm; lấy thiếu (vd k+1) sẽ nội suy nhầm sang đa thức bậc thấp hơn và sai. Khi không chắc bậc, lấy dư vài điểm cho an toàn.
  • Tràn số: mọi tích trung gian phải long long% MOD ngay sau mỗi phép nhân; int * int với MOD 109 tràn ngay. Đừng cộng dồn nhiều tích rồi mới lấy modulo một lần.

Biến thể / Mở rộng

  • Nội suy với mốc cách đều bước h (xi=x0+ih): tương tự bản liên tiếp, chỉ cần chia thêm lũy thừa của h.
  • Lấy hệ số đa thức thay vì giá trị tại một điểm: cần khai triển tích (xxj) — tốn O(N2) và thường không cần trong CP.
  • Liên hệ: kỹ thuật này thường đi kèm tính nhanh giai thừa / nghịch đảo modulo và lũy thừa nhanh — xem Toán học để ôn lại số học modulo nền tảng.

Bài tập luyện

Lưu ý: pool CTOJ hiện chưa có bài chuyên về nội suy Lagrange. Dưới đây là các bài rèn bộ công cụ số học modulo (lũy thừa nhanh, nghịch đảo, đếm theo modulo số nguyên tố, công thức đóng tại n lớn) — nền tảng bắt buộc trước khi áp dụng Lagrange.

  • Lũy thừa modulo (modexp)(Amateur) Rèn hàm power(a, b, MOD) — chính là engine để tính nghịch đảo modulo dùng khắp nơi trong Lagrange.
  • Đếm lưới ô vuông (cntgrids)(Advanced) Đếm theo công thức đóng modulo 109+7 với n tới 109, luyện tư duy "tính đại lượng tại n rất lớn" bằng lũy thừa modulo.
  • Số học mô-đun (modarith)(Advanced) Đếm số hàm modulo một số nguyên tố lẻ; củng cố thao tác nghịch đảo và đếm dưới modulo nguyên tố — đúng bộ kỹ năng Lagrange yêu cầu.
gnatmake 12.2.0 a68g 3.1.2 nasm 2.16.1 as_x64 2.46 awk 1.3.4 gcc 16.1.0 csc 6.12.0.200 g++ 16.1.0 g++-themis 16.1.0 g++17 16.1.0 g++20 16.1.0 g++23 16.1.0 clang++ 22.1.6 dmd 2.112.0 dart 3.12.1 gforth 0.7.3 gfortran 12.2.0 go 1.26.3 groovyc 5.0.6 javac 25.0.3 node 26.2.0 kotlinc 2.3.21 sbcl 2.2.9 lua 5.4.8 nim 2.2.10 fpc 3.2.2 fpc-themis 3.2.2 perl 5.36.0 php 8.5.6 pike 8.0 pypy3 7.3.23 python3 3.14.5 racket 8.7 ruby 4.0.5 rustc 1.96.0 csc 5.3.0 ctoj-scratch 0.0.1 sed 4.9 tclsh 8.6 bun 1.3.14 deno 2.8.1 v 0.5.1 zig 0.16.0