拉格朗日插值法
簡介
衆所周知, 個點 (任意兩個點橫座標不相等)可以確定一個 次多項式函數 。拉格朗日插值法可以根據這 個點求出這個多項式 。當然,實際應用中通常求出橫座標爲 的點在該( 個點確定的)多項式函數上對應的縱座標的值,代碼實現中我們也只考慮這一問題。
一個直觀的想法是利用待定係數法設 ,然後帶入 個點得到一個 元一次方程組,然後用 高斯消元 解得係數。但這個方法和拉格朗日插值法相比有兩個問題:一是時間複雜度爲 ,而拉格朗日法時間複雜度爲 ;二就是係數可能解出小數,還可能很大,而拉格朗日法可以支持取模,並跳過“係數”這一中間步驟,直接求值。
拉格朗日插值法
我們以二次函數爲例,看一看拉格朗日插值法的具體過程:已知 個點 ,,,求 。
拉格朗日(Joseph-Louis Lagrange,1736 ~ 1813)的做法非常巧妙地避開了解多元方程的過程:
令 表示經過點 ,, 的二次函數;
表示經過點 ,, 的二次函數;
表示經過點 ,, 的二次函數。
那麼 。
原因很簡單,每個子函數確保經過一個點而不經過另外兩個點。
而子函數的求法很簡單,以 爲例:
的兩根爲 和 ,於是設 ,再帶入點 ,得到 ,於是 。
求高次函數與求二次函數的方法同理,可得
於是,想求 的值,將 代入上式即可,時間複雜度 ( 爲次數)。
模板
#include <bits/stdc++.h>
const int MOD = 998244353;
const int MAXN = 2000;
int Mul(const int &a, const int &b) {
return (long long)a * b % MOD;
}
int Inv(int x) {
int y = MOD - 2, ret = 1;
while (y) {
if (y & 1)
ret = Mul(ret, x);
x = Mul(x, x);
y >>= 1;
}
return ret;
}
int N, K, X[MAXN + 5], Y[MAXN + 5];
int main() {
scanf("%d%d", &N, &K); // 求 f(K)
for (int i = 1; i <= N; i++)
scanf("%d%d", &X[i], &Y[i]);
int Ans = 0;
for (int i = 1; i <= N; i++) {
int x = Y[i], y = 1;
for (int j = 1; j <= N; j++)
if (j != i) {
x = Mul(x, (K - X[j] + MOD) % MOD);
y = Mul(y, (X[i] - X[j] + MOD) % MOD);
}
Ans = (Ans + Mul(x, Inv(y))) % MOD;
}
printf("%d", Ans);
}
DP 優化
思路
如果沒有接觸過可能很難想到這個與 DP 的聯繫。事實上,我們可以將某一維的 DP 看作一個函數,即令(注意這個 與上文中的“子函數”沒有關係)那麼,如果我們要求的 中的 值很大(例如 ),我們就可以只計算 ( 爲 的次數),並用點 ,,…, 確定多項式 ,並快速求得 ,即 ,時間複雜度爲 。
這類優化的難點在於要準確地計算 的值,即 的次數,接下來通過例題講解如何計算 。
例題一
分析
發現我們只需要計算所有遞增的合法序列的值之和,然後乘上 即爲答案,因爲每種遞增的合法序列任意打亂順序仍然是合法的,並且原先就不同,打亂後也一定不同。
令 表示:長度爲 的所含元素值不超過 的遞增的合法序列的值之和,考慮在第 個位置放元素 還是放其他小於 的元素,本質即爲一個揹包問題,則答案爲 ,然後發現 ,不可能直接 DP。
按照上文中的方法,我們令 ,所求的就是 。接下來求出多項式 的次數 ,然後我們就只需要 DP 出 到 ,再用拉格朗日插值法就能算出 了。
接下來推導 的次數,令 表示多項式 的次數:
設 ,將 和 暴力代入 這個式子,發現 這個最高次項被消掉了(代入後有關最高次項的部分僅爲 )!
於是得到 的次數爲 ,又因爲 的次數爲 ,所以
又因爲 ()所以 ,證得 的次數爲 。
然後我們只需要用樸素的 DP 求得 ,,…,(注意點數要求比次數多一才能得到正確的多項式),並用拉格朗日插值法求得 即可。
代碼
#include <bits/stdc++.h>
const int MAXN = 500;
int N, K, P;
int Dp[MAXN + 5][2 * MAXN + 1 + 5];
int Add(int a, const int &b) {
a += b; return (a >= P) ? (a - P) : a;
}
int Mul(const int &a, const int &b) {
return (long long)a * b % P;
}
int Inv(int x) {
int y = P - 2, ret = 1;
while (y) {
if (y & 1)
ret = Mul(ret, x);
x = Mul(x, x);
y >>= 1;
}
return ret;
}
int main() {
scanf("%d%d%d", &K, &N, &P);
int M = 2 * N + 1;
for (int i = 0; i <= M; i++)
Dp[0][i] = 1;
for (int i = 1; i <= N; i++)
for (int j = i; j <= M; j++)
Dp[i][j] = Add(Dp[i][j - 1], Mul(Dp[i - 1][j - 1], j));
int Ans = 0, Fac = 1;
for (int i = 1; i <= N; i++)
Fac = Mul(Fac, i);
for (int i = 1; i <= M; i++) {
int x = Dp[N][i], y = 1;
for (int j = 1; j <= M; j++)
if (i != j) {
x = Mul(x, (K >= j) ? (K - j) : (K - j + P));
y = Mul(y, (i >= j) ? (i - j) : (i - j + P));
}
Ans = Add(Ans, Mul(x, Inv(y)));
}
printf("%d", Mul(Ans, Fac));
return 0;
}
例題二
題意:給定整數 和 (,)以及一個 個結點的樹,要求給每個結點分配一個 之間的整數作爲權值,並且滿足父親結點權值大於等於兒子結點,求方案總數。
分析
令 表示:以 爲根的子樹中,每個結點的權值都在 內的方案數,同樣是一個揹包
定義與上題類似,然後得到
注意邊界 ,因爲對於一個葉子 有 。因此這就是一個子樹大小的 DP 式,於是 ,暴力算得 ,,…,,再拉格朗日即可。
代碼
#include <bits/stdc++.h>
const int MAXN = 3000;
const int MOD = 1000000007;
int N, D, M;
std::vector<int> G[MAXN + 5];
int Dp[MAXN + 5][MAXN + 5];
int Add(int a, const int &b) {
a += b; return (a >= MOD) ? (a - MOD) : a;
}
int Mul(const int &a, const int &b) {
return (long long)a * b % MOD;
}
int Inv(int x) {
int y = MOD - 2, ret = 1;
while (y) {
if (y & 1)
ret = Mul(ret, x);
x = Mul(x, x);
y >>= 1;
}
return ret;
}
void Dfs(int u) {
for (int v: G[u])
Dfs(v);
for (int i = 1; i <= M; i++) {
int tmp = 1;
for (int v: G[u])
tmp = Mul(tmp, Dp[v][i]);
Dp[u][i] = Add(Dp[u][i - 1], tmp);
}
}
int main() {
scanf("%d%d", &N, &D);
for (int i = 2; i <= N; i++) {
int p; scanf("%d", &p);
G[p].push_back(i);
}
M = N + 1;
Dfs(1);
int Ans = 0;
for (int i = 1; i <= M; i++) {
int x = Dp[1][i], y = 1;
for (int j = 1; j <= M; j++)
if (i != j) {
x = Mul(x, (D >= j) ? (D - j) : (D - j + MOD));
y = Mul(y, (i >= j) ? (i - j) : (i - j + MOD));
}
Ans = Add(Ans, Mul(x, Inv(y)));
}
printf("%d", Ans);
return 0;
}
A trick
上面兩題的“點”的橫座標有個規律:是連續的 個正整數。結合拉格朗日插值法的分子分母的特徵,發現可以用前綴積和後綴積優化拉格朗日插值法的內層循環代碼,使時間複雜度由 優化爲 ,但是複雜度的瓶頸在於開頭的樸素 DP,所以沒有提這個方法。