Tổng là một đa thức bậc 2 theo . 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 , có suy ra được tổng ở không?
Được, nhờ một đa thức bậc không quá hoàn toàn xác định bởi giá trị tại đ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ố: với điểm bất kỳ, khi các điểm là . 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 ), 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 .
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ở , bằng 1 tại nút và 0 tại các nút khác. Đổi hoặc để xem.
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;
#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
11
Độ phức tạp: , khoảng phép nhân với .
Nút liên tiếp
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;
#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
16
Độ phức tạp: .
Tổng lũy thừa
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 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 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;
#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
10
cho các lũy thừa, cho nội suy. Phần lũy thừa là chỗ tốn nhất, và làm nhanh hơn được vì 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.
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 cho mọi 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 , 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 mọi DP có 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;
#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
4
Độ phức tạp: quy hoạch động , nội suy .
Khi đi thi
- Tham số tới 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 thì 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à 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 Wiki
- Lagrange polynomialWikipedia