コンテンツにスキップ

第11回:グラフ探索(DFS・BFS)

前回は、つながり(グラフ)を隣接行列隣接リストでコンピュータに覚えさせるところまで来た。でもまだ「覚えた」だけ。今日はその上を実際に歩いて、点から点へ探しに行く。

今日の問い: 前回の最後、手で探したのは「A から F行けるか」「最短で何本の辺か」「輪(閉路)はあるか」だった。点が 6 個なら手で追えるが、100 個になったら無理。機械的に、もれなく・むだなく、全部の点を訪ね歩く手順はどう書くか? (例:迷路の全部屋を、見落とさず・二度手間なく回りきるには?)

探し方は大きく 2 通り。奥までしらみ潰しに進む DFS と、近い順に輪を広げる BFS。順に見ていく。


DFS(深さ優先探索)とは

行ける限り奥へ進む。行き止まったら一歩戻って別の道。これを繰り返すと、全部の点に届く(=しらみ潰し)。

Depth First =「深さ優先」= まず深く

アニメーションで見てみよう

可視化ツール⑫ グラフ探索 を開き、アルゴリズムに「深さ優先探索 DFS(スタック)」を選んで動かしてみよう。奥へ潜って、行き止まりで戻る動きを目で追える。

DFS の正体はスタック/再帰

戻るときの順番に注目する。「最後に来た分かれ道」を「最初に」戻る。これは 後入れ先出し(LIFO)= スタック(第4回)そのもの。

だから DFS は再帰で素直に書ける

  • 「自分の隣へ潜って、戻ってきたら次の隣へ」
  • 来た道はシステム(コールスタック)が覚えてくれる(第5回の再帰)

訪問済みマークで再訪を防ぐ

一度訪ねた点にをつける。隣を見たとき、印があればそこへは進まない(素通り)。

「もう来た」を記録するたった 1 つの配列が、探索を終わらせる

演習11-1

下のグラフ(無向)で、もし訪問済みマークを使わずに DFS したら何が起きるか。

graph LR
    A --- B --- C
    D --- E --- F
    A --- D
    B --- E
    C --- F
  • A から始め、最初の数歩を追って、起きることを一言で書け
  • なぜ訪問済みマークが必要かを、自分の言葉で説明せよ

BFS(幅優先探索)とは

開始点から近い順(辺の本数が少ない順)に広げる。まず1 本でいける点、次に2 本でいける点……とで進む。

Breadth First =「幅優先」= まず広く

アニメーションで見てみよう

可視化ツール⑫ グラフ探索 で、アルゴリズムに「幅優先探索 BFS(キュー)」を選んで動かそう。開始点を中心に、波紋のように層が広がる様子が見える。

BFS の正体はキュー

先に見つけた点」を「先に」処理する。これは 先入れ先出し(FIFO)= キュー(第4回)そのもの。

  • 見つけた点を列の後ろに並べ、列のから取り出して隣を見る
  • 開始点からの「層の番号(レベル)」= そこまでの最短の辺数

演習11-2

演習11-1 と同じグラフ(無向)で、A から BFS する。

graph LR
    A --- B --- C
    D --- E --- F
    A --- D
    B --- E
    C --- F
  • 各部屋への最小ステップ数A から最短で何本の辺でたどり着くか)を全部書け
  • A から F へは、最短で何本の辺

考えるヒント

  • 開始点 A0 歩とする。その隣はすべて 1 歩、さらにその先の未訪問の隣は 2 歩……と層で数える。
  • BFS は近い順に広げるので、ある点に初めて着いた層が、そのままその点の最短ステップ数になる(本文「BFS の正体はキュー」)。

DFS vs BFS

DFS(深さ優先) BFS(幅優先)
進み方 奥へ潜って戻る 近い順に層で広げる
必要な道具 スタック/再帰 キュー
余分なメモリ 経路の長さ分(細い) 層の分(太い)
得意なこと 行けるか・連結成分・閉路 最短の辺数(ホップ)

共通点: 訪問済みマークはどちらも必須。隣接リストで隣をたどるのも同じ。

計算量はどちらも O(n + e)

DFS も BFS も、各点を 1 回訪ね、各辺を(無向なら)両端から 2 回見るだけ。だから点の数 n + 辺の数 e に比例O(n + e)

これは隣接リストで「隣だけ」を見るから成り立つ。隣接行列だと、各点で n マス全部を見るので O(n²)(疎なグラフでは損)。前回の「疎なら隣接リスト」が、ここで効いてくる。


探索で「分かること」

DFS・BFS は、ただ全部の点を訪ねるだけではない。訪ね歩く途中で、グラフの性質がいくつも分かる。

連結成分(つながりの塊)

探索を、まだ訪問済みでない点から呼び直す。呼び直した回数 = グラフがいくつの塊に分かれているか(連結成分の数)。

  • 例: 部品の山がいくつの島に分かれているか/友達グループの数

連結性は、わざわざ別に調べる性質ではなく、探索の副産物として出てくる。

閉路検出(輪があるか)

DFS 中、まだ親ではない訪問済みの点へ戻る辺に出会ったら → 輪(閉路)がある証拠

  • 例: 探検で、別の道から印のある部屋に出てしまった = ぐるっと回って戻れる
  • 使い道: 作業の依存関係に堂々巡りがないか(あると進められない)

閉路も、探索しながら見つかるもの

重みなし最短(BFS のレベル)

BFS のレベルは、そのまま「最短の辺数」= 重みなしの最短経路

  • 使い道: 乗り換え回数を最小に/あと何手で詰むか

ただしこれは「1 辺=1」の世界だけ。駅ごとに所要時間が違ったら、「本数」では足りない(次回)。


演習11-3

下のグラフ(無向)について。まず図に描くとやりやすい。

A : B, C
B : A, C
C : A, B, D
D : C
E : F
F : E
  • (a) いくつの塊(連結成分)に分かれているか。確かめ方も一言(探索を何回呼び直したか)
  • (b) 塊の中に、同じ点を 2 度通らず一周して戻れる道(閉路)はあるか。あれば 1 つ書け
  • (c) (b) の閉路を DFS で見つけるとき、「輪がある」と分かる瞬間はどこか(どの辺を見たとき)

考えるヒント

  • (a) A から探索を始めて、届く点を塗っていく。塗り残しの点が出たら? そこから探索を呼び直した回数が塊の数(本文「連結成分」)。

演習11-4(路線で考える)

下の路線図(無向)。駅と、直通でつながる区間(辺)だけを書いた。

中央 : 緑, 桜
緑   : 中央, 終点
桜   : 中央, 港
港   : 桜, 浜
浜   : 港, 終点
終点 : 緑, 浜

ある人が次のアプリを作った: 探索(スタックを使う= DFS の動き)で、最初に見つかった行き方の区間数(辺の本数)を表示する

  • (a) 中央 → 終点 で、このアプリが表示しうる区間数全部挙げよ
    • ヒント: 中央の隣()のどちらを先にたどるかで、最初に見つかる行き方が変わる
  • (b) 中央 → 終点本当の最小は何区間か。アプリは必ずそれを表示するか
  • (c) 必ずとは限らないなら、探索の「次に取り出す点」の選び方をどう変えれば必ず最小になるか

まとめ

  • DFS: 奥へ潜って戻る(スタック/再帰)。到達・連結成分・閉路に強い
  • BFS: 近い順に層で広げる(キュー)。最短の辺数が出る
  • どちらも訪問済みマーク必須隣接リストで隣をたどるO(n + e)
  • 連結性・閉路は別の性質ではなく、探索の副産物として出てきた
  • 第4回(スタック・キュー)・第5回(再帰)・第10回(隣接リスト)が、ここで合流する

次回予告

今日まで: BFS で「最短の辺数(区間数)」が出る。ただし「1 辺=1」の世界。

でも現実は、駅ごとに所要時間が違う。

  • 例: 演習11-4 の路線で 中央-緑 だけ 30 分、他は各 5 分なら、区間数最小の 中央-緑-終点(2 区間=35 分)より、中央-桜-港-浜-終点(4 区間=20 分)の方が速い
  • =「本数が少ない」と「時間が短い」は別物

重みつきの最短は、どう求める? カギは「いまいちばん近い点を確定して広げる」。そして、その「いちばん近い 1 個を最速で取り出す」のが、第10回のヒープ(優先度付きキュー)

次回:最短経路(ダイクストラ法・ベルマンフォード法)