トヨタ自動車 AtCoder Beginner Contest 348 に参加した感想の続き(4問目)です。

問題の内容

D – Medicines on Grid

H 行 W 列のグリッドがあります。上から i 行目、左から j 列目のマス目を (i,j) と表します。各マスの状態は文字 A_ij で表され、意味は以下の通りです。

. : 空きマス。
# : 障害物。
S : 空きマスかつスタート地点。
T : 空きマスかつゴール地点。

今いるマスから上下左右に隣り合う空きマスへ移動することができますが、そのときはエネルギー 1 を消費します。エネルギーが 0 の状態では移動できません。またグリッドの外へ移動することもできません。

グリッドには合計で N 個の薬があります。i 番目の薬は空きマス (R_i ,C_i) にあり、使うとエネルギーを E_i にすることができます。現在のエネルギーが E_i に変更されるのであって必ずしもエネルギーが増えるとは限らないことに注意してください。自分のいるマスに薬がある場合、使うこともできるし使わないこともできます。使った薬は当然のことながらなくなります。

高橋くんが最初はエネルギー 0 の状態でスタート地点にいます。上記の条件下でゴール地点まで移動できるかどうか判定してください。

入力
H W (1 ≦ H, W ≦ 200)
A_11 A_12 A_13 … A_1W
A_21 A_22 A_23 … A_2W
A_31 A_32 A_33 … A_3W
… …
A_H1 A_H2 A_H3 … A_HW
N (1 ≦ N ≦ 300)
R_1 C_1 E_1
R_2 C_2 E_2
R_3 C_3 E_3
… …
R_N C_N E_N

障害物がある迷路があってスタート地点からゴールまでいけるか? そのときの最短経路は?という問題はよくあります。ゲームをつくっているとこういう問題と似たことをよく考えさせられます。

ちょっと特殊なのが「途中に落ちている薬を使ったら体力が回復する」のではなく「体力が強制的にその値になる」「薬は使ってもいいし使わなくてもよい」「体力が尽きたら動けなくなる ⇒ 失敗」という部分でしょうか?

鳩はこう考えて爆死した!

通れるところ、通れないところを表す二次元配列をつくる。
別の二次元配列をつくってそこに高橋くんのエネルギーを記録する。
薬は使ったらなくなるので存在するかどうかを表す二次元配列をつくる。

二次元配列がたくさんできてしまったが、あとは幅優先探索で実際に高橋くんを移動させてみる

壁や配列外、エネルギーがなくなったときは移動できない
最初はエネルギー0なのでスタート地点に薬がない場合はゴールには絶対にたどり着けない
薬がある場所に移動した場合は体力が薬をとることで増加する場合だけ取る。それ以外は無視

このように考えて以下のようなコードを書いてみました。このコードは間違っています。なのでなんの参考にもなりません。

これでテストが通るのかというと以下の2つは通ります。

期待どおり ‘Yes’ と出力されます。

これも期待どおり ‘No’ と出力されます。

しかし以下の場合、テストがとおりません。’Yes’ を期待しているのですが実行すると ‘No’ と出力されます。

最短コースでゴールに向かおうとするとエネルギーが足りなくなるので、いったん逆方向に移動してそれから元きた道を戻って来るルートを通ることでゴールすることができる場合があります。ところが上記のコードでは元きた道を戻って来るあいだに別の探索がおこなわれて途中で必要な薬がなくなってしまっていて、実はゴールにたどり着けるのに “No” と出力される場合が多々あるのです。

公式の解説によるとこうです。

薬は使うとなくなります。そこで薬があるかどうかを表すフラグを定義しましたが、じつは考える必要はありません。薬を使うと強制的にエネルギーが一定の値にリセットされるので、何度でも使えるとしても問題ありません。その場合は最後に使った後の行動を代わりに初めて使った後にすればよいからです。

ただ上記のコードで薬に関するフラグをなしにしても問題は解決しません。無限ループのような状態になっていつまで経ってもプログラムが終わりません。

では、どうすればよいのでしょうか?

正しい解法

薬がエネルギーを増やすのではなく、エネルギーを強制的にその値にするものであるという性質のものである以上、ある薬を使った時点で高橋くんの位置と彼のエネルギーは一意です。

そこでこう考えます。

薬 i が置いてあるマスがあり、その薬をつかったあと別の薬 j がある場所へ移動する有向グラフを考えます。薬 i を使ったらエネルギーはそれまでのエネルギーとは無関係に固定の値に変化してしまうので、i から j へ移動可能か、j から i へ移動可能かがわかります。

あとは移動が可能な部分だけで有向グラフを構築して、その上で幅優先探索をすることでこの問題を解くことができます。この有向グラフは薬の位置だけでなくスタート地点とゴール地点も必要になりますが、ゴール地点に薬がない場合はダミーの薬をおくようにすると考えやすいと思います。またスタート地点が薬をおいている位置でない場合はそもそもゴールにはたどり着けません。

薬 i を使った後、他の薬を使わずに薬 j の位置にたどり着けるか?という問題は幅優先探索で求めればよいです。N 個の薬それぞれについて幅優先探索をするのであれば、計算量は O(HWN) です。H と W の最大値は 200, N の最大値は 300 であることから充分間に合います。また、これによって構築された有向グラフを探索する計算量はこれほど大きくはなりません。

薬と薬のあいだは移動可能か?

まず幅優先探索で n 番目から他の薬まで移動するコストを求めます。コストを初期化しなければなりませんが、最小になる値を求めるので int.MaxValue などの巨大な数や -1 などの不適切な数を使います。かつて深く考えないで int.MaxValue をつかってオーバーフローしてバグったことがあるのでここでは -1 をつかっています。グリッドの大きさから考えて1,000,000のような値になることはありえないのでオーバーフローを気にしなくてよいほどほどに大きな値を使うのもありだと思います。

幅優先探索が終わったら出発点のエネルギーと移動コストが同じか小さいのであれば移動可能です。移動可能な頂点番号をリストで返します。このとき出発点と終点が同じ場合は意味がないので結果から取り除きます。またこのとき移動コストが -1 だとそもそもたどり着けないことになるのですが、それを失念していてバグを連発してしまいました。やはりオーバーフローの心配がない大きな数で初期化したほうがよいみたいです。

相互に移動可能な薬で構築された有向グラフで s-t 間の移動は可能か?

BFS2メソッドは移動が可能な部分だけで有向グラフを構築し、出発点からゴールまで移動できるかその結果を返します。まず出発点には必ず薬がないと、移動を開始することすらできないので、falseを返します。

ではゴールはどう考えればよいでしょうか? ゴールもダミーの薬があることにすると処理が簡単になります。テストケースによってはゴールにも薬があるものもあるかもしれませんが問題ありません。

あとは隣接リストをつくってそれぞれの薬の位置から別の薬の位置まで移動できるものを取得します。取得できた有向グラフで出発点からゴールまで移動できるでしょうか?