Menu
Coddy logo textTech

深さ優先探索 (DFS)

最終更新

深さ優先探索は、バックトラックする前に各枝をできるだけ深くたどってグラフを探索します。始点ノードから始めて、ある隣接ノードを訪れ、次にその隣接ノードを訪れ、というように行き止まり(未訪問の隣接ノードがないノード)に達するまで進み、そこで引き返して次の枝を試します。上の再生を押して、1 つの経路を下って底に到達し、また戻ってくる様子を見てみましょう。

DFS はスタックで自然に表現できます。明示的なスタック(ここでアニメーション化しているもの)でも、再帰による呼び出しスタックでも構いません。各ノードと辺を 1 回ずつ訪れるため、V 個の頂点と E 本の辺を持つグラフに対して O(V + E) 時間で実行されます。

時間計算量と空間計算量

指標計算量備考
時間O(V + E)各頂点と辺を 1 回ずつ訪れる
空間O(V)スタック + 訪問済み集合、最悪の場合すべてのノード
探索最も深いものから他より先に 1 つの枝を最後までたどる
データ構造スタック(または再帰)LIFO の順序が深さ優先の挙動を駆動する

ステップごと

ステップ何が起こるか
1始点ノードをスタックに積む。
2ノードを取り出す。すでに訪問済みならスキップする。
3訪問済みとして印を付け、それに到達した辺を記録する。
4未訪問の隣接ノードをすべてスタックに積む。
5スタックが空になるまで繰り返す。

具体例

グラフ A → [B, C]B → [D]C → [E] に対する A からの反復的 DFS。隣接ノードを列挙順に積む(LIFO スタックは最後に積んだものを先に取り出す):

ステップスタック(右が先頭)訪問済み動作
1[A]{}始点 A を積む。
2[B, C]{A}A を取り出し、訪問済みにし、隣接ノード B 次に C を積む。
3[B, E]{A, C}C を取り出し、訪問済みにし、隣接ノード E を積む。
4[B]{A, C, E}E を取り出し、訪問済みにする。隣接ノードなし。
5[D]{A, C, E, B}B を取り出し、訪問済みにし、隣接ノード D を積む。
6[]{A, C, E, B, D}D を取り出し、訪問済みにする。スタックが空:完了。

DFS を使うべき場面

使うべき場面避けるべき場面
閉路検出、トポロジカルソート、または連結成分の発見が必要なとき。重みなしグラフで最短経路が必要なとき。それを行うのは BFS であって DFS ではない。
グラフが幅広く浅いため、スタックが多数のフロンティアノードを持つキューよりはるかに少ないメモリで済むとき。グラフが非常に深く再帰を使うとき。呼び出しスタックがオーバーフローする可能性がある。
迷路解きやバックトラッキング探索のように、経路を網羅的に探索したいとき。始点からの距離順にノードを発見する必要があるとき。
到達可能性、または 2 つのノード間に経路が存在するかを確認するとき。見つけた経路上の辺の数を最小にすることを保証しなければならないとき。

Depth-First Searchのコード

Python, JavaScript, Java, C++, Cによるクリーンで実行可能なDepth-First Searchの実装です。言語を選んでコードをコピーするか、Coddyプレイグラウンドに読み込んだ状態で開けます。

PythonでのDepth-First Searchのコード

Python
1def dfs(graph, node, visited):2    visited.add(node)3    order = [node]4    for neighbor in graph[node]:5        if neighbor not in visited:6            order.extend(dfs(graph, neighbor, visited))7    return order8
9
10graph = {11    "A": ["B", "C"],12    "B": ["D", "E"],13    "C": ["F"],14    "D": [],15    "E": ["F"],16    "F": [],17}18
19order = dfs(graph, "A", set())20print("DFS order:", " -> ".join(order))
このコードをPythonプレイグラウンドで実行

深さ優先探索のよくある質問

DFS の時間計算量は?
DFS は O(V + E) 時間で実行されます。ここで V は頂点数、E は辺の数です。各頂点を 1 回訪れ、各辺を 1 回見るためです。スタックと訪問済み集合に O(V) の空間を使います。
DFS と BFS の違いは?
DFS はスタックを使い、バックトラックする前に 1 つの枝を深く潜りますが、BFS はキューを使い、近いノードから順に階層ごとに探索します。BFS は重みなしグラフで最短経路を見つけますが、DFS は見つけません。ただし DFS は幅広いグラフでメモリ使用量が少なく、閉路検出やトポロジカルソートのようなタスクに自然です。
DFS は再帰的ですか、反復的ですか?
どちらも使えます。再帰版はプログラムの呼び出しスタックを暗黙的に使い非常に簡潔です。反復版は明示的なスタック(ここのアニメーションのように)を使い、大きなグラフでの深い再帰によるスタックオーバーフローを避けられます。
BFS ではなく DFS を使うべきなのはいつ?
経路全体を探索する必要があるときに DFS を使います。閉路検出、トポロジカルソート、連結成分、迷路解きのようなバックトラッキング探索などです。スタックは一度に 1 つの枝のフロンティアしか保持しないため、幅広いグラフでメモリ使用量も少なくなります。重みなしグラフで最短経路が必要なとき、または始点からの距離順のノードが必要なときは BFS を使いましょう。
DFS は最短経路を見つけられますか?
確実には見つけられません。DFS はたまたま最初に見つけた経路を返しますが、それは多くの場合、辺の数が最小の経路ではありません。均等に広がるのではなく深く潜ることに専念するからです。重みなしグラフの最短経路には BFS を、重み付きグラフには Dijkstra のアルゴリズムを使いましょう。
なぜ DFS には訪問済み集合が必要なのですか?
訪問済み集合がないと、DFS は複数の経路で到達できるノードを再訪し、閉路のあるグラフでは無限ループに陥ります。ノードを取り出すときに訪問済みとして印を付け(訪問済みの取り出しはスキップし)ることで、各ノードが 1 回だけ処理されることが保証され、実行時間が O(V + E) に保たれます。よくある誤りは、取り出すときにのみ訪問済みにしつつ重複を積み続けることです。これは正しいですが、スキップすべき古いエントリがスタックに残ることがあります。
Coddy programming languages illustration

Coddy でアルゴリズムをマスターしよう

始める