Khái niệm
Duyệt theo chiều sâu (Depth-First Search — DFS) khám phá một nhánh đồ thị/cây đến hết trước khi quay lại và thử nhánh khác. Cơ chế này tự nhiên được cài đặt bằng đệ quy hoặc stack.
Ứng dụng phổ biến:
- Duyệt toàn bộ đồ thị/cây
- Phát hiện chu trình
- Tìm thành phần liên thông
- Tô màu đồ thị (kiểm tra hai phía)
- Bài toán backtracking (quay lui)
Biểu diễn đồ thị
Đồ thị có thể lưu dưới dạng danh sách kề (adjacency list):
# Đồ thị vô hướng — danh sách kề
do_thi = {
0: [1, 2],
1: [0, 3, 4],
2: [0],
3: [1],
4: [1, 5],
5: [4]
}
# 0
# / \
# 1 2
# / \
# 3 4
# |
# 5
Cài đặt DFS bằng đệ quy
def dfs_de_quy(do_thi, u, da_tham=None):
if da_tham is None:
da_tham = set()
da_tham.add(u)
print(u, end=" ")
for v in do_thi.get(u, []):
if v not in da_tham:
dfs_de_quy(do_thi, v, da_tham)
return da_tham
do_thi = {0:[1,2], 1:[0,3,4], 2:[0], 3:[1], 4:[1,5], 5:[4]}
print("Thứ tự duyệt DFS từ đỉnh 0:")
dfs_de_quy(do_thi, 0)
# 0 1 3 4 5 2
Cài đặt DFS bằng stack (không đệ quy)
def dfs_stack(do_thi, start):
da_tham = set()
stack = [start]
thu_tu = []
while stack:
u = stack.pop()
if u in da_tham:
continue
da_tham.add(u)
thu_tu.append(u)
for v in reversed(do_thi.get(u, [])): # Đảo để giữ thứ tự trái-phải
if v not in da_tham:
stack.append(v)
return thu_tu
do_thi = {0:[1,2], 1:[0,3,4], 2:[0], 3:[1], 4:[1,5], 5:[4]}
print(dfs_stack(do_thi, 0)) # [0, 1, 3, 4, 5, 2]
Bài tập luyện tập
Dễ — Đếm số thành phần liên thông
Đếm số “nhóm” tách biệt trong đồ thị — mỗi lần DFS từ một đỉnh chưa thăm là một thành phần mới.
def dem_thanh_phan_lien_thong(n, canh):
"""
n: số đỉnh (0 đến n-1)
canh: [(u, v)] — danh sách cạnh vô hướng
"""
do_thi = {i: [] for i in range(n)}
for u, v in canh:
do_thi[u].append(v)
do_thi[v].append(u)
da_tham = set()
so_thanh_phan = 0
for u in range(n):
if u not in da_tham:
so_thanh_phan += 1
# DFS từ u
stack = [u]
while stack:
node = stack.pop()
if node not in da_tham:
da_tham.add(node)
for v in do_thi[node]:
if v not in da_tham:
stack.append(v)
return so_thanh_phan
print(dem_thanh_phan_lien_thong(5, [(0,1),(1,2),(3,4)])) # 2
print(dem_thanh_phan_lien_thong(4, [])) # 4
print(dem_thanh_phan_lien_thong(3, [(0,1),(1,2),(2,0)])) # 1
Trung bình — DFS trên lưới 2D (flood fill)
Lan màu từ ô nguồn — thay toàn bộ vùng liên thông cùng màu thành màu mới.
def flood_fill(luoi, sr, sc, mau_moi):
"""
Lan màu từ (sr, sc). Tất cả ô liên thông có cùng màu gốc
đều được đổi thành mau_moi.
"""
rows, cols = len(luoi), len(luoi[0])
mau_goc = luoi[sr][sc]
if mau_goc == mau_moi:
return luoi
def dfs(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols:
return
if luoi[r][c] != mau_goc:
return
luoi[r][c] = mau_moi
dfs(r + 1, c); dfs(r - 1, c)
dfs(r, c + 1); dfs(r, c - 1)
dfs(sr, sc)
return luoi
luoi = [
[1, 1, 1],
[1, 1, 0],
[1, 0, 1]
]
print(flood_fill(luoi, 1, 1, 2))
# [[2, 2, 2],
# [2, 2, 0],
# [2, 0, 1]]
Khó — Tìm tất cả đường đi từ nguồn đến đích
def tim_all_paths(do_thi, start, end):
"""
Tìm tất cả đường đi từ start đến end trong đồ thị có hướng.
Dùng DFS + backtracking.
"""
all_paths = []
def dfs(u, path, da_tham):
if u == end:
all_paths.append(path[:]) # Lưu bản sao đường đi
return
for v in do_thi.get(u, []):
if v not in da_tham:
da_tham.add(v)
path.append(v)
dfs(v, path, da_tham)
path.pop() # Backtrack
da_tham.remove(v)
dfs(start, [start], {start})
return all_paths
do_thi = {0:[1,2], 1:[3], 2:[3], 3:[]} # Đồ thị có hướng
paths = tim_all_paths(do_thi, 0, 3)
for p in paths:
print(p)
# [0, 1, 3]
# [0, 2, 3]
Kết luận
DFS là nền tảng cho vô số thuật toán đồ thị nâng cao: topological sort, phát hiện chu trình, Tarjan’s SCC, và nhiều bài backtracking (N-Queens, sudoku solver, tổ hợp-hoán vị). Để học sâu hơn về ứng dụng DFS trên đồ thị thực tế, xem Sắp xếp tô pô trong mục Lý thuyết đồ thị. Tiếp theo hãy học BFS — tìm đường đi ngắn nhất trên đồ thị không có trọng số.
Bình luận