Một đa thức bậc không quá được xác định bởi giá trị tại đ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ố, với điểm bất kỳ và khi các điểm là . 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 ), 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 và giá trị , đặt
có bậc , bằng 1 tại và bằng 0 tại mọi nút khác, nên đi qua đủ điểm. Hai đa thức bậc nhỏ hơn trùng nhau tại điểm thì hiệu của chúng có nghiệm nên bằng 0: đa thức nội suy là duy nhất.
Theo mô-đun nguyên tố , 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 . Với mỗi , nhân tử số và mẫu số riêng rồi lấy một nghịch đảo, tổng .
Ví dụGiá Trị Qua Các Điểm
Đề. điểm với phân biệt, tính theo mô-đun với là đa thức bậc nhỏ hơn qua các điểm đó.
Lời giải. Áp thẳng công thức nội suy tại điểm :
Với mỗi , nhân tử số và mẫu số riêng trong một vòng , 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 . Hiệu có thể âm, mà phép % của C++ giữ dấu, nên đưa về trước khi nhân. Các phân biệt nên mẫu số khác 0 và có nghịch đảo.
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;
}
#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
11
, khoảng phép nhân với .
Nút liên tiếp trong O(n)
Khi các nút là , mẫu số có dạng đóng: . Tử số là tích tiền tố nhân tích hậu tố . Tính sẵn giai thừa và nghịch đảo giai thừa, mỗi số hạng , tổng .
Nếu thì trả ngay , 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ó , , . Nội suy cho bằng bao nhiêu?
đi qua ba điểm, và đa thức bậc 2 qua ba điểm là duy nhất, nên . Công thức: .
Ví dụBảng Giá Trị Liên Tiếp
Đề. Đa thức bậc không quá , biết theo mô-đun . Tính với .
Lời giải. Nút là liên tiếp. Mẫu số của cơ sở là : các cho tích , các cho với dấu trừ. Tử số là tích tiền tố nhân tích hậu tố . Tính sẵn hai mảng tích và bảng nghịch đảo giai thừa thì mỗi số hạng .
Bước 1: trường hợp có sẵ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: .
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] , suf[i] ; tử số của cơ sở là pre[i] * suf[i + 1].
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 xử lý bằng cách cộng hay trừ.
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;
}
#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
16
.
Tổng lũy thừa là đa thức
là đa thức bậc theo : hiệu có bậc , và một dãy có hiệu bậc thì chính nó có bậc (như tích phân tăng bậc). Vậy chỉ cần , rồi nội suy tại bằng cách nút liên tiếp, tổng cho việc tính các lũy thừa và 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 thì hiệu lần thứ 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 theo mô-đun với , .
Lời giải. là đa thức bậc theo , nên xác định bởi giá trị . Tính các giá trị đó bằng cộng dồn , rồi nội suy tại bằng đúng khung nút liên tiếp của ví dụ trước với thay cho .
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.
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 .
Kiểm nhanh với mẫu: , ra .
#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
10
cho các lũy thừa, 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 , là đa thức bậc nhỏ. Ví dụ đếm cách gán nhãn trong cho cây sao cho con không lớn hơn cha: gọi là số cách gán cây con với nhãn của không vượt . Lá có , bậc 1. Ở đỉnh trong, số cách khi nhãn của đúng bằng là , có bậc ; cộng dồn theo tăng bậc lên 1. Vậy có bậc , và có bậc .
Tính cho bằng DP , rồi nội suy với tới . Cùng mẹo cho DP mà một chiều là “giá trị tối đa ”: chạy DP với 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 , con không lớn hơn cha. Kết quả là đa thức bậc mấy theo ?
Mỗi đỉnh làm bậc tăng 1: cháu có bậc 1, con bậc 2, gốc bậc 3. Thật vậy, số dãy không tăng là .
Ví dụNhãn Không Tăng Trên Cây
Đề. Cây đỉnh, cha của là . Đếm cách gán nhãn trong , , sao cho nhãn con không lớn hơn nhãn cha, theo mô-đun .
Lời giải. Như phân tích ở trên, là số cách gán cây con của khi nhãn của không vượt , có bậc theo ; gốc cho đa thức bậc . Tính tại bằng quy hoạch động rồi nội suy tại bằng khung nút liên tiếp. Vì , duyệt từ 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 . Khi con xong, f[par[c]][x] nhân thêm . Tới lượt , mọi con đã xong, nên f[v][y] : số cách khi nhãn của đúng bằng .
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] .
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 . Bảng f là số int, khoảng 36 MB.
#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
4
Quy hoạch động , nội suy .
Khi đi thi
- Tham số tới 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 , tính giá trị rồi nội suy.
- Chọn nút để nội suy ; nút bất kỳ thì 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 và các thừa số âm theo mô-đun.
Tóm tắt
- , duy nhất với bậc nhỏ hơn số nút.
- Nút : mẫu là , tử là tích tiền tố nhân hậu tố; .
- là đa thức bậc theo .
- 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
- 插值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.