BFS: เส้นทางสั้นที่สุดและการท่องตามระดับ
ใช้ BFS ค้นหาเส้นทางสั้นที่สุดในกราฟที่ไม่มีน้ำหนัก แก้โจทย์บันไดคำทีละระดับ และโคลนกราฟด้วยแผนผังแฮช
BFS: เส้นทางสั้นที่สุดและการท่องตามระดับ เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
BFS และเส้นทางสั้นที่สุดในกราฟที่ไม่มีน้ำหนัก
BFS หาเส้นทางสั้นที่สุด (มีเส้นเชื่อมน้อยที่สุด) ในกราฟที่ไม่มีน้ำหนักได้ เนื่องจาก BFS สำรวจโหนดตามลำดับระยะทางจากต้นทางที่เพิ่มขึ้น ครั้งแรกที่ BFS ไปถึงโหนดใด โหนดนั้นจะถูกเข้าถึงผ่านเส้นทางที่สั้นที่สุดเท่าที่เป็นไปได้ คุณสมบัตินี้ไม่เป็นจริงสำหรับ DFS สำหรับกราฟถ่วงน้ำหนักที่มีน้ำหนักไม่ติดลบ ให้ใช้อัลกอริทึมของไดค์สตราแทน เนื่องจาก 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) เพิ่มเติมสำหรับแผนที่โหนดก่อนหน้า แต่ให้เส้นทางทั้งหมดได้ในเวลา O(ความยาวเส้นทาง) หลังจาก BFS ทำงานเสร็จ
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]บันไดคำศัพท์: BFS บนกราฟโดยนัย
บันไดคำศัพท์ (LeetCode #127) ถามหาจำนวนการเปลี่ยนอักขระทีละหนึ่งครั้งที่น้อยที่สุด เพื่อแปลงคำเริ่มต้นเป็นคำปลายทาง โดยคำระหว่างทางทุกคำต้องอยู่ในพจนานุกรม นี่คือการใช้ BFS บนกราฟโดยนัย ซึ่งมีคำเป็นโหนด และมีเส้นเชื่อมระหว่างคำที่แตกต่างกันเพียงหนึ่งตัวอักษร ให้สร้างคำที่เปลี่ยนตัวอักษรทีละหนึ่งตำแหน่งทั้งหมด แล้วตรวจสอบว่าคำเหล่านั้นอยู่ในเซตคำศัพท์หรือไม่ 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}การโคลนกราฟ
การโคลนกราฟ (LeetCode #133) สร้างสำเนาเชิงลึกของกราฟไม่มีทิศทางที่เชื่อมต่อกัน ให้ใช้ BFS และแผนที่แฮชที่จับคู่โหนดต้นฉบับกับโคลนของโหนดนั้น เมื่อเยี่ยมชมโหนดเป็นครั้งแรก ให้สร้างโคลนของโหนดนั้นและเพิ่มลงในแผนที่ เมื่อประมวลผลโหนดเพื่อนบ้าน ให้ค้นหาหรือสร้างโคลนของโหนดเหล่านั้น แล้วเชื่อมเส้นเชื่อมเข้าด้วยกัน แผนที่แฮชมีหน้าที่สองอย่าง คือใช้ติดตามโหนดที่เยี่ยมชมแล้วและจับคู่โหนดต้นฉบับกับสำเนา
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 จากทั้งต้นทางและปลายทางพร้อมกัน โดยขยายทีละระดับจากแต่ละด้าน เมื่อแนวหน้าทั้งสองมาบรรจบกัน ก็จะพบเส้นทางสั้นที่สุด สำหรับกราฟขนาดใหญ่ วิธีนี้ลดพื้นที่ค้นหาจาก O(b^d) เหลือ O(2 * b^(d/2)) โดยที่ b คือปัจจัยการแตกแขนง และ d คือความยาวเส้นทาง ซึ่งเป็นการปรับปรุงอย่างมากสำหรับกราฟที่เชื่อมโยงกันอย่างลึก เช่น บันไดคำศัพท์ที่มีพจนานุกรมขนาดใหญ่
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'])) # 50-1 BFS สำหรับกราฟถ่วงน้ำหนัก
0-1 BFSใช้จัดการกราฟที่น้ำหนักของเส้นเชื่อมมีเพียง 0 หรือ 1 แทนที่จะใช้คิวทั่วไป ให้ใช้คิวสองด้าน: เพิ่มเข้าไปด้านหลังสำหรับเส้นเชื่อมที่มีน้ำหนัก 1 (ระดับถัดไป) และเพิ่มเข้าไปด้านหน้าสำหรับเส้นเชื่อมที่มีน้ำหนัก 0 (ระดับเดียวกัน) วิธีนี้คำนวณเส้นทางสั้นที่สุดได้ในเวลา O(V + E) ซึ่งเร็วกว่าการคำนวณของไดค์สตราที่ใช้เวลา 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]กำแพงและประตู (BFS หลายต้นทาง)
กำแพงและประตูเติมค่าระยะทางจากประตูที่ใกล้ที่สุดลงในห้องว่างแต่ละห้อง ให้ใช้ 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, 2BFS ของงูและบันได
งูและบันได (LeetCode #909) เป็นโจทย์หาเส้นทางสั้นที่สุดด้วย BFS บนตารางตัวเลข จำลองกระดานเป็นกราฟไม่มีน้ำหนัก ซึ่งจากแต่ละช่องสามารถทอยลูกเต๋าได้ 1–6 และอาจไปตกบนงูหรือบันไดที่ส่งตัวไปยังตำแหน่งอื่น BFS จะหาจำนวนครั้งในการทอยลูกเต๋าที่น้อยที่สุด ความท้าทายสำคัญคือการแปลงระหว่างตำแหน่งแบบหนึ่งมิติกับพิกัดกระดานสองมิติ โดยคำนึงถึงการจัดเรียงแบบสลับทิศทางของแถว
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) เนื่องจากจุดยอดแต่ละจุดถูกใส่ลงคิวครั้งเดียว และเส้นเชื่อมแต่ละเส้นถูกตรวจสอบจำนวนครั้งคงที่ ความซับซ้อนด้านหน่วยความจำคือ O(V) สำหรับเซตที่เยี่ยมชมแล้วและคิว สำหรับกราฟตาราง V = m*n และ E = 4*m*n (แต่ละเซลล์มีโหนดเพื่อนบ้าน 4 เซลล์) ดังนั้น BFS บนตารางจึงใช้เวลา O(mn) การปรับให้เหมาะสมที่สำคัญคือ ใช้เซตสำหรับติดตามเซลล์ที่เยี่ยมชมแล้ว (ค้นหาได้ในเวลา O(1)) ไม่ใช่รายการ (ค้นหาใช้เวลา O(n)) ให้ทำเครื่องหมายว่าเยี่ยมชมแล้วเมื่อใส่ลงคิว ไม่ใช่เมื่อหยิบออกจากคิว
# 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')ศูนย์ที่ใกล้ที่สุดในเมทริกซ์ไบนารี
เมทริกซ์ 01 (LeetCode #542) หาระยะทางจากแต่ละเซลล์ไปยัง 0 ที่ใกล้ที่สุด การใช้ BFS หลายต้นทางจากเซลล์ 0 ทั้งหมดพร้อมกันให้วิธีแก้ที่เหมาะสมที่สุดในเวลา 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]]ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์เขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้BFS เพื่อหาเส้นทางสั้นที่สุดในกราฟที่ไม่มีน้ำหนัก พร้อมการติดตามโหนดก่อนหน้าเพื่อสร้างเส้นทางกลับคืน บันไดคำศัพท์ในฐานะตัวอย่างมาตรฐานของ BFS บนกราฟโดยนัย BFS สองทิศทางสำหรับกราฟขนาดใหญ่ และBFS หลายต้นทางสำหรับโจทย์ที่มีจุดเริ่มต้นหลายจุด ต่อไป เราจะประยุกต์ใช้ DFS กับองค์ประกอบเชื่อมต่อและการเติมพื้นที่
คำถามที่พบบ่อย
บทเรียน “BFS: เส้นทางสั้นที่สุดและการท่องตามระดับ” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “BFS: เส้นทางสั้นที่สุดและการท่องตามระดับ” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “BFS: เส้นทางสั้นที่สุดและการท่องตามระดับ”
ใช้ BFS ค้นหาเส้นทางสั้นที่สุดในกราฟที่ไม่มีน้ำหนัก แก้โจทย์บันไดคำทีละระดับ และโคลนกราฟด้วยแผนผังแฮช คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “BFS: เส้นทางสั้นที่สุดและการท่องตามระดับ” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การแทนกราฟและการตั้งค่าการท่อง
- BFS: เส้นทางสั้นที่สุดและการท่องตามระดับ
- DFS: องค์ประกอบเชื่อมต่อและการเติมพื้นที่
- การตรวจจับวัฏจักรในกราฟมีทิศทางและไม่มีทิศทาง