Khoá học C++ cơ bản
Bài học: Lý Thuyết Đồ Thị Cơ Bản: Duyệt BFS & DFS
🏆 +100 XP tiềm năng Thoát

Lý Thuyết Đồ Thị Cơ Bản: Duyệt BFS & DFS

2 khối nội dung 45 phút học tập MỞ BẢNG TRẮNG

Lý Thuyết Đồ Thị Cơ Bản: Duyệt BFS & DFS

Nội dung bài học

1. Bản chất đồ thị & các phương pháp biểu diễn

Đồ thị $G = (V, E)$ là cấu trúc toán học biểu diễn tập hợp các đỉnh (Vertices — $V$) và các cạnh nối giữa chúng (Edges — $E$). Đồ thị có thể là vô hướng (Undirected) hoặc có hướng (Directed), có trọng số hoặc không có trọng số.

Biểu diễn Đồ thị: Ma trận kề vs Danh sách kề

1.1. Ma trận kề (adjacency matrix)

  • Mảng 2 chiều int adj[N][N]: adj[u][v] = 1 nếu có cạnh nối giữa $u$ và $v$.
  • Ưu điểm: Kiểm tra cạnh $(u, v)$ trong $\mathcal{O}(1)$.
  • Nhược điểm: Tốn $\mathcal{O}(N^2)$ bộ nhớ. Khi $N = 10^5$, ma trận cần $40\text{GB}$ RAM $\implies$ Sập bộ nhớ ngay lập tức (MLE). Chỉ dùng khi $N \le 1000$.

1.2. Danh sách kề (adjacency list — Chuẩn mực thi đấu)

  • Sử dụng mảng các vector vector<int> adj[N + 1]: adj[u] chứa toàn bộ các đỉnh kề trực tiếp với $u$.

  • Bộ nhớ: Đúng $\mathcal{O}(V + E)$, cực kỳ tiết kiệm và tối ưu cho đồ thị thưa trong lập trình thi đấu ($N, M \le 2 \cdot 10^5$).

  • Duyệt đỉnh kề: for (int v : adj[u]) tốn thời gian tỷ lệ thuận với bậc của đỉnh $\mathcal{O}(\text{deg}(u))$.

2. Hai chiến lược duyệt đồ thị kinh điển: BFS vs DFS

So sánh BFS vs DFS

2.1. Tìm kiếm theo chiều rộng (breadth-first search — BFS)

  • Sử dụng Hàng đợi (Queue), lan tỏa theo từng tầng bán kính $d = 0, 1, 2, \dots$ tính từ đỉnh nguồn $S$.
  • Đặc tính vàng: Tìm đường đi có ít cạnh nhất (ngắn nhất) trên đồ thị không trọng số.

2.2. Tìm kiếm theo chiều sâu (depth-first search — DFS)

  • Sử dụng Đệ quy (hoặc Stack), đi sâu hết mức có thể trên một nhánh cho đến khi gặp ngõ cụt thì quay lui (Backtracking).
  • Đặc tính vàng: Cực kỳ hiệu quả để đếm thành phần liên thông, phát hiện chu trình, kiểm tra tính liên thông, định hướng cây DFS.

3. Ứng dụng: Đếm số thành phần liên thông & kiểm tra chu trình

Đếm số thành phần liên thông

  • Thuật toán đếm thành phần liên thông: Duyệt qua mọi đỉnh $i \in [1, N]$. Nếu đỉnh $i$ chưa được thăm (!visited[i]), tăng biến đếm số thành phần liên thông components++ và gọi DFS(i) để loang thăm toàn bộ các đỉnh thuộc cùng thành phần.
  • Phát hiện chu trình trên đồ thị vô hướng bằng DFS: Khi duyệt từ $u$ sang đỉnh kề $v$, nếu $v$ đã được thăm (visited[v] == true) và v != parent[u], ta khẳng định đồ thị CÓ CHU TRÌNH!

4. Các bẫy lỗi lập trình kinh điển (bug traps)

  1. Bẫy thêm cạnh đồ thị vô hướng chỉ thêm 1 chiều: * Với đồ thị vô hướng, cạnh giữa $u$ và $v$ phải thêm cả 2 chiều: adj[u].push_back(v); adj[v].push_back(u);. Quên thêm chiều thứ hai làm đồ thị biến thành đồ thị có hướng sai hoàn toàn.
  2. Bẫy tràn ngăn xếp đệ quy (Stack Overflow) khi DFS đồ thị sâu: * Nếu đồ thị là một đường thẳng $N = 2 \cdot 10^5$ đỉnh, hàm đệ quy DFS sẽ gọi sâu $2 \cdot 10^5$ tầng, vượt quá giới hạn ngăn xếp (Call Stack) của một số hệ điều hành và gây Runtime Error.
  3. Bẫy cạnh lặp và khuyên (Multiple Edges & Self-loops): * Đề bài có thể cho nhiều cạnh nối giữa cùng một cặp đỉnh $(u, v)$ hoặc cạnh tự nối $u \to u$. Cần kiểm tra hoặc xử lý cẩn thận nếu thuật toán yêu cầu tính toán bậc hoặc trọng số tối thiểu.

5. Mẫu cài đặt chuẩn thi đấu (competitive templates)

Mẫu 1: DFS đếm số thành phần liên thông và tìm kích thước từng thành phần

⚠️ Lưu ý về Stack Overflow: Hàm DFS đệ quy dưới đây có thể gây tràn ngăn xếp hệ thống (Segmentation Fault) khi đồ thị có dạng đường thẳng $N = 2 \times 10^5$ đỉnh (độ sâu đệ quy lên tới $N$ tầng). Trong thi đấu thực tế, nên dùng DFS bằng std::stack tường minh hoặc thiết lập ulimit -s unlimited (Linux) trước khi chạy. Mẫu đệ quy được giữ lại ở đây vì tính trực quan sư phạm.

#include <bits/stdc++.h>
using namespace std;

int n, m;
vector<vector<int>> adj;

vector<bool> visited;

int dfs(int u) {
    visited[u] = true;
    int comp_size = 1;
    for (int v : adj[u]) {
        if (!visited[v]) {
            comp_size += dfs(v);
        }
    }
    return comp_size;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    if (!(cin >> n >> m)) return 0;

    if (n <= 0) return 0;

    adj.assign(n + 1, vector<int>());
    visited.assign(n + 1, false);

    for (int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;

        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    int component_count = 0;
    vector<int> component_sizes;

    for (int i = 1; i <= n; ++i) {
        if (!visited[i]) {
            component_count++;
            int sz = dfs(i);
            component_sizes.push_back(sz);
        }
    }

    cout << component_count << "\n";
    for (int i = 0; i < (int)component_sizes.size(); ++i) {
        cout << component_sizes[i] << (i + 1 == (int)component_sizes.size() ? "" : " ");
    }
    cout << "\n";

    return 0;
}

Mẫu 2: Phát hiện chu trình trên đồ thị vô hướng bằng DFS

#include <bits/stdc++.h>
using namespace std;

int n, m;
vector<vector<int>> adj;

vector<bool> visited;

bool has_cycle = false;

void dfs_cycle(int u, int p) {
    visited[u] = true;
    for (int v : adj[u]) {
        if (!visited[v]) {
            dfs_cycle(v, u);
        } else if (v != p) {
            has_cycle = true; // Gặp lại đỉnh đã thăm khác cha -> Chu trình!

        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    if (!(cin >> n >> m)) return 0;

    if (n <= 0) return 0;

    adj.assign(n + 1, vector<int>());
    visited.assign(n + 1, false);

    for (int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;

        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    for (int i = 1; i <= n; ++i) {
        if (!visited[i]) {
            dfs_cycle(i, 0);
        }
    }

    if (has_cycle) {
        cout << "YES\n";
    } else {
        cout << "NO\n";
    }

    return 0;
}
1
A
B
C
D
2
A
B
C
D
3
A
B
C
D
4
A
B
C
D
5
A
B
C
D
6
A
B
C
D
7
A
B
C
D
8
A
B
C
D
9
A
B
C
D
10
A
B
C
D
11
A
B
C
D
12
A
B
C
D
13
A
B
C
D
14
A
B
C
D
15
A
B
C
D

Bài Tập Thực Hành & Rèn Luyện

15 Bài tập thực hành

Nhấn vào nút "Làm bài" bên dưới để mở giao diện làm bài trực tuyến (Online Judge) và thực hiện viết mã nguồn cho bài tập này.

Chuyển Danh Sách Cạnh Sang Danh Sách Kề
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Duyệt Đồ Thị Theo Chiều Sâu (DFS Traversal)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Duyệt Đồ Thị Theo Chiều Rộng (BFS Traversal)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đếm Số Thành Phần Liên Thông
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Kích Thước Thành Phần Liên Thông Lớn Nhất
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Kiểm Tra Đường Đi Giữa Hai Đỉnh
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Phát Hiện Chu Trình Trên Đồ Thị Vô Hướng
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đường Đi Ngắn Nhất Trên Đồ Thị Không Trọng Số
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Kiểm Tra Đồ Thị Cây (Tree Verification)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Sắp Xếp Tô-pô (Topological Sort)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Chu Trình Ngắn Nhất Trên Đồ Thị (Girth)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đếm Cặp Đỉnh Không Thể Đi Tới Nhau
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Lan Tỏa Virus Trong Mạng Lưới (Multi-Source BFS)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đếm Số Cạnh Cầu Trên Đồ Thị (Bridges)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Xây Dựng Thêm Đường Nối Toàn Mạng (Building Roads)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài