AtCoder Regular Contest 046 D - うさぎとマス目 解説
なんか全然公式解説と違うことしてました……。
問題リンク
https://atcoder.jp/contests/arc046/tasks/arc046_d
問題概要
行 列からなるグリッドグラフがあります。
このグラフにおいて、 から または に移動できます。
マス からスタートして全てのマスを通り、 に戻ってくるような経路の数を で数え上げてください。
制約
考察・解法
とりあえず実験してみます。問題の図のように矢印で考えるのではなく、下のようにパスを繋ぐようにして考えてみます。
ここで、 から への移動を 行目の縦のワープ、 から への移動を 列目の横のワープと呼ぶことにします。
の場合
重要な観察として、以下のような命題が成り立ちます。
のとき、 列目の横のワープと 行目の縦のワープは共存しない。
証明
これは 列目以降かつ 行目以降の の正方形について、上・左からはパスが高々 本しか入れないのでどのようにしてもこの正方形を全部通るようなパスはありえない、というように示せます。
縦横のワープする回数は合計でちょうど 回である。
証明
- 回未満でないことの証明
右・下にしかいけないので、下のような敷き詰め方を考えると最低でも 個の (ワープのない) パスが必要です。
- 回よりも大きくないことの証明
先程の命題から直ちに導かれます。
少なくとも 回以上縦・横のワープをそれぞれ含む
証明
全部縦・横どちらかのワープであるときを考えればありえないことは自明です。
これより、 について、ワープの位置が縦か横かを か所について決め、縦横のワープどちらも含むようにすることが必要条件と分かります。
このようなワープの位置の取り方について、以下のように順番に繋げば一意にワープの位置からパスを構築出来ます。証明は略しますが、このパスが全マスを通ること (正当性) および一つしかありえないこと (一意性) は簡単に示せるでしょう。
これで正方形のマスを全て通るパスが出来ましたが、実際にはスタート地点から動くと全てのマスを通らないような場合があります (実際、上で示したような場合はこのように半分のパスしか通りません)。
さて、全てのパスを通る必要十分条件は次のようになります。
である。ただし、 をそれぞれ横・縦のワープの個数とする
証明
とします。
上・左辺の右上から 番目と右・下辺の右上から 番目のワープ地点どうしが結ばれています。
ここで、 はどちらも で割り切れるので、どの 行目の横のワープも、 列目の縦のワープも、 の値は同じです。
よって、 のときは の値が同じようなワープ地点で 個のループを作ってしまいます。
以上より、 が全てのパスを通る必要十分条件です。
以上より、 であるときの経路の個数は、 と互いに素な についての の総和です。
の場合
下図のように、ある正方形の経路を敷き詰めたものも (スタート地点から始めると全てのマスを通るとは限りませんが) 全マスを通るパスになります。よって、一辺 の正方形のパスを 個 (ただし とした) 敷き詰めたパスは においても valid です。
逆に、一辺 の正方形に切り分けた時にパスが作れない正方形が現れる場合はどのようにしてもパスは構築出来ません。縦横どちらかのワープ位置を決めればもう片方は一意に定まるので、 の正方形繰り返し以外ではありえないことが分かります。
ここで、一辺 の正方形の横・縦のワープ位置の個数を とすると、全体のワープ位置の個数はそれぞれ です。
正方形のときと同様に、 がこのパスがスタート地点から始めて全マスを通る必要十分条件です。よって、答えは として、
を満たす全ての について、 の総和
です。これは のときも正しいことが確かめられます。
また特に、 を満たすとき答えは です。
実装
実装を展開する
ModInt[] factorial, finv; public void Solve() { var (h, w) = sr.ReadValue<int, int>(); var gcd = GCD(h, w); Init((int)gcd); ModInt ans = 0; for (int i = 1; i < gcd; i++) { if (GCD(h / gcd * (gcd - i), w / gcd * i) > 1) continue; ans += Binom((int)gcd, i); } Console.WriteLine(ans); } public ModInt Binom(int n, int r) => factorial[n] * finv[r] * finv[n - r]; public void Init(int max) { factorial = new ModInt[max + 1]; finv = new ModInt[max + 1]; factorial[0] = 1; finv[0] = 1; for (int i = 0; i < max; i++) { factorial[i + 1] = (i + 1) * factorial[i]; finv[i + 1] = factorial[i + 1].Inverse(); } } public static long GCD(long a, long b) { a = Math.Abs(a); b = Math.Abs(b); while (a > 0) { b %= a; var x = a; a = b; b = x; } return b; }
ACコード: https://atcoder.jp/contests/arc046/submissions/23113841
感想
公式解説の方が圧倒的に分かりやすく、こちらのほうが (同値ではありますが) 若干考察のステップが多く、外れ方針といえるでしょう。証明も面倒です。
未証明でも普通に通ったので証明 AC って感じでした。AC してからこの解説のように厳密な証明を与えました。