2025.07.26(土) / 12:24
Pythonで幅優先探索(BFS)の仕組みを理解する
- ID
- 31589
- Published
- 2025-07-26 12:24
- Modified
- 2026-07-17 19:33
- Author
- khiro
- Categories
- デフォルト
上長から、幅優先探索の第一引数に渡すデータを作成してほしいと依頼された。
しかし、その場ですぐに対応することができなかった。
エンジニアとしての実力が可視化された瞬間だった。
実力不足を痛感できる環境にいることを前向きに捉え、ここからスキルアップしていきたい。
手始めに、Pythonで幅優先探索を実装し、その処理の流れを一つずつ確認する。
幅優先探索(BFS)とは
幅優先探索(Breadth-First Search、BFS)は、グラフやツリー構造を探索するアルゴリズムの一つ。
スタート地点に近いノードから、横方向へ広がるように順番に探索していく。
主な特徴は次のとおり。
- 重みのないグラフでは、最短経路を求めるために利用できる
- スタート地点に近いノードから探索する
- キューを使って、次に探索するノードを管理する
- 同じノードを繰り返し処理しないように訪問済みの状態を管理する

今回使用するグラフ
今回は、次のようなノードの接続関係を使用する。
graph = {
"A": ["B", "C"],
"B": ["D", "E"],
"C": ["F"],
"D": [],
"E": ["F"],
"F": [],
}
この辞書は、各ノードから移動できる隣接ノードを表している。
例えば、次のように読み取れる。
- AからはBとCへ移動できる
- BからはDとEへ移動できる
- CからはFへ移動できる
- EからもFへ移動できる
- DとFには、その先の接続がない
イメージすると、次のような構造になる。
A
/ \
B C
/ \ \
D E → F
\→ F
実際にはFは一つのノードだが、CとEの両方から接続されている。
幅優先探索のソースコード
from collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
while queue:
node = queue.popleft()
if node not in visited:
print(f"訪問: {node}")
visited.add(node)
queue.extend(
neighbor
for neighbor in graph[node]
if neighbor not in visited
)
graph = {
"A": ["B", "C"],
"B": ["D", "E"],
"C": ["F"],
"D": [],
"E": ["F"],
"F": [],
}
bfs(graph, "A")
実行結果
訪問: A
訪問: B
訪問: C
訪問: D
訪問: E
訪問: F
Aに近いノードから、B、C、D、E、Fの順番で探索されている。
幅優先探索の基本的な流れ
処理の流れは、大きく次の4段階に分けられる。
- スタートノードをキューに追加する
- キューの先頭からノードを取り出す
- 未訪問であれば訪問済みにし、隣接ノードをキューへ追加する
- キューが空になるまで繰り返す
ここから、それぞれの処理を詳しく確認する。

Step1 : スタートノードをキューに追加する
queue = deque([start])
今回のスタートノードはA。
bfs関数の第二引数として渡した"A"が、startに格納される。


その値をdequeへ渡すことで、最初のキューが作成される。
deque(["A"])
dequeは、Pythonのcollectionsモジュールに含まれるデータ構造。

正式名称はdouble-ended queueで、先頭と末尾の両方から効率よくデータを追加・削除できる。

幅優先探索では、先に追加したデータから取り出すキューとして使用する。
Step1.5 : 訪問済みのノードを管理する
まずは4行目で、訪問済みのノードを格納する空の集合を定義している。

visitedは、すでに訪問したノードを保存する集合。

最初は、まだどのノードにも訪問していないため空になっている。
訪問したノードをここへ追加することで、同じノードを何度も処理することを防ぐ。
set()とは
重複のない要素の集まりを表すデータ型

Step2 : キューからノードを取り出す
8行目でpopleftメソッドでキューから取り出したノードをnodeに格納している。

popleft()は、キューの一番左側にある値を取り出すメソッド。
最初のキューにはAが格納されているため、nodeにはAが入る。
node = "A"
queue = deque([])
取り出したAがまだvisitedに存在しなければ、訪問済みとして追加する。
if node not in visited:
print(f"訪問: {node}")
visited.add(node)
9~11行目でvisitedにまだ格納されていないノードを格納している。

10行目の出力結果

最初に格納されている値

Step3 : 隣接ノードをキューへ追加する
次の処理では、現在のノードに隣接する未訪問のノードをキューへ追加する。
queue.extend(
neighbor
for neighbor in graph[node]
if neighbor not in visited
)

Aの隣接ノードは、BとC。
graph["A"]
実行結果は次のようになる。
["B", "C"]
BとCはまだ訪問していないため、両方がキューへ追加される。
queue = deque(["B", "C"])
Step4 : 探索の流れを具体的に確認する

1回目:Aを取り出す

取り出したノード: A

訪問済み: A

追加するノード: B、C

キュー: B、C

2回目:Bを取り出す
Bの隣接ノードはDとE。

取り出したノード: B
訪問済み: A、B
追加するノード: D、E
キュー: C、D、E
3回目:Cを取り出す
Cの隣接ノードはF。
取り出したノード: C
訪問済み: A、B、C
追加するノード: F
キュー: D、E、F
4回目:Dを取り出す
Dには隣接ノードがない。
取り出したノード: D
訪問済み: A、B、C、D
追加するノード: なし
キュー: E、F
5回目:Eを取り出す
Eの隣接ノードはF。
この時点ではFはキューに入っているが、まだ訪問済みにはなっていない。
そのため、Fがもう一度キューへ追加される。
取り出したノード: E
訪問済み: A、B、C、D、E
追加するノード: F
キュー: F、F
6回目:最初のFを取り出す
取り出したノード: F
訪問済み: A、B、C、D、E、F
追加するノード: なし
キュー: F
7回目:2回目のFを取り出す
Fはすでにvisitedに存在している。
そのため、if node not in visitedの条件を満たさず、処理されない。
取り出したノード: F
すでに訪問済みのためスキップ
キュー: 空
キューが空になったため、whileループが終了する。
なぜFが二つキューに入るのか
最初は、Fが二つ存在しているように見えて混乱した。

しかし、実際にFというノードが二つあるわけではない。
Fには、CとEの両方から到達できる。
C → F
E → F
Cを処理したときにFがキューへ追加され、その後Eを処理したときにもFが追加される。
このコードでは、キューへ追加済みかどうかではなく、訪問済みかどうかを確認している。
Eを処理した時点では、Fはまだキューの中にあるだけで、訪問済みではない。そのため、もう一度追加される。
ただし、最初のFを処理した時点でvisitedへ登録されるため、2回目のFは処理されずに弾かれる。
重複してキューへ追加しない書き方
キューへの重複追加そのものを防ぐ場合は、ノードをキューへ追加する時点で訪問済みにする方法もある。
from collections import deque
def bfs(graph, start):
visited = {start}
queue = deque([start])
while queue:
node = queue.popleft()
print(f"訪問: {node}")
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
この書き方では、Fを最初にキューへ追加した時点でvisitedにも追加する。
そのため、Eを処理するときにはFがすでに登録されており、キューへ重複して追加されない。
こちらの方が、キューに同じノードが何度も入ることを防げる。
まとめ
幅優先探索は、キューを使ってスタート地点に近いノードから順番に探索するアルゴリズム。
今回のコードでは、次の役割を理解できた。
dequeで次に探索するノードを管理するpopleft()でキューの先頭からノードを取り出すvisitedで訪問済みのノードを管理するgraph[node]から隣接ノードを取得する- キューが空になるまで探索を繰り返す
最初はコードを読んでも、何が起きているのか分からなかった。
しかし、キュー、現在のノード、訪問済みノードの中身を一つずつ追いかけることで、処理の流れが見えるようになった。
一つの仕組みを深く理解すると、似たようなコードも理解しやすくなる。
次は、今回理解した幅優先探索を使って、スタート地点からゴール地点までの最短経路を求める。