nashidos’s diary

アルゴリズムとか機械学習とか色々

Pythonで深さ優先探索(DFS)を実装してみる-ATC001

本記事では、深さ優先探索Pythonを利用して実装していきます。

実際に深さ優先探索の典型問題をスタックと再帰関数の2パターンの解き方で実装し、解説していきます。

深さ優先探索(DFS)とは

深さ優先探索とはDepth First Searchの略で、バックトラック法とも言われます。

目的のノードを見つけるか、全て探索し終わるまで1と2を繰り返します。

  1. 目的のノードをみつけるか、探索できる子ノードがなくなるまで探索
  2. 探索できる子ノードがなくなるとバックトラック(分岐したところまで戻る)して、再び探索

探索していく順序は以下の通りです。

深さ優先探索
参照元深さ優先探索 - Wikipedia

途中で分岐できる場合でも気にせず深くまで探索するので深さ優先探索と呼ばれます。←ここが幅優先探索との違い!

幅優先探索の記事はこちら↓
nashidos.hatenablog.com

例題

今回は以下の問題を扱います。
atcoder.jp

問題文

高橋君の住む街は長方形の形をしており、格子状の区画に区切られています。 長方形の各辺は東西及び南北に並行です。 各区画は道または塀のどちらかであり、高橋君は道を東西南北に移動できますが斜めには移動できません。 また、塀の区画は通ることができません。
高橋君が、塀を壊したりすることなく道を通って魚屋にたどり着けるかどうか判定してください。

再帰関数で実装

まずは再帰関数を利用して解いていきたいと思います。

実装の方針としては、深さ優先探索なのでまず最初にできるだけ深く探索します。
そして、探索できなくなると別の方向に探索を始めます。

この流れを再帰関数を利用して実装したのが以下のコードです。

import sys
sys.setrecursionlimit(10**7) #再帰関数の呼び出し制限
h,w = map(int,input().split())
c = [list(input()) for i in range(h)]

def dfs(x,y):
    if not(0 <= x < h) or not(0 <= y < w) or c[x][y] == "#": # 壁に当たったり、探索範囲外になった場合はreturn
        return
    if c[x][y] == "g": # ゴールを見つけたら”Yes”を出力して終了
        print("Yes")
        sys.exit()
        
    c[x][y] = "#" #探索済みを示すためのマーキング
    dfs(x+1,y)
    dfs(x-1,y)
    dfs(x,y+1)
    dfs(x,y-1)

for i in range(h):
    for j in range(w):
        if c[i][j] == "s":
            sx, sy = i, j # スタート位置
            
dfs(sx, sy)
print("No")

コードだけみてもよくわからないかもしれないので、実際にどのように探索しているのか見てみましょう。

入力

4 4
s...
..##
.#.#
.g..

探索プロセス

s...
..##
.#.#
.g..
--------------------
#...
..##
.#.#
.g..
--------------------
#...
#.##
.#.#
.g..
--------------------
#...
#.##
##.#
.g..
--------------------
#...
#.##
##.#
#g..
--------------------
#...
#.##
##.#
#g..
--------------------
#...
#.##
##.#
#g..
--------------------
Yes

深さを優先して探索していることがわりますね。

それでは、次はスタックで実装していきます。


スタックで実装

スタックで実装する場合も先ほどと方針は変わりません。

変更する点として、探索する位置をスタックを利用して管理します。

以下がスタックを利用して実装したコードです。

import sys
h,w = map(int,input().split())
c = [list(input()) for i in range(h)]

for i in range(h):
    for j in range(w):
        if c[i][j] == "s":
            sx,sy = i,j #スタート
        elif c[i][j] == "g":
            gx,gy = i,j #ゴール

stack = [[sx,sy]]
visited = [[0 for i in range(w)]for j in range(h)]
visited[sx][sy] = 1

dx_dy = [[1,0],[0,1],[-1,0],[0,-1]]

while stack:
    x,y = stack.pop() #要素を取り出す
    for i in range(4):
        nx,ny = x+dx_dy[i][0], y+dx_dy[i][1] #現在の位置
        if 0 <= nx < h and 0 <= ny < w and visited[nx][ny] == 0 and c[nx][ny] != '#':
            visited[nx][ny] = 1 #訪れた印をつける
            stack.append([nx,ny]) #スタックに現在位置をpush
    if visited[gx][gy] == 1: 
        print("Yes")
        sys.exit()

print("No")

再帰関数のコードよりも量が多く少し複雑になった気がします…

まとめ

個人的には再帰関数を使った方が書きやすいです。

スタックを用いた実装をすることで「スタックオーバーフロー」を回避できるというメリットもあるみたいですが、基本的には再帰関数で書いていこうかなと思いました。

おわり