You are on page 1of 8

ĐỒ THỊ VÀ CÁC CÁCH DUYỆT ĐỒ THỊ CƠ BẢN

(DFS VÀ BFS)

I. Giới thiệu.
1. Lý thuyết đồ thị cơ bản.

- Trong toán học và tin học, đồ thị là 1 phần của lý thuyết đồ thị. Đồ thị nói
theo cách dễ hiểu là 1 tập hợp các đỉnh được nối với nhau bởi các cạnh.
Một đồ thị cơ bản được vẽ dưới dạng một tập các điểm (đỉnh, nút) nối với
nhau bởi các cạnh (đoạn thẳng), các cạnh có thể có hướng hoặc vô hướng
(tùy vào yêu cầu của đề bài).

2 1

1 2
3
5

6 3
4 4
5

H1 H2

Một đồ thị G = (V, E) bao gồm V là tập hợp chứa đỉnh, còn E là tập hợp
chứa các cạnh. Trong bài viết, ta đề cập đến hai loại đồ thị, đồ thị có hướng
và đồ thị vô hướng
- Đồ thị vô hướng là đồ thị mà mà giữa hai đỉnh của một cạnh không có thứ
tự. Trong ví dụ trên, đồ thị bên trái (H1) G = (V, E) có tập đỉnh V = {1, 2, 3,
4, 5, 6} và tập cạnh E = {(1, 2), (1, 3), (1, 4), (2, 3), (2, 5), (2, 4), (3, 5),
(3,6), (5, 6), (2, 1), (3, 1), (4, 1), (3, 2), (5, 2), (4, 2), (5, 3), (6, 3), (6, 5)}
- Đồ thị có hướng là đồ thị mà giữa hai đỉnh của một cạnh luôn có thứ tự.
Trong ví dụ trên, đồ thị bên phải (H2) G = (V, E) có tập đỉnh V = {1, 2, 3, 4,
5} và tập cạnh E = {(1,2), (1, 5), (1, 4), (2, 3), (3, 1), (3, 5), (3, 4), (4, 5)}

2, Cách biểu diễn đồ thị trên máy tính


- Ma trận kề:
Cho đồ thị G = (V, E) trong đó tập cạnh V = {1, 2, 3,.., n}, tập cạnh E = {e1,
e2, e3,…, em}.
Ta gọi ma trận kề của đồ thị trên là ma trận A = aij (i, j = 1, 2, 3,…, n). Với
các phần tử của ma trận được xác định như sau:
+ aij = 1 nếu (i, j) thuộc E
+ aij = 0 nếu (i, j) không thuộc E
Ví dụ:
Đồ thị G = (V, E) có tập đỉnh V = {1, 2, 3, 4, 5, 6} và tập cạnh E = {(1, 2),
(1, 3), (1, 4), (2, 3), (2, 5), (2, 4), (3, 5), (3,6), (5, 6), (2, 1), (3, 1), (4, 1), (3,
2), (5, 2), (4, 2), (5, 3), (6, 3), (6, 5)}. Ma trận kề của G sẽ có dạng như sau:

1 2 3 4 5 6
1
0 1 1 1 0 0

2
1 0 1 1 1 0

3
1 1 0 0 1 1

4
1 1 0 0 0 0
5
0 1 1 0 0 1

6
0 0 1 0 1 0

Dễ thấy ma trận kề của đồ thị vô hướng là ma trận đối xứng, ma trận của
một đồ thị có hướng chưa chắc đã là ma trận đối xứng
- Danh sách cạnh:
Chúng ta cần khởi tạo hai mảng là mảng đầu a[m] chứa các đỉnh đầu tiên
của cạnh và mảng cuối b[m] chứa các đỉnh cuối cùng của cạnh.
Ví dụ:
Đồ thị G = (V, E) có tập đỉnh V = {1, 2, 3, 4, 5, 6} và tập cạnh E = {(1, 2),
(1, 3), (1, 4), (2, 3), (2, 5), (2, 4), (3, 5), (3,6), (5, 6), (2, 1), (3, 1), (4, 1), (3,
2), (5, 2), (4, 2), (5, 3), (6, 3), (6, 5)}. Danh sách cạnh của đồ thị G sẽ có
dạng như sau :
Đầu 1 1 1 2 2 2 3 3 5
Cuối 2 3 4 3 5 4 5 6 6

- Danh sách kề:


Trong rất nhiều vấn đề về ứng dụng của lý thuyết đồ thị, cách biểu diễn đồ
thị dưới dạng danh sách kề là cách biểu diễn thích hợp nhất được sử dụng
Trong cách biểu diễn này, với mỗi đỉnh v thuộc đồ thị, chúng ta sẽ lưu trữ
danh sách các đỉnh kể với nó (thường sử dụng vector hoặc danh sách liên
kết).
Ví dụ:
Đồ thị G = (V, E) có tập đỉnh V = {1, 2, 3, 4, 5, 6} và tập cạnh E = {(1, 2),
(1, 3), (1, 4), (2, 3), (2, 5), (2, 4), (3, 5), (3,6), (5, 6), (2, 1), (3, 1), (4, 1), (3,
2), (5, 2), (4, 2), (5, 3), (6, 3), (6, 5)}. Danh sách kề sẽ có dạng như sau:
Đỉnh 1 2 3 4
Đỉnh 2 1 3 5 4
Đỉnh 3 1 2 5 6
Đỉnh 4 1 2
Đỉnh 5 2 3 6
Đỉnh 6 3 5

II. Cách duyệt đồ thị cơ bản


Trong bài viết này, chúng ta chủ yếu đề cập đến hai cách duyệt đồ thị chính đó là
duyệt theo chiều sâu DFS (Depth - first Search) và duyệt theo chiều rộng BFS
(Breadth - first Search)
1. Depth - first Search
Ý tưởng chính của bài toán là bắt đầu từ một đỉnh u nào đó, sau đó chúng ta
sẽ đánh dấu “đã thăm” đối với đỉnh u đó. Tiếp theo với mỗi đỉnh v kề với
đỉnh u, nếu đỉnh v chưa được thăm, ta tiếp tục gọi đệ quy DFS (v). Với cách
duyệt đồ thị này, đồ thị sẽ được duyệt sâu nhất có thể, đến một bước nào đó,
nếu không duyệt được nữa, nó sẽ quay lại bước tiếp theo để duyệt các
đường đi khác.
Mã giả thuật toán:
Visited [];
DFS (u):
Print (u);
Visited [u] = true; // Đánh dấu u đã được thăm
For (v : adj[u]): // Duyệt các phần tử kề với u
If (!Visited [v]): // Kiểm tra xem v đã được thăm
chưa
DFS (v); //Gọi đệ quy, lặp cho đến khi kết
thúc

Độ phức tạp của thuật toán là O(n+m).


Ví dụ: Xét đồ thị G có hình vẽ như sau, chúng ta sẽ bắt đầu duyệt từ đỉnh 1:

2 5
1
11

4
3

Thứ tự các bước đệ quy sẽ như sau DFS (1) => DFS (2) => DFS (4) =>
DFS(5) => DFS (3);
Code C++: với input là danh sách kề, duyệt theo thứ tự từ bé đến lớn
#include <bits/stdc++.h>

using namespace std;

vector <int> adj[1001];


bool visited [1001];

void DFS (int u)


{
cout << u << " ";
visited [u] = true;
for (int v : adj[u])
{
if (!visited [v]) {
DFS (v);
}
}
}

int main ()
{
int n, m; cin >> n >> m;
for (int i =0; i < m; i++)
{
int x, y; cin >> x >> y;
adj[x].push_back (y);
adj[y].push_back (x);
}
for (int i = 1; i <= n; i++)
{
sort (adj[i].begin(), adj[i].end());
}
DFS (1);
}

2. Breadth - first Search


Tư tưởng của thuật toán là ưu tiên chiều rộng hơn là chiều sâu, thuật toán sẽ
tìm kiếm xung quanh để mở rộng trước khi lan xuống chiều sâu. Ý tưởng:
bắt đầu từ một đỉnh u nào đó, ta đánh dấu u đã được thăm và đẩy u vào
hang đợi. Ta bắt đầu một vòng lặp (khi hang đợi không rỗng), với mỗi lần
lặp, ta lấy ra phần từ v đầu tiên, in ra và xóa nó khỏi hang đợi, sau đó ta xét
đến các đỉnh kề với v, nếu các đỉnh kề với v chưa được thăm, ta đẩy hết nó
vào hàng đợi và đánh dấu đã thăm. Lặp liên tục như vậy cho đến khi kết
thúc.
Visited []
BFS (u):
Queue q; // tạo hàng đợi rỗng
q.push (u); // đẩy u vào hàng đợi
Visited [u] = true // đánh dấu đã thăm
While (!q.empty) // điều kiện hàng đợi chưa rỗng để
lặp
v = q.front()
print (v) // in ra phần tử đầu hàng đợi
q.pop () // xóa phần từ đầu hàng đợi
for (x : adj [v]) // duyệt các phần tử kề với v
q.push (x) // đẩy vào hàng đợi
Visited [x] = true // đánh dấu đã thăm

Ví dụ: cho đồ thị G như hình vẽ sau:

2 5

Thứ tự các đỉnh trong hàng đợi sẽ như sau (phần tử đầu tiên là phân tử đầu
hàng đợi): 1 2 3 4 5
Code C++: input là danh sách cạnh, duyệt theo thứ tự từ bé đến lớn
#include <bits/stdc++.h>

using namespace std;

vector <int> adj [1001];


bool visited [1001];

void BFS (int u)


{
queue <int> q;
q. push (u);
visited [u] = true;
while (!q.empty())
{
int v = q.front ();
cout << v << " ";
q.pop();
for (int x : adj[v])
{
if (!visited [x])
{
q.push (x);
visited [x] = true;
}
}
}
}

int main ()
{
int n, m; cin >> n >> m;
for (int i = 0; i < m; i++)
{
int x, y; cin >> x >> y;
adj[x].push_back (y);
adj[y].push_back (x);
}
for (int i = 1; i <= n; i++)
{
sort (adj[i].begin(), adj[i].end());
}
BFS (1);
}

You might also like