Thành phần liên thông trong đồ thị
Bài học này giúp em hiểu cách đếm số “cụm” đỉnh trong một đồ thị vô hướng. Mỗi cụm là một nhóm đỉnh có thể đi tới nhau bằng các cạnh. Ta sẽ dùng DFS hoặc BFS để tô màu từng cụm, giống như tìm các nhóm bạn quen biết nhau trong một mạng xã hội.
Mục tiêu bài học
Hiểu component
Biết thành phần liên thông là một cụm đỉnh có thể đi tới nhau.
Đánh dấu visited
Biết dùng mảng vis để tránh duyệt lặp vô hạn.
Viết DFS/BFS
Đếm số thành phần liên thông bằng DFS hoặc BFS.
Tránh lỗi
Không nhầm với đồ thị có hướng, không chỉ duyệt từ đỉnh 1.
Ý tưởng cốt lõi
Khởi tạo tất cả đỉnh là chưa thăm. Duyệt lần lượt các đỉnh từ 1 đến n.
Mỗi khi gặp một đỉnh chưa thăm, ta phát hiện một thành phần liên thông mới, tăng component thêm 1,
rồi chạy DFS/BFS từ đỉnh đó để đánh dấu toàn bộ cụm.
| Bước | Việc cần làm | Vì sao cần làm? |
|---|---|---|
| 1 | Tạo danh sách kề adj | Lưu các đỉnh nối với nhau để DFS/BFS duyệt nhanh. |
| 2 | Tạo mảng vis | Biết đỉnh nào đã thuộc một component. |
| 3 | Duyệt tất cả đỉnh i = 1..n | Có thể đồ thị không liên thông, nên không chỉ xuất phát từ đỉnh 1. |
| 4 | Nếu vis[i] == false, tăng comp | Mỗi lần gặp đỉnh chưa thăm là bắt đầu một cụm mới. |
| 5 | Chạy DFS/BFS từ i | Đánh dấu toàn bộ các đỉnh trong cùng cụm. |
Mô phỏng trực quan: tô màu từng component
Đồ thị ví dụ có 8 đỉnh. Các cạnh: (1,2), (2,3), (1,3), (4,5), (6,7). Đỉnh 8 đứng một mình.
Trạng thái thuật toán
Code mẫu đếm số thành phần liên thông
for (i = 1..n).
#include
using namespace std;
int n, m;
vector> adj;
vector vis;
void dfs(int u) {
vis[u] = 1;
for (int v : adj[u]) {
if (!vis[v]) dfs(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); // đồ thị vô hướng
}
vis.assign(n + 1, 0);
int comp = 0;
for (int i = 1; i <= n; i++) {
if (!vis[i]) {
comp++;
dfs(i);
}
}
cout << "So thanh phan lien thong: " << comp;
return 0;
} 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) # đồ thị vô hướng
def dfs(u, vis):
vis[u] = True
for v in adj[u]:
if not vis[v]:
dfs(v, vis)
vis = [False] * (n + 1)
comp = 0
for i in range(1, n + 1):
if not vis[i]:
comp += 1
dfs(i, vis)
print("So thanh phan lien thong:", comp)DFS hay BFS đều được
Với bài đếm thành phần liên thông, DFS và BFS đều có độ phức tạp O(n + m). Khác nhau chủ yếu ở cách duyệt:
DFS đi sâu bằng đệ quy/ngăn xếp, còn BFS lan rộng bằng hàng đợi.
int count_components_bfs(int n, vector>& adj) {
vector vis(n + 1, 0);
int comp = 0;
for (int start = 1; start <= n; start++) {
if (vis[start]) continue;
comp++;
queue q;
q.push(start);
vis[start] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
}
return comp;
} from collections import deque
def count_components_bfs(n, adj):
vis = [False] * (n + 1)
comp = 0
for start in range(1, n + 1):
if vis[start]:
continue
comp += 1
q = deque([start])
vis[start] = True
while q:
u = q.popleft()
for v in adj[u]:
if not vis[v]:
vis[v] = True
q.append(v)
return comp| Tiêu chí | DFS | BFS |
|---|---|---|
| Cấu trúc dữ liệu | Đệ quy hoặc stack | Queue |
| Khi nào tiện? | Đếm component, tìm chu trình, duyệt cây | Khoảng cách ngắn nhất trên đồ thị không trọng số |
| Lưu ý | Python có thể lỗi độ sâu đệ quy nếu n lớn | Cần đánh dấu ngay khi đưa vào queue |
Thực hành tương tác: nhập đồ thị và tìm component
Dòng 1:
n mm dòng tiếp theo, mỗi dòng gồm hai đỉnh u v.
Mở rộng: in ra từng thành phần liên thông
Nếu đề bài yêu cầu không chỉ đếm số nhóm mà còn in các đỉnh trong từng nhóm, ta thêm một vector/list group để lưu các đỉnh được DFS duyệt trong component hiện tại.
#include
using namespace std;
int n, m;
vector> adj;
vector vis;
void dfs(int u, vector& group) {
vis[u] = 1;
group.push_back(u);
for (int v : adj[u]) {
if (!vis[v]) dfs(v, group);
}
}
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);
}
vis.assign(n + 1, 0);
int comp = 0;
for (int i = 1; i <= n; i++) {
if (!vis[i]) {
comp++;
vector group;
dfs(i, group);
cout << "Nhom " << comp << ": ";
for (int x : group) cout << x << " ";
cout << "
";
}
}
return 0;
} def dfs(u, vis, group):
vis[u] = True
group.append(u)
for v in adj[u]:
if not vis[v]:
dfs(v, vis, group)
vis = [False] * (n + 1)
comp = 0
for i in range(1, n + 1):
if not vis[i]:
comp += 1
group = []
dfs(i, vis, group)
print("Nhom", comp, ":", *group)Lỗi học sinh hay mắc
Chỉ gọi dfs(1)
Chỉ duyệt được component chứa đỉnh 1, bỏ sót các cụm khác.
Cách sửa
Luôn có vòng lặp ngoài: for i = 1..n, gặp đỉnh chưa thăm thì tăng comp và DFS/BFS.
Quên thêm cạnh ngược
Đề là đồ thị vô hướng nhưng chỉ viết adj[u].push_back(v).
Cách sửa
Với đồ thị vô hướng, phải thêm cả adj[u].push_back(v) và adj[v].push_back(u).
Nhầm liên thông mạnh
Thành phần liên thông mạnh chỉ dùng cho đồ thị có hướng và khó hơn.
Cách sửa
Bài này đang xét đồ thị vô hướng. Với đồ thị có hướng, cần học SCC như Kosaraju hoặc Tarjan.
Bài tập luyện tập phân tầng
Bài tập được chia theo mức độ để học sinh yếu, khá và giỏi đều có đường đi rõ ràng.
Cơ bản
- Nhập đồ thị vô hướng, in danh sách kề.
- Đếm bậc của từng đỉnh.
- Kiểm tra từ 1 có đi tới n không.
Trung bình
- Đếm số thành phần liên thông.
- In ra các đỉnh trong từng component.
- Tìm component có nhiều đỉnh nhất.
Nâng cao
- Đếm số cạnh cần thêm để đồ thị liên thông.
- Kiểm tra đồ thị có phải cây không.
- Đếm số component sau khi xóa một số cạnh.
HSG
- Liên thông trong lưới ô vuông.
- DSU đếm component động.
- So sánh DFS/BFS và DSU.
comp thành phần liên thông, số cạnh ít nhất cần thêm để toàn bộ đồ thị liên thông là comp - 1.
Quiz kiểm tra nhanh
Câu 1
Vì sao phải duyệt tất cả đỉnh từ 1 đến n?
Câu 2
Đồ thị có 4 component. Cần thêm ít nhất bao nhiêu cạnh để liên thông?
➡ Tiếp theo: Đường đi ngắn nhất bằng BFS trên đồ thị không trọng số.
💳 Quét mã ủng hộ tuỳ tâm nhé!