BFS:最短経路とレベル走査
BFSで重みなしグラフの最短経路を見つけ、word-ladderをレベルごとに解き、ハッシュマップを使ってグラフを複製します。
「BFS:最短経路とレベル走査」はCoddyKit上の無料DSA Interview Prepレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDSA Interview Prep学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DSA Interview Prepコースには全4レッスンが含まれています。
BFSと重みなしグラフの最短経路
BFSは、始点からの距離が増加する順にノードを探索するため、重みなしグラフの最短経路(辺の数が最小の経路)を求められます。BFSでノードに初めて到達したとき、その経路は可能な限り短い経路です。この性質はDFSにはありません。辺の重みが非負の重み付きグラフでは、代わりにDijkstra法を使用します。BFSは暗黙的に、すべての辺の重みを 1 として扱うためです。
from collections import deque, defaultdict
def shortest_path(graph, start, end):
if start == end:
return 0
visited = {start}
queue = deque([(start, 0)]) # (node, distance)
while queue:
node, dist = queue.popleft()
for neighbour in graph[node]:
if neighbour == end:
return dist + 1
if neighbour not in visited:
visited.add(neighbour)
queue.append((neighbour, dist + 1))
return -1 # no path found
graph = defaultdict(list)
for u, v in [(0,1),(1,2),(2,3),(0,3),(1,4)]:
graph[u].append(v); graph[v].append(u)
print(shortest_path(graph, 0, 3)) # 1 (direct edge)
print(shortest_path(graph, 0, 4)) # 2 (0->1->4)実際の最短経路を追跡する
経路の長さだけでなく実際の経路を復元するには、各ノードにどのように到達したかを記録する親辞書を保持します。終点に到達したら、終点から始点まで親マップをたどり、結果を反転します。これにより親マップのために O(V) の空間が追加で必要になりますが、BFS完了後に O(path_length) の時間で完全な経路を取得できます。
from collections import deque, defaultdict
def shortest_path_with_route(graph, start, end):
parent = {start: None}
queue = deque([start])
while queue:
node = queue.popleft()
if node == end:
break
for nb in graph[node]:
if nb not in parent:
parent[nb] = node
queue.append(nb)
if end not in parent:
return [] # no path
# Reconstruct path by tracing back
path = []
node = end
while node is not None:
path.append(node)
node = parent[node]
return path[::-1] # reverse
graph = defaultdict(list)
for u, v in [(0,1),(1,2),(2,3),(0,4),(4,3)]:
graph[u].append(v); graph[v].append(u)
print(shortest_path_with_route(graph, 0, 3)) # [0, 4, 3] or [0, 1, 2, 3]Word Ladder:暗黙グラフ上のBFS
Word Ladder(LeetCode #127)は、すべての中間単語が辞書に含まれているという条件で、始点の単語を終点の単語へ変換するために必要な1文字変更の最小回数を求める問題です。これは、ノードを単語、1文字だけ異なる単語同士を辺とする暗黙グラフ上のBFSです。1文字だけ変更したすべての候補を生成し、それらが単語集合に含まれるかを確認します。BFSにより、変換手順の最小回数が保証されます。
from collections import deque
def word_ladder(begin_word, end_word, word_list):
word_set = set(word_list)
if end_word not in word_set:
return 0
queue = deque([(begin_word, 1)])
visited = {begin_word}
while queue:
word, steps = queue.popleft()
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
new_word = word[:i] + c + word[i+1:]
if new_word == end_word:
return steps + 1
if new_word in word_set and new_word not in visited:
visited.add(new_word)
queue.append((new_word, steps + 1))
return 0
print(word_ladder('hit', 'cog', ['hot','dot','dog','lot','log','cog'])) # 5レベルごとの探索:距離を追跡する
レベルごとの探索では、始点からの距離が同じノードをまとめます。これは、レベルごとの処理が必要な問題で直接役立ちます。距離を追跡するには、キューの要素にタプル (node, dist) として格納する方法と、キューサイズを利用する方法があります。後者では、各レベルの前にキューサイズを記録し、その数だけノードを処理してからレベルカウンターを増やします。どちらの方法でも同じ結果が得られます。
from collections import deque, defaultdict
def bfs_levels(graph, start):
levels = {}
visited = {start}
queue = deque([start])
dist = 0
while queue:
# Process all nodes at current distance
for _ in range(len(queue)):
node = queue.popleft()
levels[node] = dist
for nb in graph[node]:
if nb not in visited:
visited.add(nb)
queue.append(nb)
dist += 1
return levels
graph = defaultdict(list)
for u, v in [(0,1),(0,2),(1,3),(2,3),(3,4)]:
graph[u].append(v); graph[v].append(u)
print(bfs_levels(graph, 0)) # {0:0, 1:1, 2:1, 3:2, 4:3}Clone Graph
Clone Graph(LeetCode #133)は、連結した無向グラフのディープコピーを作成する問題です。BFSと、元のノードを複製先のノードに対応付けるハッシュマップを使用します。ノードを初めて訪問したときにその複製を作成し、マップに追加します。隣接ノードを処理するときは、それらの複製を検索または作成して辺を接続します。ハッシュマップは、訪問済みノードの追跡と元のノードから複製への対応付けという2つの役割を果たします。
from collections import deque
class Node:
def __init__(self, val=0, neighbors=None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
def clone_graph(node):
if not node:
return None
old_to_new = {node: Node(node.val)}
queue = deque([node])
while queue:
curr = queue.popleft()
for nb in curr.neighbors:
if nb not in old_to_new:
old_to_new[nb] = Node(nb.val)
queue.append(nb)
old_to_new[curr].neighbors.append(old_to_new[nb])
return old_to_new[node]
# Build a simple graph: 1 -- 2 -- 3 -- 4 -- 1
n1 = Node(1); n2 = Node(2); n3 = Node(3); n4 = Node(4)
n1.neighbors = [n2, n4]; n2.neighbors = [n1, n3]
n3.neighbors = [n2, n4]; n4.neighbors = [n3, n1]
cloned = clone_graph(n1)
print(cloned.val, [n.val for n in cloned.neighbors]) # 1 [2, 4]双方向BFS
双方向BFSは、始点と終点の両方から同時にBFSを開始し、それぞれの端から1レベルずつ探索します。2つの探索前線が出会ったとき、最短経路が見つかります。大規模なグラフでは、探索空間を O(b^d) から O(2 * b^(d/2)) へ削減できます。ここで b は分岐係数、d は経路長です。これは、大きな辞書を使うWord Ladderのように、深く接続されたグラフで大きな改善になります。
from collections import defaultdict
def word_ladder_bidir(begin, end, word_list):
word_set = set(word_list)
if end not in word_set:
return 0
front, back = {begin}, {end}
visited = {begin, end}
steps = 1
while front and back:
# Always expand the smaller frontier
if len(front) > len(back):
front, back = back, front
next_front = set()
for word in front:
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
nw = word[:i] + c + word[i+1:]
if nw in back: # frontiers met!
return steps + 1
if nw in word_set and nw not in visited:
visited.add(nw)
next_front.add(nw)
front = next_front
steps += 1
return 0
print(word_ladder_bidir('hit','cog',['hot','dot','dog','lot','log','cog'])) # 5重み付きグラフの0-1 BFS
0-1 BFSは、辺の重みが 0 または 1 のみであるグラフを処理します。通常のキューの代わりにdequeを使用し、重み 1 の辺(次のレベル)では末尾に追加し、重み 0 の辺(同じレベル)では先頭に追加します。これにより、最短経路を O(V + E) で計算できます。重みが二値の場合、Dijkstra法の O((V+E) log V) より高速です。一部の移動が無料で、その他の移動に 1 のコストがかかるグリッド問題でよく使われます。
from collections import deque
def zero_one_bfs(graph, start, n):
# graph: list of (neighbour, weight) where weight is 0 or 1
dist = [float('inf')] * n
dist[start] = 0
dq = deque([start])
while dq:
node = dq.popleft()
for nb, w in graph[node]:
if dist[node] + w < dist[nb]:
dist[nb] = dist[node] + w
if w == 0:
dq.appendleft(nb) # same level
else:
dq.append(nb) # next level
return dist
# Simple test:
graph = [[(1, 0), (2, 1)], # node 0: free to 1, cost 1 to 2
[(3, 1)], # node 1: cost 1 to 3
[(3, 0)], # node 2: free to 3
[]]
print(zero_one_bfs(graph, 0, 4)) # [0, 0, 1, 1]Walls and Gates(マルチソースBFS)
Walls and Gatesは、各空の部屋に最寄りのゲートまでの距離を設定する問題です。マルチソースBFSを使用し、すべてのゲート(値 0)を同時にキューへ初期化して外側へ探索します。各セルの値は、最初に到達したレベルに設定します。この O(mn) の解法は、各空の部屋から個別にBFSを実行する O(m²n²) の方法より効率的です。
from collections import deque
def walls_and_gates(rooms):
if not rooms:
return
rows, cols = len(rooms), len(rooms[0])
INF = float('inf')
queue = deque()
# Multi-source: all gates at distance 0
for r in range(rows):
for c in range(cols):
if rooms[r][c] == 0: # gate
queue.append((r, c))
dirs = [(0,1),(0,-1),(1,0),(-1,0)]
while queue:
r, c = queue.popleft()
for dr, dc in dirs:
nr, nc = r+dr, c+dc
if 0<=nr<rows and 0<=nc<cols and rooms[nr][nc]==INF:
rooms[nr][nc] = rooms[r][c] + 1
queue.append((nr, nc))
rooms = [[float('inf'),-1,0,float('inf')],
[float('inf'),float('inf'),float('inf'),-1],
[float('inf'),-1,float('inf'),-1],
[0,-1,float('inf'),float('inf')]]
walls_and_gates(rooms)
print(rooms[0][0], rooms[1][1]) # 3, 2Snakes and LaddersのBFS
Snakes and Ladders(LeetCode #909)は、数値グリッド上のBFSによる最短経路問題です。盤面を重みなしグラフとしてモデル化します。各マスから 1~6 を振って進み、ヘビやはしごに到達すると別のマスへ移動します。BFSによって、サイコロを振る回数の最小値を求められます。主な課題は、牛耕式(行の方向が交互になる)配置を考慮しながら、1次元の位置と2次元の盤面座標を変換することです。
from collections import deque
def snakes_and_ladders(board):
n = len(board)
def get_board(pos):
r, c = divmod(pos - 1, n)
if r % 2 == 1: c = n - 1 - c # alternating direction
return board[n - 1 - r][c]
visited = {1}
queue = deque([(1, 0)])
while queue:
pos, moves = queue.popleft()
for dice in range(1, 7):
next_pos = pos + dice
if next_pos > n * n:
break
val = get_board(next_pos)
if val != -1:
next_pos = val # snake or ladder
if next_pos == n * n:
return moves + 1
if next_pos not in visited:
visited.add(next_pos)
queue.append((next_pos, moves + 1))
return -1
print('BFS models game as an unweighted shortest-path problem')BFSの計算量と最適化
BFSの時間計算量はO(V + E)です。各頂点がキューに追加されるのは1回で、各辺は一定回数だけ調べられるためです。空間計算量は、訪問済み集合とキューのためのO(V)です。グリッドグラフでは V = m*n、E = 4*m*n(各セルに4つの隣接セルがある)なので、グリッド上のBFSは O(mn) です。重要な最適化は、訪問済みの検索に O(n) かかるリストではなく、O(1) で検索できる集合を使用することです。訪問済みとしてマークするのは、キューから取り出すときではなく、追加するときにします。
# BFS on a graph with V vertices and E edges:
# Time: O(V + E) -- each vertex and edge visited once
# Space: O(V) -- visited set + queue
# BFS on an m x n grid:
# V = m*n cells
# E <= 4*m*n edges (4 directions, max)
# Time: O(m*n)
# Space: O(m*n)
# Common pitfalls:
# 1. Marking visited on dequeue (not enqueue) -> same node queued multiple times
# 2. Using a list for visited -> O(n) membership check -> O(V*E) total
# 3. Not handling disconnected graph -> BFS from single source misses components
print('O(V+E) time, O(V) space -- mark visited on enqueue')二値行列の最寄りの0
01 Matrix(LeetCode #542)は、各セルから最寄りの 0 までの距離を求めます。すべての 0 から同時に開始するマルチソースBFSにより、最適な O(mn) の解法が得られます。すべての 0 セルを距離 0 でキューに初期化し、すべての 1 セルの距離を無限大に設定します。BFSは 0 から外側へ距離を伝播させ、各 1 セルが初めて到達されたときに距離を設定します。これが最短距離であることは保証されます。
from collections import deque
def update_matrix(mat):
rows, cols = len(mat), len(mat[0])
dist = [[float('inf')] * cols for _ in range(rows)]
queue = deque()
for r in range(rows):
for c in range(cols):
if mat[r][c] == 0:
dist[r][c] = 0
queue.append((r, c))
dirs = [(0,1),(0,-1),(1,0),(-1,0)]
while queue:
r, c = queue.popleft()
for dr, dc in dirs:
nr, nc = r+dr, c+dc
if 0<=nr<rows and 0<=nc<cols:
if dist[r][c] + 1 < dist[nr][nc]:
dist[nr][nc] = dist[r][c] + 1
queue.append((nr, nc))
return dist
mat = [[0,0,0],[0,1,0],[1,1,1]]
result = update_matrix(mat)
for row in result: print(row) # [[0,0,0],[0,1,0],[1,2,1]]理解度チェック
このレッスンで扱った Data Structures & Algorithms — Coding Interview Prep の概念について、理解度を確認します。
レッスンのまとめ
このレッスンでは、親ノードの追跡による経路復元を含む、重みなしグラフの最短経路探索のBFS、暗黙グラフ上の典型的なBFSとしてのWord Ladder、大規模グラフ向けの双方向BFS、複数の始点がある問題向けのマルチソースBFSを学びました。次は、DFSを使って連結成分とフラッドフィルを扱います。
よくある質問
「BFS:最短経路とレベル走査」レッスンは無料ですか?
はい。「BFS:最短経路とレベル走査」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、DSA Interview Prepコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DSA Interview Prepコースには全4レッスンが含まれています。
「BFS:最短経路とレベル走査」で何を学びますか?
BFSで重みなしグラフの最短経路を見つけ、word-ladderをレベルごとに解き、ハッシュマップを使ってグラフを複製します。 ブラウザで直接実行するハンズオンコードでDSA Interview Prepを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
DSA Interview Prepを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのDSA Interview Prepは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「BFS:最短経路とレベル走査」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このDSA Interview Prepレッスンでコードを書いて実行できますか?
はい。すべてのDSA Interview Prepレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- グラフ表現と走査の準備
- BFS:最短経路とレベル走査
- DFS:連結成分とflood fill
- 有向・無向グラフの循環検出