Nội suy Lagrange
Nội suy Lagrange (Lagrange interpolation) là kỹ thuật tìm đa thức bậc duy nhất đi qua điểm , rồi tính giá trị của nó tại một điểm bất kỳ. Nó cho phép tính trong ở trường hợp tổng quát, hoặc chỉ khi các mốc 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 rồi mới thay số.
Ứng dụng kinh điển nhất trong competitive programming: tính tổng hoặc với là đa thức, khi lên tới — những bài mà vòng lặp trực tiếp là bất khả thi.
Ý tưởng / Trực giác
Vì sao chỉ cần điểm là đủ? Một đa thức bậc có đúng hệ số . Mỗi điểm cho ta một phương trình tuyến tính theo các hệ số đó. điểm với các đôi một khác nhau cho 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 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 "viên gạch" , mỗi viên ứng với một điểm:
Viên gạch được thiết kế để có tính chất bộ lọc:
- Tại : tử và mẫu giống hệt nhau nên .
- Tại với : trong tích có thừa số nên .
Nói cách khác . Vì vậy
thỏa tại mọi mốc. Mỗi là đa thức bậc , nên cũng bậc — đúng đa thức ta cần. Trực giác: chỉ được "kích hoạt" tại đúng điểm 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? là đa thức bậc theo (định lý Faulhaber). Do đó chỉ cần tính giá trị đầu tiên bằng cộng dồn, rồi nội suy tại 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 (chính là tại ). Đa thức bậc qua 3 điểm này thực ra là thu hẹp — ta nội suy giá trị tại (kết quả đúng phải là... lưu ý bậc 3 nên qua 3 điểm KHÔNG khôi phục được ; đa thức bậc 2 qua 3 điểm này sẽ cho giá trị khác tại — 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: 0 1 2
x_i: 1 2 3
y_i: 1 8 27
| | |
x = 4 ---+--------+--------+----> tính P(4)
Tính
Tính
Tính
Ghép lại:
Bảng tổng hợp từng bước (ô đang xét là cột ):
| đóng góp | ||||
|---|---|---|---|---|
| 0 | 1 | 1 | ||
| 1 | 2 | 8 | ||
| 2 | 3 | 27 | ||
| Σ | 58 |
Kiểm chứng: đa thức bậc 2 qua 3 điểm trên là ; thật vậy ... khoan, ✓. Và ✓. Khớp với nội suy.
Cài đặt
Tổng quát — với đ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 — khi
Khi mốc liên tiếp, tích được tách thành prefix và suffix tính một lần; còn mẫu 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
là đa thức bậc nên cần đ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 ( điểm bất kỳ) | ||
| liên tiếp |
Vì sao ở bản tổng quát? Với mỗi trong điểm, tích chạy qua điểm còn lại → phép nhân. Nếu mỗi vòng lại gọi inv (lũy thừa nhanh ) thì thành — vẫn coi là vì 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 ở bản liên tiếp? Prefix/suffix và giai thừa đều tính trong một lượt ; nghịch đảo giai thừa chỉ tốn một lần lũy thừa nhanh rồi truy hồi ngược . Vòng tổng cuối là . Bộ nhớ cho các mảng pre, suf, fact, inv_fact.
⚠️ Lỗi thường gặp
- Modulo âm khi tính : có thể âm,
a % MODtrong 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à . Nếu MOD hợp số (vd , 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. - trùng một mốc : khi đó tồn tại thừa số ở tử của các viên gạch khác và ; công thức vẫn đúng nhưng nếu cài bản mà không xử lý, mẫu có thể dính 0. An toàn nhất: nếu là số nguyên trong thì trả thẳng .
- Lệch mốc (off-by-one) ở tổng lũy thừa: ứng với nhưng
lagrange_consecdùng mốc . Phải nội suy tại chứ không phải . 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 cần điểm. Tổng là bậc → cần điểm; lấy thiếu (vd ) 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 longvà% MODngay sau mỗi phép nhân;int * intvới MOD 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 (): tương tự bản liên tiếp, chỉ cần chia thêm lũy thừa của .
- Lấy hệ số đa thức thay vì giá trị tại một điểm: cần khai triển tích — tốn 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 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 với tới , luyện tư duy "tính đại lượng tại 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.