The shortest Knight's path (Incinerator) - ナイトの最短ルート
どんな問題?
The shortest Knight's path
http://www.checkio.org/mission/shortest-knight-path/
チェスのナイトが、ある地点から別の地点へ移動するときの、最短ルートを探せ。
スタートとゴールの2点が"b1-d5"のような文字列で渡される。
最短で何手で移動できるかを返す。
ナイトは、桂馬が四方向を向いているようなもので、8ヶ所に移動できる。
■ | ■ | |||
■ | ■ | |||
▲ | ||||
■ | ■ | |||
■ | ■ |
どうやって解く?
これも探索問題ってことになるのかな?
前に苦労したOpen Labyrinthに似ているが、今回は最短経路という条件が付いてくる。
1手目で進めるのは8ヶ所、2手目で進めるのは8*8=64ヶ所(実際には重複するのでもっと減るだろうけど)、どんどん広がっていくツリーを探索していって、ゴールが見つかれば終了、という流れだろうか。
探索&最短経路
アルゴリズムの本を再度読んでみたが、やっぱりBFSとかDFSとか、ちゃんと理解していないので、もう一度整理してみる。
- DFS(深さ優先探索)
- スタックを使う
- 最短経路とは限らない
- BFS(幅優先探索)
- キューを使う
- 辺の重みがなければ最短経路
- A*探索
- ヒープキュー(優先度付きキュー)を使う
- 最短経路
- 最短経路の推定値が必要
- ダイクストラ法
- A*探索の最短経路の推定値を使わない場合
他にも、最短経路問題として
などが載っていた。
書き出してみると、よけいに混乱してきた。今回、マスからマスへは一手で動くわけだから、辺の重みはなし、ってことだと思うんだが…。
とにかく、BFSでやってみる。
引数を座標に変換
引数の'b1-d5'形式の文字列を、(x1,y1)と(x2,y2)という座標に変換する。文字列のindex=0がx1、index1がy=1、と決めうちして変換しておく。あまりいい方法ではないかもしれない。
def parse(s): X = 'abcdefgh' Y = '12345678' return (X.index(s[0]), Y.index(s[1])), (X.index(s[3]), Y.index(s[4]))
次に進むことができるマスを取得
現在位置を(x,y)とすると、次に進めるマスは、(x±1,y±2),(x±2,y±1)となる。
内包表記でうまく書けた(つもり)。範囲チェックして、ボード内の座標のみを返す。
def get_next_positions(pt): pts = [(pt[0] + x * sx, pt[1] + y * sy) for sx in [-1, 1] for sy in [-1, 1] for x, y in [(1, 2), (2, 1)]] return [p for p in pts if 0 <= p[0] < 8 and 0 <= p[1] < 8]
まとめると
def checkio(move): def parse(s): # 'b1-d5' to (1,0),(3,4) X = 'abcdefgh' Y = '12345678' return (X.index(s[0]), Y.index(s[1])), (X.index(s[3]), Y.index(s[4])) def get_next_positions(pt): pts = [(pt[0] + x * sx, pt[1] + y * sy) for sx in [-1, 1] for sy in [-1, 1] for x, y in [(1, 2), (2, 1)]] return [p for p in pts if 0 <= p[0] < 8 and 0 <= p[1] < 8] def get_path(): result = [] pt = goal while pt != start: result.append(visited[pt]) pt = visited[pt] return result start, goal = parse(move) visited = dict() # Key=pos, Value=prev pos queue = [start] while len(queue) > 0: pos = queue.pop(0) if pos == goal: return len(get_path()) for new_pos in get_next_positions(pos): if new_pos not in visited: visited[new_pos] = pos queue.append(new_pos) return 0
一度通ったマスは、visited辞書で管理しておく。
visited辞書に、移動前の座標を入れておいて、ゴールに着いたら、これを逆にたどればルート(通ってきたマスのリスト)がわかる。 get_path()がその部分。ルートの長さが答えとなる。
難しそうだと思ったわりには、あっさりとクリアできてしまった。
ちょっと物足りないので、別解も考えてみる。
続く