第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へは、最短で何本の辺か
考えるヒント
- 開始点
Aを 0 歩とする。その隣はすべて 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) 塊の中に、同じ点を 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回のヒープ(優先度付きキュー)。
次回:最短経路(ダイクストラ法・ベルマンフォード法)。