ラグランジュ補間ってなーんだ?
算数エアプが書きます 多分算数エアプでもわかる 誤りには気をつけているつもりですが、エアプ特有のミスがあったらすみません
最初に
を素数とします。 基本的には
上での演算を対象とした議論です。加減乗算を
、除算を
で行えることを仮定しています。
添字は 0-indexed です。
ラグランジュ補間多項式
の組が
個あります。ここで
は相異なります。
このラグランジュ補間多項式 は、すべての
に対して
を満たす、次数が最小の多項式を指します。
証明は知らんのでしませんが、多項式の性質として、次数が最小の多項式は与えられた組に対して一意に定まります。 つまりラグランジュ補間多項式は一意です。(係数が体なら、対応する次数 以下の多項式が一意に存在するため、次数
以下の最小次数の多項式も一意)
ラグランジュ補間多項式の定義
ならば
、
ならば
を返す多項式であることを確認できます。(
なら分子分母同じなので
、
ならば分子の積のうちどこかが
になるので
です。)
このラグランジュ基底多項式を用いて、ラグランジュ補間多項式 を (2) で表せます。
のときに
となることは
の性質から明らかです。
は高々
次の多項式なので、当然その sum も高々
次です。したがって、与えられた条件を全て満たす最小次数の多項式を補間できたことになります。
多項式の係数
ラグランジュ補間多項式を復元するためには、上の定義の基づいて の係数を計算していけば良いです。
の各係数
かんたん
のアルゴリズムです。上の定義をそのまま実装するとできます。
まず、ラグランジュ基底多項式を求めます。 本の多項式をそれぞれ求めることとして、いま
番目の多項式を求めたいとします。この多項式の分母部分を
、分子部分を
とします。それぞれ以下のように計算されます。
分母 はねねちゃんをすると
で求められます。
分子 を求める際は
の
について
の係数を求めます。 各
について
を取るとき次数が
増えて
を取る時次数が変わらないとして、今までの
の次数を状態に持った
のDP により求められます。
具体的には、 を
番目まで見たときの
の次数とします。遷移は (5) です。
最後に求めたラグランジュ基底多項式の和を求めます。 足すだけなので です。
template< typename Mint > vector< Mint > lagrange_polynomial(const vector< Mint > &x, const vector< Mint > &y) { int k = (int) x.size() - 1; vector< Mint > f(k + 1); for(int i = 0; i <= k; i++) { Mint d = 1; for(int j = 0; j <= k; j++) { if(i != j) { d *= x[i] - x[j]; } } vector< Mint > dp(k + 1); dp[0] = y[i] / d; for(int j = 0; j <= k; j++) { if(i != j) { for(int l = k; l > 0; l--) { dp[l] = dp[l] * -x[j] + dp[l - 1]; } dp[0] *= -x[j]; } } for(int j = 0; j <= k; j++) { f[j] += dp[j]; } } return f; }
ふつう
のアルゴリズムです。一般的な多項式の復元はこのアルゴリズムを用いれば良いです。
かんたん のボトルネックは、ラグランジュ基底多項式の分子を求める部分でした。これを全体で で求まれば良さそうです。
前計算として を無視して、
の
の係数を求める DP をします。(4) を整理すると (6) になります。
したがって を取り除いた結果が求まればよいです。
多項式除算をすると log がかかりますが、戻すDPをすればさらに効率的に求めることが可能です。DP で取る順番を変えても計算結果を変わらないことは明らかです。また DP の遷移は (4) のように表されました。
これを について解きます。
これを用いることで 個要素を除いたときの DP を
で復元できて,各ラグランジュ基底多項式を
で求められます。
template< typename Mint > vector< Mint > lagrange_polynomial(const vector< Mint > &x, const vector< Mint > &y) { int k = (int) x.size() - 1; vector< Mint > f(k + 1), dp(k + 2); dp[0] = 1; for(int j = 0; j <= k; j++) { for(int l = k + 1; l > 0; l--) { dp[l] = dp[l] * -x[j] + dp[l - 1]; } dp[0] *= -x[j]; } for(int i = 0; i <= k; i++) { Mint d = 1; for(int j = 0; j <= k; j++) { if(i != j) { d *= x[i] - x[j]; } } Mint mul = y[i] / d; if(x[i] == 0) { for(int j = 0; j <= k; j++) { f[j] += dp[j + 1] * mul; } } else { Mint inv = Mint(1) / (-x[i]), pre = 0; for(int j = 0; j <= k; j++) { Mint cur = (dp[j] - pre) * inv; f[j] += cur * mul; pre = cur; } } } return f; }
むずかしい
のアルゴリズムです。 詳細は他の文献を参照してください。
分母 の列を効率的に求めるところから始めます。多項式
を (8) のように定義します。
は分割統治で
で求められます。 このとき、がんばると
が成立することがわかります。 多項式
について各
について評価された値を知りたいので、 multipoint evalution をすればよいです。このアルゴリズムの計算量は
でできることが知られていますが(知られていてくれ)、その解説は他の文献を参照してください(剰余の定理により
が
と等しいことが分かるので、多項式
をノードに持たせたセグ木状の subproduct tree を分割統治して作ります。その木を
を計算しながら潜っていくと、各葉でそれぞれの
の値を求めることができます。このときに、多項式のmodがNTTを用いて
で求まることを使うので、形式的冪級数のライブラリがあると良いです。 )
分母の列を で求められたので、次に分子の列及び
を掛けながら、多項式
の係数を求めます。多項式
を (9) のように定義します。
の場合は、(10) です。
そうでない場合は、 から
を求めることを考えます。これは
に
、
に
を掛けた上で足し合わせればよいです。
とすれば分割統治の計算量になって、掛けるところで畳み込みの log がかかるので
です。
実装例は https://judge.yosupo.jp/problem/polynomial_interpolation にあるんじゃないか
多項式の値
ある次数 以下の多項式
が存在することがわかっています。また、
個の
の組が与えられています。このとき、多項式の係数を求める問題ではなくて、
(
は大きめ) を求めたい場合、多項式の係数を陽に復元しないほうが
よりも効率的なアルゴリズムを導出できる場合があります。
が一般の場合です。
となり、
を値として求めれば良くて、係数は求めなくて良いです。
なのでこれに基づいて計算すると、逆元を求める計算量を
として
で求められます。
これを 本求めるので、全体で
となります。 この場合は係数を陽に求める場合と計算量は同じですが、定数倍が軽い気がします。 (
のギャグとの兼ね合いは分からん)
template< typename Mint > Mint lagrange_polynomial(const vector< Mint > &x, const vector< Mint > &y, const int64_t& T) { int k = (int) x.size() - 1; Mint ret(0); for(int i = 0; i <= k; i++) { Mint m = 1, d = 1; for(int j = 0; j <= k; j++) { if(i != j) { m *= Mint(T) - x[j]; d *= x[i] - x[j]; } } ret += y[i] * m / d; } return ret; }
ある多項式が存在して、その次数が 以下だと分かっていて
が小さい場合に、
まで DP で求めて、
(
が巨大) を多項式補間により求めるみたいな場面がよくあります(よくあってくれ)。 よくあるので、これに対応するライブラリを持っている人も結構多い気がします。
としたときのそれぞれの値
を効率的に求めたいです。
を観察します。 今回考える制約から
なので
です。
分子は です。前計算として先頭及び末尾からの
の累積積を計算しておけば
です。(これは
が一般でも適用可能)
分母は です。 これは
に対応します。前計算として階乗テーブルを求めておけば
です。
各 を
で求められるため全体で
です。
template< typename Mint > Mint lagrange_polynomial(const vector< Mint > &y, const int64_t& T) { int k = (int) y.size() - 1; if(T <= k) return y[T]; Mint ret(0); vector< Mint > dp(k + 1, 1), pd(k + 1, 1), finv(k + 1, 1); for(int i = 0; i < k; i++) dp[i + 1] = dp[i] * (T - i); for(int i = k; i > 0; i--) pd[i - 1] = pd[i] * (T - i); for(int i = 2; i <= k; i++) finv[k] *= i; finv[k] = Mint(1) / finv[k]; for(int i = k; i >= 1; i--) finv[i - 1] = finv[i] * i; for(int i = 0; i <= k; i++) { Mint tmp = y[i] * dp[i] * pd[i] * finv[i] * finv[k - i]; if((k - i) & 1) ret -= tmp; else ret += tmp; } return ret; }
が項数
の等差数列になっている場合です(初項が非
ならそれを引けば
の場合に帰着できます)。
について求めたい場合は、
を
で割って
の場合について解けば良いことがわかり、終わりです。差が一定であることを用いて直接導出することも可能です。
多項式の値たち(標本点のシフト)
陽にラグランジュ補間多項式を求めて、この多項式に対して、 つずつ
を求めると
、multipoint evalution をすると
で求まる気がします。(これいる?)
を仮定しています。
の場合は、その部分について既知の
の値を使うことで
の場合に帰着できます。
どちらかが単調ではない場合、先に示した遅め or 定数倍大爆発 アルゴリズムを使う必要があると思っています。
ここでは両方とも等差 の数列のときに、より効率的に求めるアルゴリズムを解説します。このアルゴリズムは、例えば階乗を
で求めるアルゴリズムに用いられていたりします。
例によって を観察します。
分母は で
に依存しないので、階乗テーブルを用いて
で求めておきます。 これらの値の逆数を並べた上で
を掛けた列を
とすると
の値は (11) により求められます。
これを整理します。
の値は
に対し、全体で
で求められます。
の値から
の値に移すときの差分を考えると、
を掛けることで求まることが確認できます。
したがって の値を各
に対して求めることが本質です。
、
とします。
これを見ると次の関係がわかります。
これは、求めたい値そのものです。そして、 と
を畳み込んだあとの
番目の要素に対応していることが分かります。畳み込みは NTT などを用いて
で求められます。
したがって、全体でも で求めることが出来ます。
下の実装例では となる部分で破滅するので、そこの場合分けもしています。
template< typename Mint, typename F > vector< Mint > lagrange_polynomial(const vector< Mint > &y, int64_t T, const int &m, const F &multiply) { int k = (int) y.size() - 1; T %= Mint::get_mod(); if(T <= k) { vector< Mint > ret(m); int ptr = 0; for(int64_t i = T; i <= k and ptr < m; i++) { ret[ptr++] = y[i]; } if(k + 1 < T + m) { auto suf = lagrange_polynomial(y, k + 1, m - ptr, multiply); for(int i = k + 1; i < T + m; i++) { ret[ptr++] = suf[i - (k + 1)]; } } return ret; } if(T + m > Mint::get_mod()) { auto pref = lagrange_polynomial(y, T, Mint::get_mod()-T, multiply); auto suf = lagrange_polynomial(y, 0, m - pref.size(), multiply); copy(begin(suf), end(suf), back_inserter(pref)); return pref; } vector< Mint > finv(k + 1, 1), d(k + 1); for(int i = 2; i <= k; i++) finv[k] *= i; finv[k] = Mint(1) / finv[k]; for(int i = k; i >= 1; i--) finv[i - 1] = finv[i] * i; for(int i = 0; i <= k; i++) { d[i] = finv[i] * finv[k - i] * y[i]; if((k - i) & 1) d[i] = -d[i]; } vector< Mint > h(m + k); for(int i = 0; i < m + k; i++) { h[i] = Mint(1) / (T - k + i); } auto dh = multiply(d, h); vector< Mint > ret(m); Mint cur = T; for(int i = 1; i <= k; i++) cur *= T - i; for(int i = 0; i < m; i++) { ret[i] = cur * dh[k + i]; cur *= T + i + 1; cur *= h[i]; } return ret; }
multiply は例えば、 ntt
を畳み込み用の構造体として、以下を渡せばよいです。
auto multiply = [&](const vector< modint > &a, const vector< modint > &b) { return ntt.multiply(a, b); };
信憑性
ここに示したコードは以下のいずれかでverify済みです。実装が上手いので多分あってますが間違ってたら燃やしてください。
まとめ
ねんね