幅優先探索 (BFS)
最終更新
幅優先探索はグラフをレベルごとに探索します。始点ノードから出発し、まずその直接の隣接ノードすべてを訪問し、次にそれらの未訪問の隣接ノードすべてを訪問し、というように進み - 距離が増していく輪の形で外側へ広がります。上の再生を押すと、始点ノードから一層ずつ広がっていく様子を見られます。
BFS は先入れ先出しのキューを使い、これがレベルごとの順序を強制します。ホップ距離の順にノードへ到達するため、BFS は重みなしグラフで最短経路(辺の数が最小)を見つけます。各ノードと辺を一度ずつ訪問するので、O(V + E) 時間で動作します。
時間計算量と空間計算量
| 指標 | 計算量 | 備考 |
|---|---|---|
| 時間 | O(V + E) | 各頂点と辺を一度ずつ訪問 |
| 空間 | O(V) | キューと訪問済み集合、最悪の場合すべてのノード |
| 探索順 | レベルごと | 最も近いノードから、輪の形で |
| 最短経路 | はい(重みなし) | 各ノードへ最小の辺数で到達 |
ステップごと
| ステップ | 何が起こるか |
|---|---|
| 1 | 始点ノードをキューに入れる。 |
| 2 | 先頭のノードを取り出し、訪問済みとして印を付ける。 |
| 3 | その隣接ノードそれぞれを調べる。 |
| 4 | まだ訪問済みでもキューにもない隣接ノードをキューに入れる。 |
| 5 | キューが空になるまで繰り返す。 |
具体例
ノード 0 からこのグラフを探索する。ここで 0-1、0-2、1-3、2-3、2-4 は辺である:
| ステップ | 訪問済み | キュー(フロンティア) |
|---|---|---|
| 開始 | {} | [0] |
0 を取り出す | {0} | [1, 2] |
1 を取り出す | {0, 1} | [2, 3] |
2 を取り出す | {0, 1, 2} | [3, 4] |
3 を取り出す | {0, 1, 2, 3} | [4] |
4 を取り出す | {0, 1, 2, 3, 4} | [](完了) |
BFS を使うべきとき
| 使うべきとき | 避けるべきとき |
|---|---|
| 重みなしグラフで最短経路が必要なとき | 辺に重みがあるとき - 代わりに Dijkstra を使う |
| 目標がおそらく始点の近くにあるとき | グラフが非常に幅広いとき - キューが巨大なフロンティアを保持しうる |
| グラフをレベル順に探索したいとき | 任意のノードに到達できればよいだけのとき、DFS のほうがメモリを節約できる |
| 連結成分や最小ホップ経路を求めるとき | トポロジカルソートや閉路検出が必要なとき - DFS のほうが適する |
Breadth-First Searchのコード
Python, JavaScript, Java, C++, Cによるクリーンで実行可能なBreadth-First Searchの実装です。言語を選んでコードをコピーするか、Coddyプレイグラウンドに読み込んだ状態で開けます。
PythonでのBreadth-First Searchのコード
Python
1from collections import deque2
3
4def bfs(graph, start):5 visited = {start}6 queue = deque([start])7 order = []8 while queue:9 node = queue.popleft()10 order.append(node)11 for neighbor in graph[node]:12 if neighbor not in visited:13 visited.add(neighbor) # mark on enqueue, not dequeue14 queue.append(neighbor)15 return order16
17
18graph = {19 "A": ["B", "C"],20 "B": ["D", "E"],21 "C": ["F"],22 "D": [],23 "E": ["F"],24 "F": [],25}26
27print("BFS order:", " -> ".join(bfs(graph, "A")))JavaScriptでのBreadth-First Searchのコード
JavaScript
1const graph = {2 A: ["B", "C"],3 B: ["D", "E"],4 C: ["F"],5 D: [],6 E: ["F"],7 F: [],8};9
10function bfs(start) {11 const visited = new Set([start]);12 const queue = [start];13 const order = [];14 while (queue.length > 0) {15 const node = queue.shift(); // Dequeue the oldest node16 order.push(node);17 for (const next of graph[node]) {18 if (!visited.has(next)) {19 visited.add(next);20 queue.push(next);21 }22 }23 }24 return order;25}26
27console.log("BFS from A:", bfs("A").join(" -> "));JavaでのBreadth-First Searchのコード
Java
1import java.util.ArrayDeque;2import java.util.List;3import java.util.Queue;4
5public class Main {6 public static void main(String[] args) {7 List<List<Integer>> adj = List.of(8 List.of(1, 2), // neighbors of 09 List.of(0, 3, 4), // neighbors of 110 List.of(0, 5),11 List.of(1),12 List.of(1, 5),13 List.of(2, 4)14 );15 boolean[] visited = new boolean[adj.size()];16 Queue<Integer> queue = new ArrayDeque<>();17 visited[0] = true;18 queue.add(0);19
20 StringBuilder order = new StringBuilder("BFS from 0:");21 while (!queue.isEmpty()) {22 int node = queue.poll();23 order.append(" ").append(node);24 // Mark neighbors when enqueued so nothing enters twice25 for (int next : adj.get(node)) {26 if (!visited[next]) {27 visited[next] = true;28 queue.add(next);29 }30 }31 }32 System.out.println(order);33 }34}C++でのBreadth-First Searchのコード
C++
1#include <iostream>2#include <queue>3#include <vector>4
5void bfs(int start, const std::vector<std::vector<int>>& adj) {6 std::vector<bool> visited(adj.size(), false);7 std::queue<int> frontier;8 visited[start] = true;9 frontier.push(start);10 // Visit nodes level by level11 while (!frontier.empty()) {12 int node = frontier.front();13 frontier.pop();14 std::cout << node << " ";15 for (int next : adj[node]) {16 if (!visited[next]) {17 visited[next] = true;18 frontier.push(next);19 }20 }21 }22}23
24int main() {25 // 6-node undirected graph as an adjacency list26 std::vector<std::vector<int>> adj = {27 {1, 2}, // 028 {0, 3, 4}, // 129 {0, 5}, // 230 {1}, // 331 {1, 5}, // 432 {2, 4}, // 533 };34 std::cout << "BFS from node 0: ";35 bfs(0, adj);36 std::cout << "\n";37 return 0;38}CでのBreadth-First Searchのコード
C
1#include <stdbool.h>2#include <stdio.h>3
4#define N 65
6void bfs(int start, const int adj[N][N]) {7 bool visited[N] = {false};8 int queue[N]; // fixed-size queue: head chases tail9 int head = 0, tail = 0;10 visited[start] = true;11 queue[tail++] = start;12 // Visit nodes level by level13 while (head < tail) {14 int node = queue[head++];15 printf("%d ", node);16 for (int next = 0; next < N; next++) {17 if (adj[node][next] && !visited[next]) {18 visited[next] = true;19 queue[tail++] = next;20 }21 }22 }23}24
25int main(void) {26 // 6-node undirected graph as an adjacency matrix27 int adj[N][N] = {28 {0, 1, 1, 0, 0, 0},29 {1, 0, 0, 1, 1, 0},30 {1, 0, 0, 0, 0, 1},31 {0, 1, 0, 0, 0, 0},32 {0, 1, 0, 0, 0, 1},33 {0, 0, 1, 0, 1, 0},34 };35 printf("BFS from node 0: ");36 bfs(0, adj);37 printf("\n");38 return 0;39}幅優先探索に関するよくある質問
BFS の時間計算量は?
BFS は
O(V + E) 時間で動作します。ここで V は頂点の数、E は辺の数で、各頂点を一度訪問し各辺を一度調べるためです。キューと訪問済み集合のために O(V) の空間を使います。BFS は最短経路を見つけますか?
はい、重みなしグラフでは見つけます。BFS は始点からのホップ距離の順にノードへ到達するため、あるノードに最初に到達するのは辺の数が最小の経路を通ってです。重み付きグラフでは代わりに Dijkstra のアルゴリズムが必要です。
BFS と DFS の違いは?
BFS はキューを使ってレベルごとに(近いものから)探索し、DFS はスタックを使って一つの枝を深く進んでから戻ります。BFS は重みなしの最短経路を見つけ、DFS は幅広いグラフでメモリを節約でき、閉路検出やトポロジカルソートに適します。
Dijkstra のアルゴリズムではなく BFS を使うべきなのはいつですか?
すべての辺のコストが等しいときは BFS を使いましょう。優先度付きキューなしで
O(V + E) 時間で辺の数が最小の経路を見つけるからです。辺の重みが異なるときは Dijkstra のアルゴリズムが必要で、重み付きグラフに素の BFS を実行すると、最小コストではなく最小ホップの経路が得られます。なぜ BFS には訪問済み集合が必要なのですか?
グラフには閉路が含まれることがあるため、訪問済み集合がないと BFS は同じノードを何度もキューに入れて無限にループします。ノードを最初にキューに入れるとき(取り出すときではなく)に印を付けることで、同じノードが異なる隣接ノードによって二度キューに追加されるのも防げます。
ノードはキューに入れるときと取り出すときのどちらで訪問済みにすべきですか?
キューに入れるときに印を付けます。取り出すまで待つと、処理される前に同じノードが異なる隣接ノードによって複数回キューに追加され、メモリと時間を浪費します。キューに入れるときに印を付ければ、各ノードがちょうど一度だけキューに入ることが保証されます。