P-recursive sequenceの第N項の平方分割計算アルゴリズムの理解ログ
これ↓ を理解するまでのログです。
https://nyaannyaan.github.io/library/fps/find-p-recursive.hpp.html
問題定義
扱う数は素数 を法とした剰余環 とし、以降は単に整数・数と呼びます。
求めたい数列を とし、最終的には を求めます。
数列 は、整数を入力とする高々 次の多項式 個の列 を用いて、以下の関係が成り立ちます。
行列積としての表現
線形漸化式における行列累乗のような考えで、この関係式を行列積として表現し直します。
まず関係式を少し整理します:
よって、 から に変換するには、行列
を用いて、 と書けます。
求めたいものは 、つまり の最初の要素なので、 を求めようと思うと、( と定義して)
が計算できれば良いです。(行列積が非可換であるため、総乗は逆順である点に留意します。逆順総乗は記法あってるか分かりませんが雰囲気で……)
なお、ここでは としても関係式が成り立つとしていますが、そうでなければ と適切にシフトした数列について考え、 として を求めることを考えれば良いです。
さて、分母については 行列 の総乗だと思うと、これは で とした場合と構造としては同じになります。よって を計算するアルゴリズムがあれば、これを分母・分子それぞれに1回ずつ適用することで が求まります。
(天下り的な)平方分割の構成
行列同士の積の計算量を とします。
と定義します。これは 個の行列の積です。見通しの良さのため半開区間を用います。
ブロックの分割数 を とします。この数字は調整可能です。
ブロックの幅 を とすると なので、 個の「 個の行列積のブロック」に分けます。
前半にかかっている は より なので愚直に計算すれば です。よって後半の 個の総乗 を高速に求められれば良いです。
ここまでの図はこんな感じです(説明用の例: N=22, B=5):
この各 を、以下の図のようなイメージのダブリングによってまとめて計算します(説明用の例: N=22, B=5, d=1):
このダブリングをうまく動かすには、これらの値(行列積)がある多項式によって表現されていることを意識する必要があります。
行列積の多項式表現
乗算される行列の多項式表現を再掲します:
例えば は に を代入した時の行列です。
では について考えると、これも とすれば良いですが、ここであえて とシフト(変数変換)した行列 を考えます:
すると、 の各要素も高々 次の多項式で、また です。
このようにシフトされた行列表現 を考えると、「ある多項式行列の積の の標本点」と表現し直すことが可能です:
多項式としての行列積にも表記を与えておきます:
シフトされた行列 の各要素は高々 次の多項式だったので、その積である行列 はの各要素は高々 次の多項式になります。
(ここまで次数を執拗に確認していますが、(上界の)次数のみが重要で、実際にどのようなシフトをしてどう展開され係数がいくつになるか、ということには興味が無い点に留意してください。)
また から とシフト出来るように、 から とシフトすることも可能です。これを利用すると、 についてもシフトが可能です:
これを考えると、例えば となります。
ダブリングの構成
さて、行列積の多項式表現と、そのシフトを考えながら図の例に戻してみると、要はこういった構造になっています:
つまり、各ステップで という多項式を考え、その各点の標本点(行列)を管理する、という形になっています。 の各要素は高々 次の多項式なので、 個の標本点を管理すれば、多項式としての情報量を損ないません(ref: ラグランジュ補間)。
よって作られるダブリングアルゴリズムは以下の通りで、最終的に の での標本点が分かれば良いです。
- 幅 の行列積 の での標本点 が求まっていると仮定する。
- 標本点の数は 個。
- のケースは愚直に 個の行列を評価をすることで計算可能。
- 幅を1増やすには:
- の場合: 各標本点 に単純に行列を掛ける。
- 回の行列評価・行列乗算
- の場合: 単純に行列を掛ける。
- 回の行列評価、 回の行列乗算
- (動作例: 次数 d=2。w=1 (標本点3個) から w=2 (標本点5個) を w+=1 で作る)
- の場合: 各標本点 に単純に行列を掛ける。
- 幅を2倍に増やすには:
あとは実装するだけです。
多項式の標本点シフト
標本点シフトは以下の問題を解くアルゴリズムです:
- 高々 次の多項式 の標本点 が与えられる
- 整数 が与えられるので、 を求めよ
行間埋めは以下が非常に参考になります:
https://mathlog.info/articles/3351
最終的に3回のFFT畳み込みによって で高速に計算が可能なことが分かります。
本問題は library-checker-problems (yosupo judge) に収録されているので、具体的な実装はその想定解ファイルを読むのが一番簡便かと思われます。
今回の問題では という形の標本点になっているので、そのまま使うことは出来ません。
とすると、 となります。
として管理をした場合、標本点シフトは と考えられ、結局 に関して のシフトを実行すれば良いことになります。
残り
なお、「最終的に必要なブロックの数は 」「幅 の行列積の多項式の次数は で、必要な標本点は 」ということを考えると、ダブリングの結果が余ったり足りなかったりします。
- 余るケース
- 何も考えずに、最終的に捨てれば良いです。
- 足りないケース
- 回の標本点シフトによって標本点の数を にすることが出来ます。
だったことを思い出すと、この条件は以下と等価です:
- 余るケース
- (が成り立つならば余る)
- 足りないケース
- (が成り立つならば足りない)
特に の時、
が成り立つので、 と設定することで、足りないケースは回避することが出来ます。
(もちろん実装時には不等式 を満たす最大の を全探索/二分探索で求めた方が効率的です。ここではオーダー把握のため簡易な形式を採用します。)
計算量
最後に、計算量の見積もりを行います。
ある行列 を評価するのに必要な計算量は、多項式が一番上の行と対角線一個下にしか無いことに注意すると、 です。
行列の乗算に必要な計算量を と置いていました。ここではシンプルに乗算するとして ということにします。
を求める計算量を と置きます。
です。
が奇数の場合は に再帰します。
です。
が偶数の場合は に再帰します。
です。
が高々2ステップで になることを考えると です。
よって です。
とすればこのあとに追加で評価点シフトする必要も無いです。
あとは 個の と高々 個の を乗算することで、 が求まります。
この行列積を求める総計算量は
となります。 とすれば で、 を思い出すと です。
(なお、NTT-friendly でない素数の場合、畳み込みに garner's algorithm 等を用いた任意mod畳み込みを使用する必要があります。garner's algorithm を用いる場合 での逆数計算が必要になるため、 になります。)
その他
- 本アルゴリズムでは「たかだか 次の多項式は 個の標本点を保存すれば良い」という性質を用いて、標本点の数自体も幅 と一緒にダブリングしていますが、最初から最終的な多項式の次数 に対応した 個の標本点を持ち続けるアルゴリズムも設計可能です。
- この場合、標本点の数が最初から最後まで なので、段数がかかって管理する標本点の総数が になります。
- 標本点の数自体もダブリングする方式では級数を考えることで、標本点の総数が になるので、この分の が落ちることになります。
- また、多項式 を陽に求め、多点評価によって を求めるアルゴリズムも設計可能です。
- 等比数列でない多点評価は のため、やはり が1個多くなります。
- ref: https://37zigen.com/multipoint-evaluation/
後記
- お久しぶりです
- また の記事書いてる……
- 関係ないですがTTPC良かったですね。僕はTTPC upsolveに睡眠時間を削られています。来年はTTPC(という名前のコンテスト)が開催されるのでしょうか……
- P-recursive がちょうど流行り始めてそうな2020年~2021年頃にぬるっと引退したので存在を知らず、すげ~となった
- でも 自体はもっと昔から存在はしていたらしいので、単純に精進が足りなかっただけでもありそう
- 雑に Berlekamp-Massey に投げる感覚で P-recursive 判定に投げる世界、ヤバい
- 階乗を で解けるのだいぶ驚きだったんだけど、 はやばそうだな……という感覚がある
- 素因数分解が解けてしまうので……
- と思ったらそれが Strassen’s Factoring algorithm だった
- FPSの文脈で出がちなイメージだけど、おそらくコレ自体は”FPS”とは関係ない
参考文献
- https://atcoder.jp/contests/abc222/editorial/2742?lang=ja
- https://github.com/noshi91/n91lib_rs/blob/master/src/algorithm/polynomial_matrix_prod.rs
- noshi91さんによるRustでの実装例、上記ページからのリンク
- https://nyaannyaan.github.io/library/fps/find-p-recursive.hpp.html
- https://nyaannyaan.github.io/library/matrix/polynomial-matrix-prefix-prod.hpp
- NyaanさんによるC++での実装例
- https://nyaannyaan.github.io/library/modulo/factorial.hpp.html
- Nyaanさんによる n! mod p を高速計算するアルゴリズムの実装例と解説
- これ自体は P-recursive の解説ではないが、本質的には同じことをしている
- 現存している日本語資料では一番詳細……な気がする
- Bostan, A., Gaudry, P., & Schost, É. (2007). Linear recurrences with polynomial coefficients and application to integer factorization and Cartier–Manin operator. SIAM Journal on Computing, 36(6), 1777-1806.
- おそらく原典はこれ
- https://qiita.com/ryuhe1/items/da5acbcce4ac1911f47a
- ryuhei moriさんによる線形漸化式関連の記事
- おまけで P-recursive の高速計算アルゴリズムに関する言及がある
- ここでこの高速計算が baby-step giant-step という言及(?)がある
- Strassen’s algorithm (素因数分解) まで遡ると、 オーダーのブロックごとの階乗冪を多点評価で求めてざっくりgcdで判定するパートが “giant step” で、そのブロックの中を一個ずつ検算するのが “baby step” とのことだった (3乗根オーダーの素数が混じっていると gcd が素数にならないことがあるので)
- この意味合いを継承して、ブロックごとの行列積の多点評価パートが “giant step”、その具体的なブロックの多項式を求めるパートが “baby step” と呼ばれている……?
- おそらくそれが「その他」に書いたバリエーションで、今回調べたアルゴリズム自体はそれを更に改良したもの(結果としてbsgsのフレームワークから大きく外れているように見える)……という感じ?