D - 交互移動ロボット / Alternating Movement Robot Editorial by admin
gpt-6-astra-medium概要
「現在いるマス」に加えて「次に移動する方向」を状態として管理し、幅優先探索(BFS)で最小移動回数を求めます。最初は縦・横のどちらにも移動できるため、始点には両方の状態を用意します。
考察
マスの位置だけでは状態を区別できない
通常の迷路の最短経路問題では、各マスを訪問済みにする BFS が使えます。しかし、この問題では同じマスにいても、次に移動できる方向が異なる場合があります。
例えば、あるマスに縦方向の移動で到着した場合、次は左右にしか移動できません。一方、横方向の移動で到着した場合、次は上下にしか移動できません。
このため、マス単位で訪問済みにしてしまうと、次に移動できる方向が異なる経路まで探索対象から除外し、正しい答えを求められないことがあります。
「次に移動する方向」を状態に加える
そこで、次のように状態を定義します。
- \((i,j,0)\):マス \((i,j)\) にいて、次は縦方向へ移動する
- \((i,j,1)\):マス \((i,j)\) にいて、次は横方向へ移動する
縦方向へ移動すると次は横方向、横方向へ移動すると次は縦方向になるので、移動するたびに状態の末尾の値を切り替えます。
各マスにつき状態は \(2\) 個なので、全体の状態数は最大 \(2HW\) 個です。
移動経路をすべて列挙する方法では、同じマスへの再訪も許されるため、探索が際限なく続く可能性があります。しかし、同じ「マスと次の方向」の状態に戻ったなら、その後にできる移動は同じです。したがって、各状態を最短で到達したときに一度だけ処理すれば十分です。
すべての移動のコストが同じなので BFS を使う
どの移動も移動回数を \(1\) 増やします。このようなグラフでは、BFS によって移動回数の少ない状態から順に探索できます。
最初の移動方向は自由なので、始点の \(2\) 状態
- \((1,1,0)\)
- \((1,1,1)\)
を、どちらも距離 \(0\) として探索を始めます。
アルゴリズム
- 各空きマスについて、「次は縦方向」「次は横方向」の \(2\) 状態を用意します。
- 始点の \(2\) 状態を訪問済みにし、キューに入れます。現在の移動回数を \(0\) とします。
- キューから状態を取り出し、次の処理を行います。
- 現在位置がゴールなら、現在の移動回数を出力して終了します。
- 次が縦方向なら、上下のマスを調べます。
- 移動先が空きマスで、「次は横方向」の状態が未訪問なら、訪問済みにしてキューに入れます。
- 次が横方向なら、左右のマスを調べます。
- 移動先が空きマスで、「次は縦方向」の状態が未訪問なら、訪問済みにしてキューに入れます。
- 同じ移動回数の状態をすべて処理したら、移動回数を \(1\) 増やします。
- ゴールに到達せずにキューが空になった場合は、
NONEを出力します。
正しさ
状態遷移では、指定された方向にだけ移動し、その後で次の方向を切り替えています。そのため、探索で得られる経路は必ず縦・横の交互移動という条件を満たします。
逆に、条件を満たす移動は、この状態遷移としてすべて表せます。また、始点の両方の状態から探索するため、最初に縦へ動く経路も横へ動く経路も調べられます。
BFS は移動回数の小さい順に状態を取り出すので、初めてゴールの状態を取り出したときの移動回数が最小値です。探索を終えてもゴールが見つからなければ、到達する経路は存在しません。
計算量
- 時間計算量: \(O(HW)\)
- 空間計算量: \(O(HW)\)
状態数は最大 \(2HW\) 個で、各状態から調べる移動先は最大 \(2\) 個です。各状態は一度しかキューに入れないため、全体で \(O(HW)\) 時間となります。
実装のポイント
盤面の周囲に番兵を置く
コードでは、盤面の周囲を進入不可のマスで囲み、\((H+2)\times(W+2)\) の配列を使っています。
横幅を stride = W + 2 とすると、マス \((i,j)\) の一次元配列上の位置は
\(pos=i\cdot \mathrm{stride}+j\)
です。上下左右の位置は、それぞれ次のように求められます。
- 上:
pos - stride - 下:
pos + stride - 左:
pos - 1 - 右:
pos + 1
周囲の番兵は常に進入不可なので、移動先が盤面の範囲内かを毎回判定する必要がありません。
進入可能かどうかと訪問済み情報をまとめる
コード中の vertical と horizontal は、次の意味を持つ bytearray です。
vertical[pos] == 1:空きマスであり、「次は縦方向」の状態が未訪問horizontal[pos] == 1:空きマスであり、「次は横方向」の状態が未訪問
障害物・番兵・訪問済み状態はいずれも 0 にします。キューに追加する時点で 0 にすることで、同じ状態の重複追加を防ぎます。
状態を整数 \(1\) 個で表す
位置を pos、次の方向を d として、状態を
(pos << 1) | d
で表しています。d = 0 が縦方向、d = 1 が横方向です。
取り出すときは、state >> 1 で位置を、state & 1 で方向を取得できます。
BFS を移動回数ごとに処理する
for _ in range(len(queue)) により、その時点でキューにある状態だけを処理します。処理中に追加された状態は次の周回に回るため、周回ごとに distance を \(1\) 増やすだけで移動回数を管理できます。
ソースコード
import sys
from collections import deque
def main():
input = sys.stdin.buffer.readline
H, W = map(int, input().split())
stride = W + 2
size = (H + 2) * stride
vertical = bytearray(size)
for i in range(1, H + 1):
start = i * stride + 1
vertical[start:start + W] = bytearray(
x == b'0' for x in input().split()
)
horizontal = vertical[:]
start = stride + 1
goal = H * stride + W
vertical[start] = 0
horizontal[start] = 0
queue = deque((start << 1, (start << 1) | 1))
append = queue.append
popleft = queue.popleft
distance = 0
while queue:
for _ in range(len(queue)):
state = popleft()
pos = state >> 1
if pos == goal:
print(distance)
return
if state & 1:
nxt = pos - 1
if vertical[nxt]:
vertical[nxt] = 0
append(nxt << 1)
nxt = pos + 1
if vertical[nxt]:
vertical[nxt] = 0
append(nxt << 1)
else:
nxt = pos - stride
if horizontal[nxt]:
horizontal[nxt] = 0
append((nxt << 1) | 1)
nxt = pos + stride
if horizontal[nxt]:
horizontal[nxt] = 0
append((nxt << 1) | 1)
distance += 1
print("NONE")
if __name__ == "__main__":
main()
この解説は gpt-6-astra-medium によって生成されました。