素数modで問題を解くためのアルゴリズムは色々有名なものがありますが、合成数modだとうまくいかないものも多いです。
このようなとき、素因数分解によって素数modの問題に分解することができれば、中国剰余定理を用いて元の問題の解を復元できることがあります。(いわゆるCRT復元)
AtCoderではC++を使っている限りはACLにcrt
という関数が用意されていますし、今は多くの言語にACLが移植されていますから、同等機能の関数なりメソッドなりで事足ります。
しかし、ある程度理解できるようになると案外ソラ書きできるなと気づいてきたので、メモしときます。
中国剰余定理について
概要です。証明はしません。
各文字についての条件は以下の通りです。
- は正整数
- は非負整数
- すべてのについて
- は共通因数を持たない(すべての2つ組が互いに素)
これらの条件下で、, , …, をすべて満たす正整数を見つけたいとします。
中国剰余定理は、このようながの範囲で一意に定まることを主張する定理です。
合成数modで問題を解きたいが、そのために便利なアルゴリズムは素数modを要求している、という場合にこれが便利です。
素因数分解によって素数modの問題に分解し、それぞれが解をもつならば、中国剰余定理によって元の問題の解も復元できます。
あるいは、modを取れば効率的に求められるアルゴリズムがあって、これをmodを取らない場合の解に復元するのにも中国剰余定理が役に立ちます。
具体的には、いくつかの異なる大きな素数の法を設定して複数回問題を解きます。設定した法の総積が元の問題の解の上界を上回っているなら、中国剰余定理によって解を復元できます。
これらの解を復元する操作が俗にCRT復元とか言われるものです。
CRT復元のアルゴリズム
解きたい合同式は上記の通り何本あってもいいわけですが、2本の場合で解ければ繰返し適用するだけでよいので、合同式が2本ある場合のみを対象にします。
ここでまず、の場合、が解になります。
したがって、以下はとしてよいです。
また、一般にとしてよいです。
そうでないとき、2本の合同式を入れ替えることでにできます。
こんな感じの方程式はまずmodを使わない不定方程式に書き直します。
新しい整数を導入するとそれぞれ、
となります。2本の式では共通しているので、1本の方程式にまとめます。
のついている項は左辺に、定数項は右辺にまとめると、
ここまでわかると、2元1次不定方程式を解けばいいんだなとなります。条件からかつなので、これは必ず解を持ちます。
2元1次不定方程式をとく
結論、拡張ユークリッドの互除法を書けばいいんですが、なんとなく拡張ユークリッドの互除法ってを解くイメージがあるので、2つ目の項が負になってると微妙に混乱しちゃいます。
先ほどのについての不定方程式を眺めると、でmodとってもよさそうだなと思えます。
であってですから、はです。
ところでなので、は逆元を持ちます。
したがって、
うん。まあそれはそうかって結果なのですが、なんか見慣れた形になるので、安心して計算できます。
を求める
が素数ならみんな大好きフェルマーの小定理でなんとかできますが、今回はそういう制約ではないのでダメです。
結局のところ、拡張ユークリッドの互除法でなんとかします。
拡張ユークリッドの互除法をソラ書きするのは、noshiさんのブログを見るのが覚えやすいです。
以下の2本の自明な恒等式を考える、ということだけ覚えればよいです。
この式の両辺で、左辺でGCDを取る操作と同じ操作をすることを考えます。
をで割った余りをとるのはと同じなので、下の式の両辺にをかけて上の式から引けばよいです。
これを繰り返すと、最終的に片方は左辺がになり、そうでないほうは以下のような形になります。
条件からなので
と同じで、両辺で考えれば、
となり、結果、です。
実装例は以下のようになります。
個人的には、再帰的なルーチンよりもこっちのほうが素直でわかりやすいと思います。
意味はないですが、一応上の式で言うところのがでのの逆元になっていることも確認しておきます。
ちなみに、もっとデカい数字でやると乗算でオーバーフローすることはないのかというのが心配ですが、しないらしいです。
なぜかはまだ理解してないので、とりあえず今はそれを信用することにしておきます。
CRT復元の実装の続き
はわかったので、これをつかって
を求めます。
なんかこれで満足しちゃいそうですが、求める対象はだということを思い出します。
求めたを使うと、は以下のように表せます。
ところで、ですが、だとすると、
なので、
となります。よって、を計算する時点で正しくに収まっていれば、求めたをそのまま使ってよいことがわかります。
したがって、
を計算することでが求まります。
実装例は以下の通りです。
Wikipediaの「中国の剰余定理」のページに載っていた例ほぼそのままですが、上手くいってそうでしょうか。
の場合
ここまで、の場合のみを考えてきましたが、そうでない場合にも拡張ができます。
主張や証明についてはけんちょんさんのQiitaの記事とか高校数学の美しい物語とか見るのが良いと思いますが、雑に言えばの総積ではなくて、すべてののLCMまでの範囲で一意にを決めることができるようになります。
ただし、すべてのが互いに素な場合と異なり、解が存在するための条件が加わります。
また2つの合同式から、1本の方程式にまとめます。
ここで、なので、それをと置くと、左辺はでくくることができます。
したがって、右辺がの倍数でない場合には解がありません。つまるところ、
が解が存在する条件となります。
とすると、先ほどの方程式は
となり、ですから、これは先ほどと同じように解けます。
となります。
さて、ここからを用いての具体的な値を求めたいですが、はどのような値であるべきでしょうか?
とりあえずはとしておきます。
ところで、少なくとも、以下の等式は正しいことがわかります。(は適当な整数)
両辺にをかけると、
これをで評価すると、
です。
これを用いて、の右辺にの値を代入した結果がを満たすか確認します。
まず、で評価すると、の項が消えるので、条件を満たします。
次に、で評価すると、
ここで、なので、さらに変形すると、
となり、条件を満たします。
よって、として問題なく、が解となります。
に収まっているかどうかについては、であることからわかります。
実装例は以下の通りです。
ext_gcd
がGCDを追加で返すように関数定義を変更し、GCDが1でなくてもパニックしないようにassert!
を削除しました。
こうして返されたGCDが1より大きいか否かで処理を分岐しています。(実際はよく見るとわかる通り、ルーチンを分岐する必要はない。)
crt(1, 8, 5, 12)
で、8と12は互いに素ではないですが、ちゃんとLCMである24までの間に満たす値を発見して返しています。
まとめ
最初はソラ書きすることが目的だったんですが、だんだん話がソラ書きとは関係ない方向にそれたので、単に書き方の学習メモになりました。
一応、上記全部ソラでなんとか書けるようになりましたが、以下のことを最低減覚えておけばその場でアルゴリズムが導出できるとわかりました。
ext_gcd
の書き方:2本の恒等式を書くことと、左辺でGCDを取る操作をすることだけ覚えればよいです- 合同式と等式の変換:法を何か整数とかけて足せば合同式は等式になるし、2元1次不定方程式はどっちかの係数でmodを取れば合同式にできます
あとは出てきた式をごにょごにょいじり倒せば、その場で何とかできます。
とはいえ、なんとなく雰囲気で流していた部分もあるので、記事書きながら理解を深められて良かったです。