Professional Documents
Culture Documents
Chuong2 GraphTheory
Chuong2 GraphTheory
1
Nội dung
2
Duyệt đồ thị là gì?
Duyệt hay tìm kiếm trên đồ thị là viếng mỗi đỉnh/nút trong
đồ thị một cách có hệ thống.
Có hai cách chính để duyệt đồ thị: duyệt theo chiều sâu
trước (depth-first-traversal) và duyệt theo chiều rộng
trước (breadth-first-traversal).
Duyệt theo chiều rộng trước sử dụng hàng đợi như là cấu
trúc dữ liệu hỗ trợ và duyệt theo chiều sâu trước sử dụng
stack như là cấu trúc dữ liệu hỗ trợ.
Stack là cấu trúc dữ liệu dạng LIFO (Last In First Out) và cho
phép chúng ta thực hiện nhanh các thao tác Push, Pop.
Queue là cấu trúc dữ liệu dạng FIFO (First In First Out) và cho
phép chúng ta thực hiện nhanh các thao tác Enqueue,
Dequeue
3
1. Duyệt theo chiều rộng trước
Khi duyệt đồ thị nếu ta dùng một queue như một cấu trúc dữ
liệu hỗ trợ, ta sẽ đi đến một giải thuật duyệt theo chiều rộng
trước (breadth-first-search).
procedure bfs;
procedure visit(n: vertex);
begin
đưa đỉnh n vào hàng đợi;
while hàng đợi khác rỗng do
lấy một đỉnh ở đầu hàng đợi, xử lý nó và thêm bất kỳ đỉnh nào
kế cận với đỉnh này mà chưa được xử lý vào hàng đợi và
đổi trạng thái của chúng sang trạng thái “sẵn sàng”.
end;
begin
Khởi tạo trạng thái của mọi đỉnh trong đồ thị;
for mỗi đỉnh n trong đồ thị do
if trạng thái của n là “chưa được viếng” then visit(n)
end;
4
Giải thuật duyệt theo chiều rộng trước
Giả sử chúng ta biểu diễn đồ thị vô hướng bằng một tập
danh sách kế cận. Mã giả chi tiết của giải thuật duyệt đồ thị
theo chiều rộng trước (list-bfs) được cho như sau:
procedure list-bfs;
var id, k: integer; val: array[1..maxV] of integer;
begin
id := 0; queue-initialze;
for k := 1 to V do val[k]: = 0; /* khởi tạo trạng thái của mọi đỉnh */
edges := null; /* khởi tạo tập cạnh sẽ được duyệt */
for k := 1 to V do
if val[k] = 0 then visit(k);
output edges
end;
Ghi chú: Tập edges dùng để lưu những cạnh được duyệt qua
trong quá trình BFS.
5
procedure visit(k: integer);
var t: link;
begin
put(k); /* đưa đỉnh k vào hàng đợi */
repeat
k := get; /* lấy ra một đỉnh từ đầu hàng đợi */
id := id + 1; val[k] := id; /* chuyển trạng thái của đỉnh k sang
“đã viếng” */
t := adj[k]; /* tìm các đỉnh kế cận với đỉnh k, t là biến chạy */
while t <> z do /* z là nút rỗng */
begin
if val[t .v] = 0 then
begin
edges := edges {(k, t .v)} /* lưu cạnh (k, t .v ) vào tập edges
put(t.v); val[t.v]: = -1 /* chuyển đỉnh t.v sang trạng thái
“sẵn sàng” */
end;
t := t.next
end
until queueempty
end; 6
Ghi chú: Mảng val[1..V] chứa trạng thái của cácđỉnh.
val[k] = 0 nếu đỉnh k chưa hề được viếng (“not yet visited”),
val[k] ≠ 0 nếu đỉnh k đã được viếng.
val[k]= j nghĩa là đỉnh jth mà được viếng trong quá trình
duyệt là đỉnh k.
val[k] = -1 nghĩa là đỉnh k đã đang ở trong hàng đợi (tức là
trạng thái sẵn sàng).
7
A: F-C-B-G B: A C: A G: A-E F: E-D
E: F-D-G D: F-E
D D D D D
E E E E
G G G G
B B B M M M
edges = {AF, AC, AB, AG,
C C L L FE, FD, HI, JK, JL, JM}
F K
A J
Hình 2.1 Nội dung của hàng đợi khi thực thi BFS cho đồ thị bên cạnh
10
Giải thuật duyệt theo chiều sâu trước không đệ quy
procedure visit(n:vertex);
begin
đưa đỉnh n vào stack;
while stack còn chưa rỗng do
lấy một đỉnh ra khỏi stack, xử lý đỉnh này,
và đưa mọi đỉnh kế cận của n mà chưa hề được xử lý vào stack.
if một đỉnh đã có trong stack rồi, thì không cần đưa nó vào stack
nhưng chuyển nó lên vị trí đỉnh stack.
end;
procedure dfs;
begin
Khởi tạo trạng thái của mọi đỉnh trong đồ thị;
for mỗi đỉnh n trong đồ thị do
if trạng thái của n là “chưa được viếng” then visit(n)
end;
11
Mã giả giải thuật dfs (không đệ quy)
Giả sử chúng ta biểu diễn đồ thị vô hướng bằng một tập danh sách kế
cận. Mã giả chi tiết của giải thuật duyệt đồ thị theo chiều sâu trước
(không đệ quy) được cho như sau:
procedure list-dfs;
var id, k: integer; val: array[1..maxV] of integer;
p:array[1..maxV] of integer; /* mảng dùng để suy ra tập edges */
begin
id := 0;
for k := 1 to V do /* khởi tạo trạng thái của mọi đỉnh */
begin val[k]: = 0; p[k]: = nil end;
for k := 1 to V do
if val[k] = 0 then visit(k);
edges := null; /* xác định tập cạnh được duyệt */
for k := 1 to V do
if p[k] <> nil then edges := edges {(p[k], k)};
output edges
end;
12
procedure visit(k: integer);
var t: link;
begin
push(k); /* đưa đỉnh k vào stack */
repeat
k := pop; /* lấy ra một đỉnh từ stack */
id := id + 1; val[k] := id; /* chuyển trạng thái của đỉnh k sang “đã viếng” */
t := adj[k]; /* tìm các đỉnh kế cận với đỉnh k, t là biến chạy */
while t <> z do
begin
if val[t .v] = 0 then
begin
push(t.v); val[t.v]: = -1 /* chuyển đỉnh t.v sang trạng thái “sẵn sàng” */
p[t.v] := k /* ghi nhận cạnh (k, t .v)
end
else if val[t .v] = -1 then
begin shift(t.v); /*chuyển đỉnh t.v lên top của stack*/
p[t.v] := k end;
t := t.next /* chuyển qua đỉnh kế tiếp trong danh sách kế cận */
end
until stackempty
end;
13
Ghi chú: Mảng val[1..V] chứa trạng thái của cácđỉnh.
val[k] = 0 nếu đỉnh k chưa hề được viếng (“not yet visited”),
val[k] ≠ 0 nếu đỉnh k đã được viếng.
val[k]= j nghĩa là đỉnh jth mà được viếng trong quá trình duyệt là
đỉnh k.
val[k] = -1 nghĩa là đỉnh k đã đang ở trong stack (tức là trạng thái
sẵn sàng).
Khi đỉnh kế cận của đỉnh k đang xét đã được đưa vào stack thì
không cần đưa đỉnh này vào stack, nhưng phải dịch chuyển nó lên
vị trí đỉnh của stack (tác vụ shift)
14
Thí dụ 1: DFS trên đồ thị vô hướng
Mảng p:
p(G) = A, p(C) = A,
p(B) = A, p(F) = A
p(E) = F, p(D) = F
p(G) = E, p(D) = E
Chú ý: Có sự dịch chuyển G, D lên vị trị đỉnh của stack.
Thứ tự duyệt: A – F – E – G – D – B – C
Tập edges = { AC, AB, AF, FE, EG, ED}
15
Cây DFS
Thứ tự duyệt: A – F – E – G – D – B – C
Tập edges = { AC, AB, AF, FE, EG, ED}
19
Diễn tiến quá trình gọi đệ quy A: F-G-B-C
B: A
C: A
G: A-E
F: A-E
E: F-G-D
D: E-F
visit (A) visit(F) visit(E) visit(G) hoàn tất lượt gọi đệ quy tại
đỉnh G, quay lui về đỉnh E visit(D) hoàn tất lượt gọi đệ quy tại
đỉnh D, quay lui về đỉnh E hoàn tất lượt gọi đệ quy tại đỉnh E, quay
lui về đỉnh F hoàn tất lượt gọi đệ quy tại đỉnh F, quay lui về đỉnh A
visit(B) hoàn tất lượt gọi đệ quy tại đỉnh B, quay lui về đỉnh A
visit(C) hoàn tất lượt gọi đệ quy tại đỉnh C, quay lui về đỉnh A
hoàn tất A, kết thúc .
Tập edges = {AF, FE, EG, ED, AB, AC} 20
Thí dụ 2: DFS trên đồ thị có hướng (dùng giải
thuật lặp)
A: B
B: D-C
C: A
D: A-C
E: F-G
F: B
G: F-D
Rừng DFS có những cạnh lùi như CA, DA. Cạnh CA cho thấy A-
B-C là một chu trình và và cạnh DA cho thấy A-B-D cũng là một
chu trình
22
Độ phức tạp của DFS
23
DFS đệ quy– biểu diễn bằng ma trận kế cận
Cùng một phương pháp có thể được áp dụng cho đồ thị được
biểu diễn bằng ma trận kế cận bằng cách dùng thủ tục visit
sau đây:
25
Nhận diện chu trình trong đồ thị vô hướng
Ta có thể cải biên thủ tục visit trong giải thuật DFS đệ quy để giải thuật
DFS trở thành giải thuật nhận diện chu trình trong đồ thị vô hướng. Thủ
tục visit được cải biên thành thủ tục có tên cycledetection như sau:
procedure cycledetection(k: integer);
var t: link;
begin
id := id + 1; val[k] := id; /* chuyển trạng thái của đỉnh k sang “đã viếng” */
t := adj[k]; / * duyệt các đỉnh kế cận của đỉnh k, dùng biến chạy t */
while t <> z do /* z là nút rỗng */
begin
if val[t .v] = 0 then cycledetection(t.v)
else if val[t .v] <= val[k] - 2 then thông báo phát hiện một chu trình ;
t := t.next /* dịch chuyển sang đỉnh kế tiếp trong danh sách kề */
end
end;
27
Chương trình chính:
for k := 1 to V do val[k]:= 0;
cycledetection(s) /* chọn một đỉnh s nào đó làm đỉnh xuất phát */
Thí dụ
call G
call E call E
call F call F call F
call A call A call A call A
Main Main Main Main
Ta có thể cải biên giải thuật DFS để nhận dạng các thành phần liên
thông của một đồ thị vô hướng như sau:
procedure visit (k: integer);
var t: link;
begin
id := id + 1; val[k] := id;
write(name(k)); /* print out the name of the vertex which has
been visited */
t := adj[k]; /* duyệt danh sách kế cận của đỉnh k, dùng biến chạy t */
while t <> z do
begin
if val[t.v] = 0 then visit(t.v);
t := t.next /* chuyển sang nút kế tiếp trong danh sách kề */
end
end;
29
begin
id := 0;
for k := 1 to V do val[k] := 0;
write(‘The first connected component:’)
for k := 1 to V do
begin
if val[k] = 0 then visit(k);
writeln; writeln; write(‘Another connected component:’)
// move to new line,
// prepare to print out the list of vertices belonging to a
// new connected component
end;
end;
Ghi chú: Số cây trong rừng DFS bằng với số thành phần liên thông
của đồ thị
30
Thí dụ
Kết qủa:
The first connected component: A F E G D B C
H I
B C G
D E J K
L M
Hình 2.7. Một thí dụ về đồ thị có hướng
32
Thường thì hướng của các cạnh biểu thị mối liên hệ trước
sau (precedence relationship) trong ứng dụng được mô hình
hóa.
Trong phần này, chúng ta xem xét giải thuật sắp thứ tự topo
(topological sorting)
33
Xếp thứ tự tôpô
Đồ thị có hướng không chu trình (Directed Acyclic Graph)
Đồ thị có hướng mà không có chu trình được gọi là các đồ
thị có hướng không chu trình (dags).
Tập thứ tự riêng phần và xếp thứ tự tôpô
Cho G là một đồ thị có hướng không chu trình. Xét quan
hệ thứ tự < được định nghĩa giữa các đỉnh của đồ thị như
sau:
u < v nếu có một lối đi từ u đến v trong G.
35
A
H I
B C G
D E J K
L M
Các nút trong đồ thị ở hình trên có thể được sắp thứ tự tôpô
theo thứ tự sau:
J K L M A G H I F E D B C
36
Phương pháp 1 sắp xếp tôpô
Phương pháp này thực hiện theo kiểu tìm kiếm theo chiều sâu trước :
thêm một nút vào danh sách mỗi khi cần thiết lấy một nút ra khỏi
stack để tiếp tục. Khi gặp một nút không có nút đi sau thì ta sẽ lấy ra
(pop) một phần tử từ đỉnh stack. Lặp lại quá trình này cho đến khi
stack rỗng. Đảo ngược danh sách này ta sẽ được thứ tự tôpô.
Giải thuật:
1. Nhận diện các đỉnh không có đỉnh đi trước (tức các đỉnh nguồn),
đưa chúng vào stack.
2. while stack khác rỗng do
if phần tử đang ở đỉnh stack là một đỉnh có một số đỉnh kế cận
then đưa tất cả những đỉnh kế cận này vào stack
else lấy phần tử đang ở đỉnh stack ra khỏi stack, gỡ đỉnh tương
ứng ra khỏi đồ thị và đưa vào danh sách kết quả.
3. Đảo ngược danh sách kết quả để được thứ tự tô pô.
.
37
Hình 2.8 Sắp thứ tự
tôpô cho đồ thị có
hướng bằng phương
pháp 1
List: 8- 5- 3- 6- 4- 10- 2- 1- 9- 7
Kết quả: 7- 9- 1- 2- 10- 4- 6- 3- 5- 8
38
Với một đồ thị có hướng không chu trình, được biểu diễn bằng tập
danh sách kế cận và duy trì một mảng indeg và một mảng outdeg
mà với đỉnh k, indeg[k] = bậc vào và outdeg[k] = bậc ra đỉnh k. Giải
thuật sắp thứ tự tô pô theo phương pháp thứ nhất được chi tiết
hóa như sau:
procedure toposort1;
var k: integer;
begin
1 for k := 1 to V do
2 if indeg[k] = 0 then push(k); /* đưa các đỉnh nguồn vào stack */
3 repeat
4 k1 := top(stack); /* top(stack) là phần tử đang ở đỉnh stack */
if outdeg[k1]= 0 then /* nếu đỉnh k1 là đỉnh tận cùng */
begin
5 k:= pop; /* lấy phần tử đanh ở đỉnh stack ra khỏi stack */
output k to the output-list /* đưa k vào danh sách kết quả */
giảm outdeg (bậc ra) của những đỉnh đi vào đỉnh k
end
39
else /* đỉnh k1 đang xét không phải là đỉnh tận cùng */
begin
t := adj[k1]; /* t là biến chạy dùng để duyệt qua danh sách
kế cận của đỉnh k1 */
while t <> z do /* z là nút rỗng */
begin
8 push(t.v); /* đưa đỉnh kế cận với đỉnh k1 vào stack */
9 t := t.next;
end
end
10 until stackempty;
11 đảo ngược output-list để được thứ tự tô pô
end;
40
Độ phức tạp của giải thuật sắp xếp tô pô
phương pháp 1
Tính chất: Độ phức tạp tính toán của giải thuật sắp
xếp tô pô là O(|E|+|V|) nếu đồ thị được diễn tả bằng
tập danh sách kế cận.
41
Phương pháp 2 sắp thứ tự tô pô
42
Giải thuật của phương pháp 2
Giải thuật của phương pháp 2 để xếp thứ tự tô pô như sau:
Nhận diện những đỉnh không có đỉnh đi trước, đưa chúng vào hàng
đợi.
while hàng đợi khác rỗng do
Lấy đỉnh N ở đầu hàng đợi ra
for mỗi đỉnh M kế cận với đỉnh N do
xóa bỏ cạnh nối từ N đến M
if nếu đỉnh M không còn có đỉnh đi trước then
đưa đỉnh M vào đuôi hàng đợi
endfor
endwhile
43
Hình 2.9
Thứ tự tô pô là a, b, c, d, e
procedure toposort2;
var k: integer;
begin
queueinitialize; /* khởi tạo hành đợi */
1 for k := 1 to V do
2 if indeg[k] = 0 then put(k); /* đưa các đỉnh nguồn vào hàng đợi */
3 repeat
4 k := get; /* lấy phần tử ở đầu hàng đợi ra để xét */
5 output k to the output-list ;
6 t := adj[k]; /* duyệt danh sách kế cận của đỉnh k */
45
7 while t <> z do /* z là nút rỗng */
begin
8 indeg[t.v] = indeg[t.v] -1; /* xóa bỏ cạnh (k, t.v) ra khỏi
đồ thị */
9 if (indeg[t.v] = 0) then put(t.v); /* nếu đỉnh t.v trở thành
đỉnh nguồn thì đưa đỉnh này vào hàng đợi */
10 t := t.next;
end
11 until queueempty;
end;
46
Kết luận
Duyệt theo chiều sâu trước và duyệt theo bề rộng trước
là hai giải thuật duyệt đồ thị quan trọng.
Bằng cách biểu diễn một đồ thị dưới hình thức cây DFS
hay cây BFS, chúng ta có thể tìm ra nhiều đặc tính của
đồ thị như tính liên thông, có chứa chu trình hay không.
Hai giải thuật này có độ phức tạp tính toán tương đồng.
Xếp thứ tự tô pô là một bài toán dùng với đồ thị có
hướng không có chu trình và có ứng dụng trong các ứng
dụng xếp lịch. Có thể xếp thứ tự tô pô dựa vào một trong
hai phương pháp: phương pháp dựa vào duyệt theo
chiều sâu trước và phương pháp dựa vào duyệt theo bề
rộng trước
47
Phụ Lục A
Diễn tiến cấp phát vùng bộ nhớ khi
thực thi giải thuật DFS đệ quy
48
Diễn tiến cấp phát vùng bộ nhớ khi thực thi giải thuật
DFS đệ quy
Ứng với đồ thị vô hướng ở hình trên (bên trái), diễn tiến
cấp phát các vùng bộ nhớ hoạt động (activation
record) cho các trình con được gọi khi thực thi giải thuật
DFS đệ quy được minh họa ở slide kế tiếp.
49
Các vùng bộ nhớ của
các trình con được
hệ thống cấp phát
xếp chồng lên nhau
theo cơ chế cấu trúc
stack.
51
Cây DFS
52