Polaris.AI プログラミングコンテスト 2025(AtCoder Beginner Contest 429)
Polaris.AI プログラミングコンテスト 2025(AtCoder Beginner Contest 429)
次の言語アップデートで導入されるCodonについて調べていた矢先、早速、打って付けの問題が来た。
アップデート後に来てくれていれば。。。
F - Shortest Path Query
問題文
- $3$ 行 $N$ 列のグリッドが与えられます。
#ならば壁マスで、.ならば空きマスであり通行可能です。 - $Q$ 個のクエリが与えられるので、順に処理してください。
- 各クエリでは整数 $r,c$ が与えられるので、マス $(r,c)$ の状態を反転させてください。
- つまり、マス $(r,c)$ が壁マスならば空きマスにし、空きマスならば壁マスにしてください。
- その後、以下の問題の答えを出力してください。
- マス $(1,1)$ から上下左右に隣接する空きマスに移動する操作を繰り返してマス $(3,N)$ に移動することを考えます。
- このとき、マス $(3,N)$ に到達できるか判定し、到達できる場合は操作回数の最小値を求めてください。
制約
- $2\le N\le 2\times 10^5$
- $S_{i,j}$ は
#または. - $S_{1,1}=S_{3,N}=$
. - $1\le Q\le 2\times 10^5$
- $1\le r\le 3$
- $1\le c\le N$
- $(r,c) \neq (1,1),(3,N)$
- $N,Q,r,c$ は整数
解法
グリッド上の経路で「左に戻る」のが最短になるには、下のように最低5行必要になる。
3行しかないこの問題では、最短経路において左移動は発生しない。
...... #####. ...... .##### ......
つまり、セグメント木に「ある区間の最左○行目から最右○行目までの最短距離」を載せると、 マージの際は「左区間の右端→右区間の左端間の移動を、何行目でおこなうか」だけ気にすればよくなる。
■セグメント木に載せる情報
0: 単位元かどうか?
1: 最左1行目→最右1行目 2: 最左1行目→最右2行目 3: 最左1行目→最右3行目
4: 最左2行目→最右1行目 5: 最左2行目→最右2行目 6: 最左1行目→最右3行目
7: 最左3行目→最右1行目 8: 最左3行目→最右2行目 9: 最左3行目→最右3行目
...のそれぞれの最短距離(到達不能や、左右いずれかの該当マスが壁ならINF)
マージは、いずれかが単位元ならもう一方がそのまま採用される。
どちらも意味のある区間なら、左右の間で3行のどこを通過したかを全て試す。
通過する際に $+1$ が発生する点に注意。
セグメント木に載せる情報の idx:1~9 の $9$ 個を埋めるのに $27$ 回の計算で可能となる。定数倍が重め。
+1 たとえばマージ後の idx:1(1行目→1行目)を求めたければ、 ....##.. → .##...## 左1→1 → 1→1右 ###..#.# → ..#.#.#. 左1→2 → 2→1右 の3通りを試し、最小値をとればよい。 ...#.... → ....#... 左1→3 → 3→1右
高速化について
こういう、持たせる情報の数が多くなるセグメント木は、特にPython,PyPyは遅くなりがち。
なかなかTLEが取れず、Numbaで実装しなおしてようやく通った。
「Python で class を使ってライブラリ化しているデータ構造」は、 Numbaはいまいち使いづらい(jitclass はあるが、結局書き換えが必要だし、まだexperimental)ので、 書き換えになかなか踏ん切りがつきづらい。
なお Codon だと、細かな点でエラーが出たが(@classmethod や int.bit_length() が使えない)、 これらは少ない書き換え量で対応でき、コンパイルを通すことができた。class もそのまま使えるのは大きい。
それでも Python で通している人はいるもので、 テスターや、コンテスト中にACしている他の方の提出を見ると、 だいたいTLEした自分の方針と大きくは違わないようには見えるのだが、以下の傾向がある?
- タプルではなくリストで持たせる(参照渡しにならないよう注意)
- マージ時の各最小値の計算は、展開せず for 文で回す
タプルよりリストの方が、わずかながら生成が高速っぽい。
また、意外なのが、マージ時に各最短距離を求めるのに
return [
min(a[1]+b[1], a[2]+b[4], a[3]+b[7]) + 1,
min(a[1]+b[2], a[2]+b[5], a[3]+b[8]) + 1,
...
]
より、
c = [INF]*9
for i in range(3):
for j in range(3):
for k in range(3):
c[i*3+j] = min(c[i*3+j], a[i*3+k]+b[k*3+j]+1)
のような実装の方が、明確にPyPyで速くなった。
リストアクセスや、+1 の演算回数など、前者の方が少ないはずなんだけど。
やっぱりPyPyの挙動、非自明すぎる。。。

