Bài 10: BFS & DFS - Duyệt Đồ Thị¶
Tác giả: FPTOJ Team
Nội dung tham khảo từ: VNOI Wiki - BFS, Cây DFS và ứng dụng, CP-Algorithms
1. Đồ thị là gì?¶
Bản chất vấn đề¶
Thành phố có \(N\) ngã tư (đỉnh), \(M\) con đường (cạnh) nối các ngã tư. Muốn đi từ đỉnh \(A\) đến đỉnh \(B\) — cần thuật toán tìm đường.
Đồ thị là một cặp \(G = (V, E)\) trong đó \(V\) là tập đỉnh, \(E\) là tập cạnh nối giữa các đỉnh.
Các loại đồ thị¶
| Loại | Mô tả | Ví dụ thực tế |
|---|---|---|
| Vô hướng | Cạnh không có chiều | Bạn bè trên Facebook |
| Có hướng | Cạnh có chiều | Theo dõi trên Twitter |
| Có trọng số | Cạnh có giá trị | Bản đồ (khoảng cách) |
| Liên thông | Đi được từ mọi đỉnh đến mọi đỉnh khác | Mạng internet |
| Nhị phân | Chia đỉnh thành 2 tập, cạnh chỉ nối 2 tập khác nhau | Phân công công việc |
Biểu diễn đồ thị¶
Cách 1: Danh sách kề — phổ biến nhất, bộ nhớ \(O(V + E)\).
Cách 2: Ma trận kề — dùng khi cần kiểm tra cạnh \(O(1)\), bộ nhớ \(O(V^2)\).
So sánh hai cách biểu diễn¶
| Tiêu chí | Danh sách kề | Ma trận kề |
|---|---|---|
| Bộ nhớ | \(O(V + E)\) | \(O(V^2)\) |
| Kiểm tra cạnh | \(O(\text{degree})\) | \(O(1)\) |
| Duyệt đỉnh kề | \(O(\text{degree})\) | \(O(V)\) |
| Phổ biến | Rất phổ biến | Ít dùng |
Quy tắc: Luôn dùng danh sách kề trừ khi cần kiểm tra cạnh nhanh \(O(1)\).
2. BFS — Duyệt theo chiều rộng¶
Bản chất vấn đề¶
Cho đồ thị \(G = (V, E)\) và đỉnh xuất phát \(s\). Muốn thăm tất cả đỉnh reachable từ \(s\), đồng thời tính khoảng cách ngắn nhất (theo số cạnh) từ \(s\) đến mọi đỉnh.
Tư duy cốt lõi¶
BFS sử dụng hàng đợi (queue) — cấu trúc dữ liệu FIFO (vào trước, ra trước).
Ẩn dụ: Thả hòn đá xuống hồ — sóng lan đồng đều. BFS cũng vậy: duyệt đỉnh cách 1 bước trước, rồi 2 bước, 3 bước...
Các bước thực hiện:
- Cho đỉnh nguồn \(s\) vào hàng đợi, đánh dấu đã thăm
- Lấy đỉnh đầu hàng đợi ra → duyệt tất cả đỉnh kề chưa thăm
- Đánh dấu đã thăm, tính khoảng cách, cho vào hàng đợi
- Lặp lại cho đến khi hàng đợi rỗng
Đặc điểm: BFS luôn thăm đỉnh theo thứ tự tầng (level) — đỉnh cách 1 bước trước, rồi 2 bước, 3 bước...
Cài đặt¶
Phân tích tính đúng đắn¶
Tại sao BFS cho khoảng cách ngắn nhất trên đồ thị không trọng số?
Gọi \(d(v)\) là khoảng cách ngắn nhất từ \(s\) đến \(v\) (theo số cạnh).
Bất biến quan trọng: Khi đỉnh \(v\) được lấy ra khỏi hàng đợi, \(d(v)\) đã được xác định đúng.
Chứng minh bằng induction:
- Base: \(d(s) = 0\) — đúng vì \(s\) là đỉnh xuất phát.
- Inductive step: Giả sử mọi đỉnh có khoảng cách \(\leq k\) đã được xác định đúng. Xét đỉnh \(v\) có \(d(v) = k + 1\). Khi đó tồn tại đỉnh \(u\) với \(d(u) = k\) sao cho \((u, v) \in E\). Theo giả thuyết induction, \(u\) đã được xác định đúng. Khi \(u\) được duyệt, \(v\) sẽ được thăm và \(d(v) = d(u) + 1 = k + 1\).
Vì BFS duyệt theo tầng (tất cả đỉnh cách \(k\) bước trước đỉnh cách \(k + 1\) bước), nên không có đỉnh cách ngắn hơn bị bỏ qua.
Đánh giá độ phức tạp¶
- Thời gian: \(O(V + E)\) — mỗi đỉnh được lấy ra khỏi hàng đợi đúng 1 lần, mỗi cạnh được xét đúng 2 lần (với đồ thị vô hướng).
- Bộ nhớ: \(O(V)\) — hàng đợi chứa tối đa \(V\) đỉnh, mảng
visited,dist,parentmỗi mảng \(O(V)\).
Minh họa từng bước¶
| Bước | Hành động | Hàng đợi | Khoảng cách |
|---|---|---|---|
| 1 | Lấy 1, thêm kề 2, 3 | \([2, 3]\) | \(d[1]=0\) |
| 2 | Lấy 2, thêm kề 4, 5 | \([3, 4, 5]\) | \(d[2]=1\) |
| 3 | Lấy 3, kề 5 đã thăm | \([4, 5]\) | \(d[3]=1\) |
| 4 | Lấy 4, không kề mới | \([5]\) | \(d[4]=2\) |
| 5 | Lấy 5, không kề mới | \([]\) | \(d[5]=2\) |
Thứ tự thăm: \(1 \to 2 \to 3 \to 4 \to 5\).
3. DFS — Duyệt theo chiều sâu¶
Bản chất vấn đề¶
Cho đồ thị \(G = (V, E)\) và đỉnh xuất phát \(s\). Muốn thăm tất cả đỉnh reachable từ \(s\) bằng cách đi sâu hết mức có thể trước khi quay lui.
Tư duy cốt lõi¶
DFS sử dụng ngăn xếp (stack) — có thể cài đặt trực tiếp bằng đệ quy.
Ẩn dụ: Khám phá hang động — tại mỗi ngã rẽ, chọn 1 đường đi sâu hết mức. Đến đường cụt → quay lui → thử đường khác.
Các bước thực hiện:
- Đánh dấu đỉnh hiện tại \(u\) đã thăm
- Duyệt tất cả đỉnh kề \(v\) chưa thăm
- Gọi đệ quy \(\text{DFS}(v)\)
- Khi không còn đỉnh kề nào chưa thăm → quay lui
Đặc điểm: DFS đi sâu hết mức có thể trước khi quay lại. Không đảm bảo tìm đường ngắn nhất.
Cài đặt¶
Phân tích tính đúng đắn¶
Tại sao DFS thăm đúng tất cả đỉnh reachable từ \(s\)?
DFS thực hiện duyệt trên cây DFS (DFS Tree) — một cây spanning của thành phần liên thông chứa \(s\).
Chứng minh:
-
Mọi đỉnh reachable đều được thăm: Giả sử tồn tại đỉnh \(v\) reachable từ \(s\) mà không được thăm. Khi đó tồn tại đường đi \(s = v_0, v_1, \ldots, v_k = v\). Vì \(v_0 = s\) được thăm, và nếu \(v_i\) được thăm thì \(v_{i+1}\) cũng sẽ được thăm (vì \(v_{i+1}\) là đỉnh kề của \(v_i\) chưa thăm). Theo induction, \(v\) phải được thăm — mâu thuẫn.
-
Mỗi đỉnh thăm đúng 1 lần: Nhờ đánh dấu
visitedtrước khi duyệt kề. -
Cây DFS không có chu trình: Vì chỉ đi đến đỉnh chưa thăm, mỗi đỉnh có đúng 1 cha trong cây DFS.
Đánh giá độ phức tạp¶
- Thời gian: \(O(V + E)\) — mỗi đỉnh được thăm đúng 1 lần, mỗi cạnh được xét đúng 2 lần (vô hướng) hoặc 1 lần (có hướng).
- Bộ nhớ: \(O(V)\) — stack đệ quy tối đa \(V\) tầng, mảng
visited\(O(V)\).
Minh họa từng bước¶
| Bước | Hành động | Đệ quy stack | Ghi chú |
|---|---|---|---|
| 1 | Thăm 1 → gọi DFS(2) | \([1]\) | Đi sâu |
| 2 | Thăm 2 → gọi DFS(4) | \([1, 2]\) | Đi sâu |
| 3 | Thăm 4 → quay lui | \([1, 2, 4]\) | Đỉnh lá |
| 4 | Quay 2 → gọi DFS(5) | \([1, 2]\) | Thử nhánh khác |
| 5 | Thăm 5 → gọi DFS(3) | \([1, 2, 5]\) | Đi sâu |
| 6 | Thăm 3 → quay lui | \([1, 2, 5, 3]\) | Kề 1, 5 đã thăm |
| 7 | Quay hết → xong | \([]\) | Hoàn thành |
Thứ tự thăm: \(1 \to 2 \to 4 \to 5 \to 3\).
4. So sánh BFS vs DFS¶
| Tiêu chí | BFS | DFS |
|---|---|---|
| Cấu trúc dữ liệu | Queue (FIFO) | Stack / Đệ quy |
| Thứ tự duyệt | Lan rộng theo "tầng" | Đi sâu rồi quay lui |
| Đường ngắn nhất | Có (không trọng số) | Không đảm bảo |
| Bộ nhớ | \(O(V)\) | \(O(V)\) |
| Thời gian | \(O(V + E)\) | \(O(V + E)\) |
Khi nào dùng BFS?¶
- Cần tìm đường ngắn nhất trong đồ thị không trọng số
- Duyệt theo tầng (level-order)
- Bài toán trên lưới 2D (tìm đường, flood fill)
Khi nào dùng DFS?¶
- Kiểm tra chu trình
- Sắp xếp tô-pô
- Tìm thành phần liên thông
- Bài toán cần quay lui (backtracking)
- Đồ thị rất sâu (ít tốn bộ nhớ hơn BFS)

5. Ứng dụng thực tế¶
5.1. Tìm đường ngắn nhất trong lưới (BFS)¶
Bài toán: Cho lưới \(N \times M\), tìm đường đi ngắn nhất từ \(A\) đến \(B\) (tránh vật cản).
Đánh giá độ phức tạp: Thời gian \(O(N \times M)\), bộ nhớ \(O(N \times M)\).
Ứng dụng: Game tìm đường (Pacman, RPG), robot di chuyển, GPS.
5.2. Đếm thành phần liên thông (DFS/BFS)¶
Bài toán: Cho đồ thị có \(N\) đỉnh, đếm số nhóm liên thông.
Đánh giá độ phức tạp: Thời gian \(O(V + E)\), bộ nhớ \(O(V)\).
5.3. Kiểm tra đồ thị nhị phân (BFS/DFS)¶
Bài toán: Kiểm tra đồ thị có thể chia thành 2 tập sao cho cạnh chỉ nối 2 tập khác nhau không.
Tư duy: Tô màu 2 màu xen kẽ. Nếu đỉnh kề cùng màu → không nhị phân.
Đánh giá độ phức tạp: Thời gian \(O(V + E)\), bộ nhớ \(O(V)\).
5.4. Phát hiện chu trình (DFS)¶
Bài toán: Kiểm tra đồ thị có chứa chu trình không.
Đồ thị vô hướng: Khi DFS gặp đỉnh đã thăm mà không phải cha → có chu trình.
Đồ thị có hướng: Dùng 3 trạng thái — 0: chưa thăm, 1: đang thăm, 2: đã xong.
Đánh giá độ phức tạp: Thời gian \(O(V + E)\), bộ nhớ \(O(V)\).
5.5. Sắp xếp tô-pô (DFS)¶
Bài toán: Với DAG (đồ thị có hướng không chu trình), tìm thứ tự tuyến tính sao cho nếu có cạnh \(u \to v\) thì \(u\) đứng trước \(v\).
Đánh giá độ phức tạp: Thời gian \(O(V + E)\), bộ nhớ \(O(V)\).
Ứng dụng: Sắp xếp thứ tự học môn (môn tiên quyết), build hệ thống, xử lý dependency.
5.6. Flood Fill (BFS/DFS)¶
Bài toán: Cho lưới 2D, tô màu tất cả ô liên thông cùng màu với ô xuất phát.
Đánh giá độ phức tạp: Thời gian \(O(N \times M)\), bộ nhớ \(O(N \times M)\).
Ứng dụng: Paint bucket tool, đếm số vùng trong ảnh, game Minesweeper.
6. Cạm bẫy hay gặp¶
Bẫy 1: Quên đánh dấu đã thăm¶
Bẫy 2: Đệ quy quá sâu → Stack Overflow¶
DFS đệ quy với \(N = 100.000\) có thể tràn stack.
Khắc phục: Dùng DFS iterative (stack) hoặc tăng giới hạn đệ quy.
Bẫy 3: BFS trên đồ thị có trọng số¶
BFS chỉ cho đường ngắn nhất trên đồ thị không trọng số. Nếu có trọng số → dùng Dijkstra (xem Bài 13).
Bẫy 4: Quên duyệt tất cả thành phần liên thông¶
Mẹo thi cử¶
| Bài toán | Thuật toán |
|---|---|
| Đường ngắn nhất (không trọng số) | BFS |
| Thành phần liên thông | DFS / BFS |
| Kiểm tra chu trình | DFS |
| Sắp xếp tô-pô | DFS |
| Kiểm tra nhị phân | BFS / DFS |
| Flood Fill | BFS / DFS |
| Đồ thị có trọng số | Dijkstra (Bài 13) |
7. Bài tập luyện tập¶
| Bài | Nền tảng | Độ khó | Chủ đề |
|---|---|---|---|
| CSES - Building Roads | CSES | ⭐ | Thành phần liên thông |
| CSES - Message Route | CSES | ⭐⭐ | BFS đường ngắn nhất |
| CSES - Labyrinth | CSES | ⭐⭐ | BFS trên lưới |
| CSES - Building Teams | CSES | ⭐⭐ | Kiểm tra nhị phân |
| CSES - Round Trip | CSES | ⭐⭐ | Phát hiện chu trình |
| CSES - Course Schedule | CSES | ⭐⭐ | Sắp xếp tô-pô |
| SPOJ - BUGLIFE | SPOJ | ⭐⭐ | Kiểm tra nhị phân |
| LeetCode - Number of Islands | LeetCode | ⭐⭐ | DFS/BFS trên lưới |
| LeetCode - Flood Fill | LeetCode | ⭐ | BFS/DFS cơ bản |
| LeetCode - Course Schedule | LeetCode | ⭐⭐ | Topo sort + DFS |
| LeetCode - Max Area of Island | LeetCode | ⭐⭐ | DFS đếm vùng |
| VNOJ - Gặm cỏ | VNOJ | ⭐⭐ | BFS trên lưới |
Tài liệu tham khảo¶
- CP-Algorithms - BFS
- CP-Algorithms - DFS
- CP-Algorithms - Bipartite Check
- VNOI Wiki - BFS
- VNOI Wiki - Cây DFS và ứng dụng
- USACO Guide - Graph Traversal
- VisuAlgo - Graph Traversal