Duyệt đồ thị bằng DFS và BFS
Bài học này giúp em hiểu cách “đi thăm” các đỉnh trong đồ thị. DFS giống như đi sâu vào một con đường trước, còn BFS giống như lan truyền từng lớp từ điểm xuất phát. Đây là nền tảng để giải các bài: tìm đường đi, đếm thành phần liên thông, khoảng cách ngắn nhất trên đồ thị không trọng số.
Mục tiêu bài học
Biết cách dùng DFS và BFS để đi qua các đỉnh bắt đầu từ một đỉnh cho trước.
Nắm được mẫu code C++ và Python với danh sách kề adj.
Không nhầm ngăn xếp/đệ quy của DFS với hàng đợi của BFS.
Biết khi nào dùng BFS để tính khoảng cách ngắn nhất, khi nào dùng DFS để loang thành phần.
Lý thuyết cần nhớ
Giả sử đồ thị được lưu bằng danh sách kề adj. Mỗi adj[u] chứa các đỉnh kề với đỉnh u.
DFS – Depth First Search
DFS là duyệt sâu: đi sâu vào nhánh đầu tiên trước, sau đó quay lui.
- Dùng đệ quy hoặc stack.
- Phù hợp để: đếm thành phần liên thông, kiểm tra chu trình, duyệt cây.
- Không đảm bảo đường đi ngắn nhất trên đồ thị không trọng số.
BFS – Breadth First Search
BFS là duyệt rộng: duyệt lần lượt từng lớp khoảng cách.
- Dùng queue – hàng đợi.
- Phù hợp để: tìm khoảng cách ngắn nhất khi mỗi cạnh có trọng số 1.
- Thường dùng trong bài mê cung, lan truyền, mỗi bước đi 4 hướng.
Bảng phân biệt nhanh
| Tiêu chí | DFS | BFS |
|---|---|---|
| Ý tưởng | Đi sâu hết một nhánh rồi quay lại. | Đi theo từng tầng quanh đỉnh xuất phát. |
| Cấu trúc dữ liệu | Đệ quy hoặc stack. | Queue. |
| Thứ tự trên ví dụ | 1 → 2 → 4 → 5 → 3 → 6 | 1 → 2 → 3 → 4 → 5 → 6 |
| Dùng khi | Duyệt thành phần, chu trình, cây, topo. | Khoảng cách ngắn nhất không trọng số, lan truyền theo bước. |
| Lỗi hay gặp | Quên đánh dấu visited trước khi đi tiếp. | Đánh dấu visited quá muộn làm một đỉnh bị đưa vào queue nhiều lần. |
Mô phỏng trực quan DFS / BFS
Đồ thị ví dụ có các cạnh: (1,2), (1,3), (2,4), (2,5), (3,6). Hãy bấm từng bước để thấy khác biệt.
Cấu trúc đang chờ xử lý
Đã thăm
Nhật ký
sort(adj[u].begin(), adj[u].end()) trong C++ hoặc adj[u].sort() trong Python.
Code mẫu đầy đủ
Hai chương trình dưới đây giữ đúng tinh thần bài gốc: đọc đồ thị vô hướng, duyệt DFS từ đỉnh 1, sau đó reset visited và duyệt BFS từ đỉnh 1.
#include
using namespace std;
int n, m;
vector> adj;
vector visited;
void dfs(int u) {
visited[u] = 1;
cout << u << " ";
for (int v : adj[u]) {
if (!visited[v]) {
dfs(v);
}
}
}
void bfs(int start) {
queue q;
q.push(start);
visited[start] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
cout << u << " ";
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = 1; // đánh dấu ngay khi đưa vào queue
q.push(v);
}
}
}
}
int main() {
cin >> n >> m;
adj.assign(n + 1, {});
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u); // vì đồ thị vô hướng
}
visited.assign(n + 1, 0);
cout << "DFS: ";
dfs(1);
cout << "
BFS: ";
visited.assign(n + 1, 0); // rất quan trọng: reset trước khi BFS
bfs(1);
return 0;
}
from collections import deque
n, m = map(int, input().split())
adj = [[] for _ in range(n + 1)]
for _ in range(m):
u, v = map(int, input().split())
adj[u].append(v)
adj[v].append(u) # vì đồ thị vô hướng
def dfs(u, visited):
visited[u] = True
print(u, end=' ')
for v in adj[u]:
if not visited[v]:
dfs(v, visited)
def bfs(start):
visited = [False] * (n + 1)
q = deque([start])
visited[start] = True
while q:
u = q.popleft()
print(u, end=' ')
for v in adj[u]:
if not visited[v]:
visited[v] = True # đánh dấu ngay khi đưa vào queue
q.append(v)
visited = [False] * (n + 1)
print("DFS:", end=' ')
dfs(1, visited)
print("
BFS:", end=' ')
bfs(1)
visited? Nếu đồ thị có cạnh hai chiều, từ 1 đi sang 2 rồi từ 2 lại quay về 1.
Không có visited, chương trình có thể lặp vô hạn hoặc gọi đệ quy không dừng.
BFS tính khoảng cách ngắn nhất
BFS rất quan trọng vì khi đồ thị không trọng số, BFS cho ta số cạnh ít nhất từ đỉnh bắt đầu đến mỗi đỉnh.
vector dist(n + 1, -1);
queue q;
int start = 1;
dist[start] = 0;
q.push(start);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (dist[v] == -1) {
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
for (int i = 1; i <= n; i++) {
cout << "dist[" << i << "] = " << dist[i] << "
";
}
from collections import deque
dist = [-1] * (n + 1)
start = 1
q = deque([start])
dist[start] = 0
while q:
u = q.popleft()
for v in adj[u]:
if dist[v] == -1:
dist[v] = dist[u] + 1
q.append(v)
for i in range(1, n + 1):
print(f"dist[{i}] = {dist[i]}")
Thực hành nhanh: tự nhập đồ thị
Nhập số đỉnh, danh sách cạnh, chọn BFS hoặc DFS để xem thứ tự duyệt. Công cụ này giúp học sinh kiểm tra nhanh cách thuật toán hoạt động.
Lỗi học sinh hay nhầm
Nhầm BFS với DFS
Thấy đều duyệt đồ thị nên dùng lẫn lộn, dẫn đến sai ở bài cần khoảng cách ngắn nhất.
Cách nhớ
BFS có chữ Breadth: duyệt rộng theo tầng, dùng queue. DFS có chữ Depth: đi sâu, dùng đệ quy/stack.
Quên reset visited
Chạy DFS xong rồi BFS ngay, nhưng các đỉnh vẫn đang bị đánh dấu đã thăm.
Cách sửa
Trước mỗi lần duyệt mới, dùng visited.assign(n + 1, 0) trong C++ hoặc tạo lại list trong Python.
Tưởng duyệt từ 1 là duyệt hết đồ thị
Nếu đồ thị không liên thông, bắt đầu từ 1 chỉ đi được trong thành phần chứa đỉnh 1.
Cách sửa
Muốn duyệt toàn bộ đồ thị, cần vòng lặp for i = 1..n, nếu chưa thăm thì gọi DFS/BFS từ i.
Bài tập luyện tập phân loại rõ
Bài tập được chia thành nhiều nhóm để học sinh luyện đúng kỹ năng, tránh học thuộc code nhưng không biết dùng vào dạng bài nào.
Mức 1: Duyệt cơ bản
- Duyệt DFS từ đỉnh 1, in thứ tự duyệt.
- Duyệt BFS từ đỉnh 1, in thứ tự duyệt.
- In các đỉnh chưa được thăm sau khi duyệt từ 1.
Mức 2: BFS khoảng cách
- Tính khoảng cách từ 1 đến mọi đỉnh.
- Tìm số cạnh ít nhất từ
sđếnt. - In
-1nếu không đi được từsđếnt.
Mức 3: DFS thành phần
- Đếm số thành phần liên thông.
- In kích thước từng thành phần.
- Kiểm tra đồ thị có liên thông không.
Mức 4: So sánh
- Chạy DFS và BFS trên cùng đồ thị.
- Giải thích vì sao thứ tự khác nhau.
- Sắp xếp danh sách kề rồi so sánh lại.
int components = 0;
visited.assign(n + 1, 0);
for (int i = 1; i <= n; i++) {
if (!visited[i]) {
components++;
dfs(i);
}
}
cout << components;
from collections import deque
dist = [-1] * (n + 1)
dist[1] = 0
q = deque([1])
while q:
u = q.popleft()
for v in adj[u]:
if dist[v] == -1:
dist[v] = dist[u] + 1
q.append(v)
print(dist[1:])
Quiz kiểm tra nhanh
Câu 1
Bài yêu cầu “số cạnh ít nhất từ s đến t” trên đồ thị không trọng số. Nên dùng gì?
Câu 2
Chạy DFS xong muốn chạy BFS lại trên cùng đồ thị, thao tác nào rất quan trọng?
Tổng kết
- DFS: đi sâu, dùng đệ quy/stack, mạnh trong bài thành phần liên thông, chu trình, cây.
- BFS: đi rộng theo tầng, dùng queue, mạnh trong bài khoảng cách ngắn nhất không trọng số.
- visited là bắt buộc để tránh duyệt lặp.
- Muốn duyệt hết đồ thị không liên thông, phải thử từ mọi đỉnh chưa được thăm.
➡ Tiếp theo: Thành phần liên thông và đường đi trong đồ thị.
💳 Quét mã ủng hộ tuỳ tâm nhé!