迷路の最短経路探索を実装する

この記事は[tomowarkar ひとりAdvent Calendar 2019](https://qiita.com/advent-calendar/2019/tomowarkar)の20日目の記事です。

# はじめに
幅優先探索とか、深さ優先探索とかなんか難しそうで今まであまり触れてこなかったんですが、なんとなくやる気になったので迷路の最短経路探索を書いてみました。

内容自体はよく出てくるような内容で、特段変なことはしてないのですが
備忘録兼アウトプットとして書いていきます。

### 成果物
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235435/29bc6583-4287-f35e-34cd-c7bd3b08c160.png)
# コード
```py
def printMaze(maze):
    """迷路を描画"""
    for i in range(maze["height"]):
        row = maze["data"][i * maze["width"] : (i + 1) * maze["width"]]
        print(" ".join(row))


def at(x, y, maze):
    """迷路の(x, y)座標のオブジェクトを返す"""
    return maze["data"][y * maze["width"] + x]


def setChar(x, y, char, maze):
    """迷路の(x, y)座標のオブジェクトを設定する"""
    arr = list(maze["data"])
    arr[y * maze["width"] + x] = char
    maze["data"] = "".join(arr)


if __name__ == "__main__":
    maze = {
        "width": 43,
        "height": 35,
        "data": "###########################################S #   #           #                       ## ### ########### # ################# ### ##   #           # # #               # # # #### ########### # # ##### ##### ### ### # ## #         # # #       # #   # # #     # ## ####### ### # ####### ### ### # ##### # ##         #       #         #     #     # ###### ### ##### ### ##### ### ##### ##### ##   # # # #   # #   # #   #   #     #     ## # # # # ### # # ### ### # ### ### # ### ## # # # #   # # # #     # # #   # # # # # ## # # # ##### # ####### ### # ### # # # # ## # # #       #       #     # #   # # # # ## # # # ##### ### ### ####### ### ### # # ## # # #   # #   # # #           #     # # ## # # ##### ### ### ########### ### ### # ## # #         #             # # # # #   # ## # ##### ### # ### ### ##### # # # ### # ## #     # # # # # #   # #       # #   # # ## ##### ### # ### ##### ### ### # ### # # ##     #     #               # # #   # #   #### # # ### ### ######### ### ### ### ### ## # # # # # #   #       # #             # ## ### ### ### ##### # ### ### ####### ### ##             #   # # #     # #   #   #   ## ### ####### ### ### # ##### ### ### # #### # # # #       #     # #       #   # #   ## # # # ### ### # ##### ##### ##### # ### ## # # # #   # # # #         #       #   # ## # # # # ### ### # # ####### ##### ### # ## # #   # #       # # #       #   # #   # ## # ##### # ####### # ######### ### # ### ## #       #         #               # #   G###########################################",
    }
    start = [0, 1]
    goal = [42, 33]

    visited = [False] * maze["width"] * maze["height"]
    costs = [999999] * maze["width"] * maze["height"]

    # スタート位置(0, 1)をqueueに入れる
    queue = [start]
    costs[start[1] * maze["width"] + start[0]] = 0

    while len(queue) > 0:
        # queueから位置を取り出す
        p, queue = queue[0], queue[1:]
        visited[p[1] * maze["width"] + p[0]] = True

        # 上下左右を検証
        for i in [[0, -1], [0, 1], [-1, 0], [1, 0]]:
            x, y = p[0] + i[0], p[1] + i[1]
            if x < 0 or y < 0 or x > maze["width"] - 1 or y > maze["height"] - 1:
                continue

            if at(x, y, maze) == " " and visited[y * maze["width"] + x] == False:
                queue.append([x, y])
                costs[y * maze["width"] + x] = costs[p[1] * maze["width"] + p[0]] + 1
            if at(x, y, maze) == "G":
                queue = []
                costs[y * maze["width"] + x] = costs[p[1] * maze["width"] + p[0]] + 1
                break
    # costsをゴールから辿り、最短経路を求める
    point = goal
    cost = costs[point[1] * maze["width"] + point[0]]
    while cost != 1:
        for i in [[0, -1], [0, 1], [-1, 0], [1, 0]]:
            x, y = point[0] + i[0], point[1] + i[1]
            if x < 0 or y < 0 or x > maze["width"] - 1 or y > maze["height"] - 1:
                continue
            if costs[y * maze["width"] + x] == cost - 1:
                cost = costs[y * maze["width"] + x]
                point = [x, y]
                setChar(x, y, ".", maze)
                break

    printMaze(maze)
```

# 解説
迷路情報を辞書型で定義します。
`"#"`が壁`" "`が通路, `"S"`, `"G"`がそれぞれスタートとゴールという形です。
同時にスタートとゴールの`(x, y)`座標も手入力で入れてしまいます。

```py
maze = {
    "width": 43,
    "height": 35,
    "data": "###########################################S #   #           #                       ## ### ########### # ################# ### ##   #           # # #               # # # #### ########### # # ##### ##### ### ### # ## #         # # #       # #   # # #     # ## ####### ### # ####### ### ### # ##### # ##         #       #         #     #     # ###### ### ##### ### ##### ### ##### ##### ##   # # # #   # #   # #   #   #     #     ## # # # # ### # # ### ### # ### ### # ### ## # # # #   # # # #     # # #   # # # # # ## # # # ##### # ####### ### # ### # # # # ## # # #       #       #     # #   # # # # ## # # # ##### ### ### ####### ### ### # # ## # # #   # #   # # #           #     # # ## # # ##### ### ### ########### ### ### # ## # #         #             # # # # #   # ## # ##### ### # ### ### ##### # # # ### # ## #     # # # # # #   # #       # #   # # ## ##### ### # ### ##### ### ### # ### # # ##     #     #               # # #   # #   #### # # ### ### ######### ### ### ### ### ## # # # # # #   #       # #             # ## ### ### ### ##### # ### ### ####### ### ##             #   # # #     # #   #   #   ## ### ####### ### ### # ##### ### ### # #### # # # #       #     # #       #   # #   ## # # # ### ### # ##### ##### ##### # ### ## # # # #   # # # #         #       #   # ## # # # # ### ### # # ####### ##### ### # ## # #   # #       # # #       #   # #   # ## # ##### # ####### # ######### ### # ### ## #       #         #               # #   G###########################################",
}
start = [0, 1]
goal = [42, 33]
```
### 下準備
探索に用いる`queue`と`visited`, 最短経路を導く際に用いる`costs`を定義します。

迷路は二次元配列で出力しますが、データ加工は全て一次元配列でおこなうので、適宜変換を行う必要があります。

```py

visited = [False] * maze["width"] * maze["height"]
costs = [999999] * maze["width"] * maze["height"]

# スタート位置(0, 1)をqueueに入れる
queue = [start]
costs[start[1] * maze["width"] + start[0]] = 0
```

### 探索メイン処理
幅優先探索なので、キューの頭から取り出して検証していきます。
上下左右の状態が通路であればどんどんキューに追加していきます。
コストの更新は常に基準点のコストから+1していくことで、最短経路を導出する際に使用することができます。

```py
    while len(queue) > 0:
        # queueから位置を取り出す
        p, queue = queue[0], queue[1:]
        visited[p[1] * maze["width"] + p[0]] = True

        # 上下左右を検証
        for i in [[0, -1], [0, 1], [-1, 0], [1, 0]]:
            x, y = p[0] + i[0], p[1] + i[1]
            if x < 0 or y < 0 or x > maze["width"] - 1 or y > maze["height"] - 1:
                continue

            if at(x, y, maze) == " " and visited[y * maze["width"] + x] == False:
                queue.append([x, y])
                costs[y * maze["width"] + x] = costs[p[1] * maze["width"] + p[0]] + 1
            if at(x, y, maze) == "G":
                queue = []
                costs[y * maze["width"] + x] = costs[p[1] * maze["width"] + p[0]] + 1
                break
```
### 最短経路導出
`cost`はスタートからかかる距離と同意であるため、ゴール位置のコストから０まで遡っていくことで最短経路が導出できます。

```py
    # costsをゴールから辿り、最短経路を求める
    point = goal
    cost = costs[point[1] * maze["width"] + point[0]]
    while cost != 1:
        for i in [[0, -1], [0, 1], [-1, 0], [1, 0]]:
            x, y = point[0] + i[0], point[1] + i[1]
            if x < 0 or y < 0 or x > maze["width"] - 1 or y > maze["height"] - 1:
                continue
            if costs[y * maze["width"] + x] == cost - 1:
                cost = costs[y * maze["width"] + x]
                point = [x, y]
                setChar(x, y, ".", maze)
                break

    printMaze(maze)
```
### 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235435/29bc6583-4287-f35e-34cd-c7bd3b08c160.png)
# おわりに
以上明日も頑張ります！！
[tomowarkar ひとりAdvent Calendar Advent Calendar 2019](https://qiita.com/advent-calendar/2019/tomowarkar)

# 参考
https://qiita.com/nati-ueno/items/a789095aff0aec10d5d0
https://vigne-cla.com/18-2/