AtCoder を攻略していく上で、 に慣れることは大きな課題です。特に、有理数 は、初見では何を言っているか分からないと感じる方も多いかもしれないですね。下図は ABC 323 E - Playlist の補足を表示したものです1。
本記事では、有理数 を徹底解説します! なお、この記事は競プロアドベントカレンダー 5 日目の記事として執筆しています。
1:前提知識
まず、そもそも「 で割った余りを求めよ」という形式そのものに不慣れで、意味がわからないと感じる方は、先に次の記事を読んでもらえたらと思います。
最低限必要な知識を超特急でおさらいすると......
- を で割った余りを求めたいときは、先に を で割った余りに置き換えて、それらを足して、改めて で割った余りを求めてもよい
- を で割った余りを求めたいときは、先に を で割った余りに置き換えて、それらをかけて、改めて で割った余りを求めてもよい
ということを確認しておきましょう。このように、「計算したい各値について、先に で割った余りを求めてから、計算しても結果は変わらない」という事実はとても重要です。
また、本記事では合同式の知識を前提としています。合同式について知りたい方は、ぜひ次の記事を読んでみてください。
2: とは何か
それでは、いよいよ とは何かを紐解いていきましょう!!
2-1:そもそも分数とは何か
そもそも分数って何でしょうか。分数は「割り算」です。たとえば、mod のことは一旦忘れて、
が成り立ちます。それでは「割り算」とはなんでしょうか。割り算は「かけ算の逆」です。 とは、 となるような のことですね。mod の世界でも一緒です。まとめると、
とは、
を満たすような のこと
です!!!(ここでは を文字 で表しました)
有理数 とは、要するに「一次方程式の解」なのですね。それでは、一次方程式はどうやって解いたらよいでしょうか。
再び mod の世界ではなく、普通の方程式をどのように解いたかを思い出しましょう。たとえば、一次方程式 を解くためには、 の逆数である を両辺にかけることで解けます。他にも、行列の方程式 を解くためには、 の逆行列である を両辺にかけることで解けます。
2-2:逆元
mod の世界でも同様です。mod の世界でも、普通の数の世界における「逆数」に対応する概念として、「逆元」という概念があります。 における の逆元とは、「 にかけて 1 になる数」のことです。数式で書けば、
を満たす のことです。逆元は と表記することが多いです。つまり、
となります。いくつか具体例を見てみましょう。
において、
となります。1 個目の「 の逆元」は明らかですね。 ですので、 です。2 個目の を確かめてみましょう。 と をかけると、
となります。確かに の逆元は であることが見て取れます。同様に、 の逆元についても、ぜひ確かめてみてください!
なお、実際のプログラムで逆元を求める方法を知りたい方は、次の記事を読んでみてください。
そして、AtCoder では、公式ライブラリ AtCoder Library (通称 ACL) が提供されていて、その中に modint
と呼ばれる構造体があります。modint
は、mod を指定して (たとえば )、四則演算や逆元計算などを直感的に操作できるようにしたものです。たとえば、 における、1 から 10 までの数の逆元は、次のように関数 inv()
を用いて簡単に計算できます!!
#include <bits/stdc++.h> #include <atcoder/modint> using namespace std; // mod 998244353 での演算を行う構造体 using mint = atcoder::modint998244353; int main() { for (int a = 1; a <= 10; ++a) { // a の逆元を求める mint ra = mint(a).inv(); // a の逆元を出力する cout << a << "^-1 = " << ra.val() << endl; } }
実行結果は次のようになります。
1^-1 = 1 2^-1 = 499122177 3^-1 = 332748118 4^-1 = 748683265 5^-1 = 598946612 6^-1 = 166374059 7^-1 = 855638017 8^-1 = 873463809 9^-1 = 443664157 10^-1 = 299473306
2-3:分数 を解き明かす!
それではいよいよ、 を実際に求める作業に入ります。例として、 を求めてみましょう。これは、先ほどの話を踏まえると、
一次方程式 の解
なのでした。そして、一次方程式を解くためには、両辺に逆元をかければよいのです。今回は を両辺にかけます。そうしますと、 なのですから、
と求められます! こうして、私たちは、 のような分数 (確率や期待値など) を の世界で、整数値として表現する方法を得たのでした!!!
なお、ここまで分数を「一次方程式」を経由して解説したのですが、もっと簡単に
と表すこともできます。この計算は、普通の数の計算と一緒ですので、とても直感的で分かりやすいと思います。例の ACL の modint
においても、割り算の演算子「/=
」は次のように実装されています2。まさに、上の計算式そのものを実装していることが分かります!
mint& operator/=(const mint& rhs) { return *this = *this * rhs.inv(); }
つまり、まとめると「割り算とは逆元をかけることである」と一言で言えるわけですね。ACL を使って の値を求めてみましょう。次のコードのように書けます。
#include <bits/stdc++.h> #include <atcoder/modint> using namespace std; // mod 998244353 での演算を行う構造体 using mint = atcoder::modint998244353; int main() { mint b = 5; mint a = 3; mint answer = b / a; cout << answer.val() << endl; }
実行結果は次のようになります。
665496237
不安な方は、同じように b * a.inv()
の値も出力してみてください。きっと同じ値が返ってくるはずです!
補足 (1):逆元の存在と一意性
逆元について、こんなことが疑問に浮かんだ方もいるかもしれません。
- における の逆元は常に存在するのだろうか?
- における の逆元は複数個存在することもあり得るのだろうか?
しかし、実は が の倍数でなければ、 の逆元はただ 1 つだけ存在するのです。ちゃんと書くと、次の定理が成り立ちます!
を素数として、 を の倍数ではない整数とする。このとき、
を満たす整数 () がただ 1 つ存在する。
とても美しい定理ですね!!!! は素数ですから、 の逆元はそれぞれ 1 個ずつ存在するのです。
まず、 を で割った余りが重複しないことを証明します。そのために、 が成り立つような () が存在すると仮定して矛盾を導きます。仮定の式を変形すると、 となります。つまり、 は の倍数となります。ここで、 は の倍数ではないため、 は の倍数であると言えます。しかし、 ですから、 であり、 は の倍数とはなり得ません。よって、 を満たす () が存在するという仮定が誤りであることが分かりました。 以上より、 を で割った余りは互いに相異なります。特に、 を満たす整数 () は存在したとしてもただ 1 つしかないことが言えました。さらに、もし仮に、 を満たす () が存在しないと仮定すると、 を で割った余り ( 個あります) は のいずれか ( 種類です) となります。鳩の巣原理より、 を で割った余りの中に重複があることになります。これは矛盾です。 以上より、 を満たす整数 () はただ一つ存在することが証明されました。 (証明終わり)
逆元の性質の証明
証明
補足 (2):Fermat の小定理
せっかく美しい定理が示せたので、さらに一歩補足してみます!
を素数として、 を の倍数ではない整数とする。このとき、 を で割った余りを並び替えると となる。 実際、 として、1 から 6 までの数同士をかけざんして九九もどきを作ると下図のようになります。
2 の列に着目すると、 となっています。これは、 として を で割った余りを並び替えると となることを表しています。 この性質を利用すると、かの有名な定理が証明できてしまいます!! を で割った余りを並び替えると になるということは、 が成り立つことを意味します。整理すると となります。 と は互いに素なので、両辺を で割ってよくて となります。これは Fermat の小定理です!! なお、Fermat の小定理は、 の逆元を計算する方法をも示唆しています。Fermat の小定理の式を変形すると となります。先ほど、まさに逆元の一意性は示したので、次のことが言えます! つまり、 を計算することで逆元が求められます。実際、ACL の ただし、逆元を Fermat の小定理で求める方法は、mod が素数の場合にしか通用しません。合成数である場合 (Fermat の小定理について
modint
においても、逆元を求める関数 inv()
は次のように実装されています。このコードで pow(umod() -2)
の部分がそれを表しています。mint inv() const {
if (prime) {
assert(_v);
return pow(umod() - 2);
} else {
auto eg = internal::inv_gcd(_v, m);
assert(eg.first == 1);
return eg.second;
}
}
else
文の中身) は、別の方法で逆元を求めています。
補足 (3):なぜ mod をとるのか
そもそも根本的な疑問として、確率や期待値の値を double
型で計算するのではダメなのだろうか......とも思われるかもしれません。実際、そのような形式で出題される問題もあります。
しかし、double
型で確率や期待値を求めると、誤差の問題が常につきまといます。一方、 で求めた整数値を答えさせると、誤差の問題はまったくありません。出題者も安心して出題できますし、参加者も安心して回答できますね。
3:分数の足し算、引き算、かけ算、割り算
さて、次のステップに行きましょう!!!
3-1:分数の足し算
早速ですが、次の問題を解いてみてください。いずれ、ABC-D などで出題されるかもしれません。
例題 (1)
正の整数 が与えられるので、
を計算してください。ただし、計算結果を と表したとき、その値を で出力してください。
制約
解法 1
この問題を読んで、次のように考えた方もいるかもしれないですね。通分すると、
となりますので、ACL の modint を用いて (b * c + a * d) / (a * c)
の値を出力すれば解けると。つまり、次のように実装できます。
#include <bits/stdc++.h> #include <atcoder/modint> using namespace std; // mod 998244353 での演算を行う構造体 using mint = atcoder::modint998244353; int main() { int ia, ib, ic, id; cin >> ia >> ib >> ic >> id; mint a = ia, b = ib, c = ic, d = id; mint bunshi = b * c + a * d; mint bunbo = a * c; mint answer = bunshi / bunbo; cout << answer.val() << endl; }
解法 2
もちろん、上の解法 1 は正解です!!
......が、 の式変形の計算を手元でやるのはミスのリスクがあります。実は、もっと簡単に次のコードでよいのです!
#include <bits/stdc++.h> #include <atcoder/modint> using namespace std; // mod 998244353 での演算を行う構造体 using mint = atcoder::modint998244353; int main() { int ia, ib, ic, id; cin >> ia >> ib >> ic >> id; mint a = ia, b = ib, c = ic, d = id; mint answer = b / a + d / c; cout << answer.val() << endl; }
どちらのコードを用いても、たとえば とすると、答えは となります。ぜひ確かめてみてください!
補足: の計算について
上の解法では、
としたときに、
が成り立つということを証明なしで使いました。一応これを証明しておきましょう。一見すると、整数 に対して かつ ならば が成り立つことから自明だと思われるかもしれません。しかし、 や は整数ではないので、証明が必要ですね。
であるから、 を示すためには、 であることを示せばよいです。ここで、, より、 が成り立つことに着目して、 が成り立ちます。以上より、示せました。 (証明終わり)
証明
証明
3-2:分数のかけ算
足し算に続いて、かけ算もやってみましょう!
例題 (2)
正の整数 が与えられるので、
を計算してください。ただし、計算結果を と表したとき、その値を で出力してください。
制約
解説
足し算の場合と同様に、次のコードのように、式をそのまま実装すればよいと期待できます。実際、これで正解です!!
#include <bits/stdc++.h> #include <atcoder/modint> using namespace std; // mod 998244353 での演算を行う構造体 using mint = atcoder::modint998244353; int main() { int ia, ib, ic, id; cin >> ia >> ib >> ic >> id; mint a = ia, b = ib, c = ic, d = id; mint answer = (b / a) * (d / c); cout << answer.val() << endl; }
補足: の計算について
上の解法では、足し算の場合と同様に、
としたときに、
が成り立つことを使っています。一応、これを証明しておきましょう。
であるから、 を示すためには、 であることを示せばよいです。ここで、, より、 が成り立つことに着目して、 が成り立ちます。以上より、示せました。 (証明終わり)
証明
証明
3-3:分数の四則演算のまとめ
ここまで、分数の足し算と引き算について見てきました。同様のことは引き算や割り算でも成り立ちます。まとめると、次のことが言えます。
を の倍数ではない整数として、
とする。このとき、
が成り立つ。
こうしてみると、 における分数計算は、本当に直感的にできることが分かりますね!
4:実際の確率計算の例
ここまでに得た知見をもとに、実際に簡単な問題を解いてみましょう。
例題 (3)
から までの目が等確率で出るサイコロがあります。このサイコロを 3 回振るとき、目の最大値が である確率を求めてください。
ただし、確率を と表したとき、その値を で出力してください。
制約
解法
目の最大値が である確率は、次のように求められます。
(3 個のサイコロの目がすべて 以下である確率)
- (3 個のサイコロの目がすべて 以下である確率)
最大値が であるということは、すべての目が 以下であることが必要です。しかし、 の目が存在しなければなりません。そこで、 の目が存在しない場合......つまり、すべての目が 以下であるような場合の確率を引くのです。そして、
- 3 個のサイコロの目がすべて 以下である確率:
- 3 個のサイコロの目がすべて 以下である確率:
であるから、求める確率は次のようになります。
あとは、これを実装すればよいです。modint を使えば、次のように、上の式をそのまま直感的に実装できます!
#include <bits/stdc++.h> #include <atcoder/modint> using namespace std; // mod 998244353 での演算を行う構造体 using mint = atcoder::modint998244353; int main() { int N, A; cin >> N >> A; mint p = mint(A) / N; mint q = mint(A-1) / N; mint res = p * p * p - q * q * q; cout << res.val() << endl; }
5:おわりに
ここまで、有理数 について解説してきました。確率や期待値のように「分数で表したいもの」を求めるときに、時々そのような形式で出力することを要求されることがあります。
しかし modint
さえあれば、結局まったく難しいことはなく、数式をそのまま直感的に実装すればよいのです!