Giới thiệu về DFS
Thuật toán Tìm kiếm theo chiều sâu (Depth First Search – DFS) là một trong những thuật toán phổ biến nhất để duyệt qua đồ thị. DFS hoạt động bằng cách bắt đầu từ một đỉnh nguồn, sau đó khám phá càng xa càng tốt dọc theo một nhánh trước khi quay lui để khám phá các nhánh khác. DFS có thể được áp dụng trên các cấu trúc dữ liệu như cây, đồ thị (cả có hướng và vô hướng), và các bài toán như mê cung.
DFS là nền tảng cho nhiều bài toán quan trọng:
- Tìm các thành phần liên thông trong đồ thị.
- Sắp xếp topo trong đồ thị có hướng.
- Phát hiện chu trình trong đồ thị.
- Giải quyết các bài toán tìm đường (ví dụ: giải mê cung).
- Giải các câu đố logic (ví dụ: Sudoku).
Bây giờ chúng ta sẽ đi sâu vào chi tiết của DFS, bao gồm lý thuyết, các bước thuật toán, các trường hợp sử dụng và cách triển khai.
DFS là gì?
DFS là một phương pháp duyệt qua các đỉnh của đồ thị theo chiến lược tìm kiếm sâu nhất trước. Bắt đầu từ một đỉnh, DFS sẽ khám phá tất cả các đỉnh có thể từ đỉnh đó, di chuyển càng sâu càng tốt trước khi quay lại đỉnh trước đó để khám phá nhánh còn lại.
Các đặc trưng quan trọng của DFS
- Chiến lược duyệt: DFS bắt đầu từ một đỉnh nguồn và khám phá toàn bộ nhánh trước khi chuyển sang nhánh kế tiếp.
- Cấu trúc dữ liệu cần thiết: DFS sử dụng một ngăn xếp (stack) để theo dõi các đỉnh cần khám phá. Nếu triển khai đệ quy, nó sẽ sử dụng ngăn xếp đệ quy của hệ thống.
- Các phương pháp triển khai: DFS có thể được triển khai theo hai cách:
- DFS đệ quy: Dùng đệ quy để quay lui khi không còn đỉnh nào để khám phá.
- DFS lặp: Sử dụng ngăn xếp để thay thế ngăn xếp đệ quy.
Lợi ích và ứng dụng của DFS
DFS có nhiều ứng dụng thực tiễn trong các bài toán đồ thị, cụ thể:
- Phát hiện chu trình trong đồ thị: DFS có thể dễ dàng phát hiện chu trình trong đồ thị có hướng và vô hướng.
- Tìm thành phần liên thông: Trong đồ thị vô hướng, DFS được sử dụng để tìm tất cả các thành phần liên thông.
- Sắp xếp topo: DFS rất hữu ích trong đồ thị có hướng không có chu trình (DAG) để thực hiện sắp xếp topo.
- Giải quyết các bài toán tìm đường: Các bài toán như tìm đường trong mê cung, hoặc tìm tất cả các đường dẫn có thể từ điểm bắt đầu đến điểm kết thúc.
Các bước của thuật toán DFS
Thuật toán DFS đệ quy
DFS được triển khai bằng cách gọi đệ quy qua các đỉnh chưa được thăm, theo các bước như sau:
- Bắt đầu tại đỉnh nguồn.
- Đánh dấu đỉnh hiện tại là đã thăm.
- Duyệt qua tất cả các đỉnh kề chưa được thăm của đỉnh hiện tại.
- Gọi đệ quy DFS cho từng đỉnh kề chưa được thăm.
- Quay lui khi tất cả các đỉnh kề đều đã thăm.
Pseudocode cho DFS đệ quy
def dfs(graph, node, visited):
# Đánh dấu đỉnh hiện tại là đã thăm
visited.add(node)
print(node) # Xử lý đỉnh hiện tại
# Duyệt qua các đỉnh kề
for neighbor in graph[node]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
Ví dụ về đồ thị:
Danh sách kề:
graph = {
'A': ['B', 'C'],
'B': ['D'],
'C': ['E'],
'D': [],
'E': ['F'],
'F': []
}
Thực hiện DFS từ đỉnh ‘A’:
visited = set()
dfs(graph, 'A', visited)
Kết quả in ra:
Thuật toán DFS lặp
Phiên bản DFS lặp sử dụng ngăn xếp thay cho đệ quy. Các bước của DFS lặp như sau:
- Đưa đỉnh nguồn vào ngăn xếp.
- Trong khi ngăn xếp không rỗng:
- Lấy đỉnh ở đầu ngăn xếp.
- Nếu đỉnh chưa được thăm, đánh dấu là đã thăm và xử lý đỉnh.
- Đưa tất cả các đỉnh kề chưa thăm vào ngăn xếp.
Pseudocode cho DFS lặp
def dfs_iterative(graph, start):
stack = [start] # Ngăn xếp chứa đỉnh bắt đầu
visited = set() # Tập chứa các đỉnh đã thăm
while stack:
node = stack.pop() # Lấy đỉnh từ ngăn xếp
if node not in visited:
visited.add(node)
print(node) # Xử lý đỉnh
# Đưa các đỉnh kề chưa thăm vào ngăn xếp
for neighbor in reversed(graph[node]):
if neighbor not in visited:
stack.append(neighbor)
Thực hiện DFS lặp từ đỉnh ‘A’:
dfs_iterative(graph, 'A')
Kết quả in ra:
Phân tích độ phức tạp
Độ phức tạp thời gian
- Độ phức tạp thời gian: O(V + E)
- V là số lượng đỉnh (vertices).
- E là số lượng cạnh (edges).
- DFS duyệt qua mỗi đỉnh và cạnh một lần, do đó độ phức tạp là tuyến tính.
Độ phức tạp không gian
- Đệ quy DFS: O(V) do sử dụng ngăn xếp đệ quy.
- DFS lặp: O(V) do sử dụng ngăn xếp rõ ràng và tập hợp các đỉnh đã thăm.
Ứng dụng của DFS
Phát hiện chu trình trong đồ thị
DFS là một công cụ hữu ích để phát hiện chu trình trong đồ thị có hướng và vô hướng. Nếu có một đỉnh quay trở lại chính nó hoặc quay lại một đỉnh đã xuất hiện trong ngăn xếp hiện tại của DFS, thì tồn tại một chu trình.
Pseudocode phát hiện chu trình trong đồ thị có hướng:
def is_cyclic(graph, node, visited, rec_stack):
visited.add(node)
rec_stack.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
if is_cyclic(graph, neighbor, visited, rec_stack):
return True
elif neighbor in rec_stack:
return True
rec_stack.remove(node)
return False
Sắp xếp topo trong đồ thị
DFS cũng được sử dụng để thực hiện sắp xếp topo trong đồ thị có hướng không chu trình (DAG). Trong sắp xếp topo, các đỉnh được sắp xếp sao cho nếu có cạnh từ đỉnh u đến đỉnh v, thì u sẽ xuất hiện trước v trong thứ tự sắp xếp.
Tìm thành phần liên thông
Trong đồ thị vô hướng, DFS có thể tìm ra các thành phần liên thông, nghĩa là các nhóm đỉnh mà trong đó có thể đi từ bất kỳ đỉnh nào đến đỉnh khác. Mỗi lần DFS bắt đầu từ một đỉnh chưa thăm sẽ khám phá một thành phần liên thông mới.
DFS và BFS: Sự khác biệt
- DFS (Depth First Search – Tìm kiếm theo chiều sâu): Duyệt sâu nhất có thể, thích hợp với các bài toán cần khám phá toàn bộ đường dẫn trước khi đưa ra kết luận.
- BFS (Breadth First Search – Tìm kiếm theo chiều rộng): Duyệt theo từng lớp từ đỉnh gần đến xa, phù hợp với bài toán tìm đường ngắn nhất trong đồ thị không có trọng số.
Kết luận
DFS là một thuật toán cơ bản, nhưng vô cùng mạnh mẽ trong việc giải quyết các bài toán đồ thị. Nó có thể được triển khai dễ dàng cả theo cách đệ quy lẫn cách lặp. DFS là công cụ cần thiết cho các bài toán phát hiện chu trình, tìm thành phần liên thông, và các bài toán tìm đường trong nhiều ứng dụng thực tiễn khác.