0Pricing
DSA Interview Prep · レッスン

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, 2

Snakes 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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. グラフ表現と走査の準備
  2. BFS:最短経路とレベル走査
  3. DFS:連結成分とflood fill
  4. 有向・無向グラフの循環検出
← DSA Interview Prepに戻る