Bats Bunker (O'Reilly) - コウモリ間の伝達
どんな問題?
Bats Bunker
http://www.checkio.org/mission/bats-bunker/
バンカーには、コウモリが多数いる。
入口(座標0,0)から、ボスコウモリまで、警報が伝達する最短時間を求めよ。
コウモリ間は直線でつながっていれば伝達が可能。壁を通り抜けての伝達はできない。
伝達時間は距離に比例し、隣のセルまでの伝達時間は1秒とする。
- 入口にボスコウモリがいる場合もある。
- マップの幅と高さは、3以上7以下
- ボスコウモリは1匹だけ
引数は、マップのデータ。Wが壁、Bが斥候コウモリ、Aがボスコウモリとなる。
戻り値は、最小の伝達時間を小数点2桁で返す。
例題:
checkio([ "B--", "---", "--A"]) == 2.83 checkio([ "B-B", "BW-", "-BA"]) == 4 checkio([ "BWB--B", "-W-WW-", "B-BWAB"]) == 12 checkio([ "B---B-", "-WWW-B", "-WA--B", "-W-B--", "-WWW-B", "B-BWB-"]) == 9.24
どうやって解く?
重み付きのグラフ探索というやつかな。アルゴリズムの本によると、重みが負でない場合の最短経路探索は「ダイクストラのアルゴリズム(ダイクストラ法)」を使えばいいらしい。
初期設定
引数のマップ情報を、使いやすいよう辞書に変換しておく。
map_data = {(x, y): c for y, line in enumerate(bunker) for x, c in enumerate(line)}
さらに、すべてのコウモリ(斥候とボス)の位置をsetにしておく。
bats = {k for k, v in map_data.items() if v in 'AB'}
コウモリ間の伝達が可能かどうかを判定
コウモリaからコウモリbに伝達可能かどうかを判定する方法だが、線分abが横切るセルをリストアップして、そのセルに壁がなければ、伝達可能と判断することにした。
問題は「線分abが横切るセルをリストアップ」という部分。ab間は、壁でふさがれていなくても、下記のように壁の角が接しているだけでも、伝達はできない。
a■ □b a■□ □□b a□□ □■b
あれこれ考えて、線分abを、点aからbに向けて、xを0.5ずつずらしていき、x,yの値によって、どのセルを横切るかを調べていくことにした。
例えば、右の図では、a=(0,0)、b=(2,1)なので、線分abの経過点は (0.5, 0.25)、(1, 0.5)、(1.5, 0.75) の3つある。ここから、
- (0.5, 0.25) → (0, 0)、(1, 0)にヒット
- (1, 0.5) → (1, 0)、(1, 1)にヒット
- (1.5、0.75) → (1, 1)、(2, 1)にヒット
と判断する。つまり、値が0.5なら0と1の両方、それ以外なら0.25なら0、0.75なら1のように、丸めればいい。
コードはこちら。0.5刻みで動かすため、先にrange_float()関数を定義している。
def range_float(start, stop, step): # range()の浮動小数点版 while start < stop: yield start start += step def get_cells_on_line(a, b): # 線分abが横切るセルの座標をsetで取得 (x1, y1), (x2, y2) = a, b result = set() delta = max(abs(x2 - x1), abs(y2 - y1)) # xの差分とyの差分の大きい方 for i in range_float(0.5, delta, 0.5): # 0.5刻みで動かす x = x1 + (x2 - x1) * i / delta y = y1 + (y2 - y1) * i / delta xs = [int(x), int(x) + 1] if x % 1.0 == 0.5 else [round(x)] # 例えば、2.5なら2と3の両方 ys = [int(y), int(y) + 1] if y % 1.0 == 0.5 else [round(y)] result |= set(itertools.product(xs, ys)) return result - {a, b} # セルa,bを除外
浮動小数点を == で比較している点が気になるが、、、。
これで、コウモリaからコウモリbに伝達可能かどうかを判定する部分はこうなる。
def is_connected(a, b): return all(map_data[pt] not in 'W' for pt in get_cells_on_line(a, b))
ダイクストラ法(Dijkstra)
アルゴリズムの本によると、ダイクストラ法では、A*と同じようにヒープキュー(優先度付きキュー)を使う。A*ではゴールまでの距離の推定値を使うが、ダイクストラ法ではスタートからの距離を使う、という違いのようだ。
- Pythonでヒープキューを使うには、heapqモジュールを使う。
- ヒープキューに、(スタートからの距離、現在位置、訪問済みの頂点)を入れる。
- heappop()で、スタートからの距離が短い順にキューから取り出す。
heap = [(0, (0, 0), {(0, 0)})] # (length, pos, visited) while heap: length, pos, visited = heapq.heappop(heap) if map_data[pos] == 'A': # Aに到達したらゴール return round(length, 2) for bat in bats - visited: if is_connected(pos, bat): # 未訪問で伝達可能なコウモリがあれば、ヒープキューに追加 new_length = length + get_length(pos, bat) heapq.heappush(heap, (new_length, bat, visited | {bat}))
まとめ
import itertools import heapq def range_float(start, stop, step): while start < stop: yield start start += step def checkio(bunker): def get_length(a, b): return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5 def get_cells_on_line(a, b): (x1, y1), (x2, y2) = a, b result = set() delta = max(abs(x2 - x1), abs(y2 - y1)) for i in range_float(0.5, delta, 0.5): x = x1 + (x2 - x1) * i / delta y = y1 + (y2 - y1) * i / delta xs = [int(x), int(x) + 1] if x % 1.0 == 0.5 else [round(x)] ys = [int(y), int(y) + 1] if y % 1.0 == 0.5 else [round(y)] result |= set(itertools.product(xs, ys)) return result - {a, b} def is_connected(a, b): return all(map_data[pt] not in 'W' for pt in get_cells_on_line(a, b)) map_data = {(x, y): c for y, line in enumerate(bunker) for x, c in enumerate(line)} bats = {k for k, v in map_data.items() if v in 'AB'} heap = [(0, (0, 0), {(0, 0)})] # (length, pos, visited) while heap: length, pos, visited = heapq.heappop(heap) if map_data[pos] == 'A': return round(length, 2) for bat in bats - visited: if is_connected(pos, bat): new_length = length + get_length(pos, bat) heapq.heappush(heap, (new_length, bat, visited | {bat}))
http://www.checkio.org/mission/bats-bunker/publications/natsuki/python-3/first/
なかなか難しかったが、なんとかクリア。
やっぱり浮動小数点の == が気になる。
x % 1.0 == 0.5
これは、
abs(x % 1.0 - 0.5) < 0.0001
こうしたほうが良かったかな。(しかし、適切な誤差ってどれくらいだろう?)
(追記)
get_length()のところは、math.hypotを使えばよかった・・・。