You are on page 1of 76

Chương 4

Độ phức tạp của các giải thuật đồ thị

1
Nội dung

1. Các giải thuật đồ thị căn bản


2. Đồ thị có trọng số
3. Đồ thị có hướng

2
1.Các giải thuật đồ thị căn bản

Có nhiều bài toán được định nghĩa theo đối tượng và các
kết nối giữa các đối tượng ấy.
Một đồ thị là một đối tượng toán học mà mô tả những bài
toán như vậy.
Các ứng dụng trong các lãnh vực:
Giao thông
Viễn thông
Điện lực
Mạng máy tính
Cơ sở dữ liệu
Trình biên dịch
Các hệ điều hành
Lý thuyết đồ thị
3
Một thí dụ

H I

B C G

D E J K

L M

Hình 4.1a Một đồ thị thí dụ

4
Thuật ngữ

Một đồ thị là một tập các đỉnh và các cạnh. Các đỉnh là
những đối tượng đơn mà có thể có tên và có một số tính
chất khác và cạnh là đường kết nối giữa hai đỉnh.

Một lối đi từ x đến y trong một đồ thị là một danh sách


những đỉnh mà những đỉnh kế tiếp nhau được kết nối nhờ
vào những cạnh trên đồ thị.
Một đồ thị là liên thông nếu có một lối đi từ mỗi nút đến
một nút khác trong đồ thị. Một đồ thị mà không liên thông
thì bao gồm nhiều thành phần liên thông.

Một lối đi đơn là một lối đi mà trên đó không có đỉnh nào


lặp lại.

5
Thuật ngữ (tt.)

Một chu trình (cycle ) là một lối đi đơn ngoại trừ đỉnh đầu
tiên và đỉnh cuối cùng trùng nhau (một lối đi từ một đỉnh
quay về chính nó).
Một đồ thị không có chu trình được gọi là cây (tree). Một
nhóm các cây không liên thông được gọi là rừng ( forest ).
Cây bao trùm của một đồ thị là một đồ thị con mà chứa tất cả
các đỉnh trong cây nhưng một số cạnh đủ chạm đến mọi đỉnh.
Gọi số đỉnh trong một đồ thị là V, số cạnh là E, số cạnh của
đồ thị có thể có từ 0 đến V (V-1)/2. (Chứng minh truy chứng).
Đồ thị có tất cả mọi cạnh hiện diện giữa mọi cặp đỉnh được
gọi là đồ thị đầy đủ (complete graphs).

6
Thuật ngữ (tt.)

Các đồ thị với số cạnh tương đối ít được gọi là đồ thị thưa;
các đồ thị với chỉ một số ít cạnh mất đi được gọi là đồ thị dày.

Các đồ thị mô tả cho đến giờ là những đồ thị vô hướng


(undirected graphs). Trong các đồ thị có trọng số (weighted
graphs), những giá trị số (trọng số ) được gắn vào mỗi cạnh để
diễn tả thí dụ khoảng cách hay chi phí.

Trong đồ thị có hướng (directed graphs) các cạnh là “một


chiều”: một cạnh đi từ x sang y chứ không phải đi từ y sang x.
Các đồ thị có hướng, có trọng số còn được gọi là các mạng
(networks).

7
Cách biểu diễn đồ thị

Ta phải ánh xạ các tên đỉnh thành những số nguyên trong


tầm trị giữa 1 và V.

Giả sử có tồn tại hai hàm:


- hàm index: chuyển đổi từ tên đỉnh thành số nguyên
- hàm name: chuyển đổi số nguyên thành tên đỉnh.

Có hai cách biểu diễn đồ thị:


- dùng ma trận kế cận
- dùng tập danh sách kế cận

8
Cách biểu diễn ma trận kế cận

A B C D E F G H I J K L M
Một ma trận V
A 1 1 1 0 0 1 1 0 0 0 0 0 0
B 1 1 0 0 0 0 0 0 0 0 0 0 0 hàng V cột chứa
C 1 0 1 0 0 0 0 0 0 0 0 0 0 các giá trị Boolean
D 0 0 0 1 1 1 0 0 0 0 0 0 0 mà a[x, y] là true if
E 0 0 0 1 1 1 1 0 0 0 0 0 0 nếu tồn tại một
F 1 0 0 1 1 1 0 0 0 0 0 0 0
cạnh từ đỉnh x đến
G 1 0 0 0 1 0 1 0 0 0 0 0 0
H 0 0 0 0 0 0 0 1 1 0 0 0 0 đỉnh y và false nếu
I 0 0 0 0 0 0 0 1 1 0 0 0 0 ngược lại.
J 0 0 0 0 0 0 0 0 0 1 1 1 1
K 0 0 0 0 0 0 0 0 0 1 1 0 0 Hình 4.1b: Ma trận kế
L 0 0 0 0 0 0 0 0 0 1 0 1 1 cận của đồ thị ở hình
M 0 0 0 0 0 0 0 0 0 1 0 1 1 4.1a

9
Ghi chú về cách biểu diễn ma trận kế cận

Mỗi cạnh tương ứng với 2 bit trong ma trận: mỗi


cạnh nối giữa x và y được biểu diễn bằng giá trị true
tại cả a[x, y] và a[y, x].
Để tiện lợi giả định rằng có tồn tại một cạnh nối mỗi
đỉnh về chính nó.
Lối biểu diễn này chỉ thích hợp khi các đồ thị là dầy.

10
Giải thuật
program adjmatrix (input, output);
const maxV = 50;
var j, x, y, V, E: integer;
a: array[1..maxV, 1..maxV] of boolean;
begin
readln (V, E);
for x: = 1 to V do /*initialize the matrix */
for y: = 1 to V do a[x, y]: = false;
for x: = 1 to V do a[x, y]: = true;
for j: = 1 to E do
begin
readln (v1, v2);
x := index(v1); y := index(v2);
a[x, y] := true; a[y, x] := true
end;
end.
11
Cách biểu diễn bằng tập danh sách kế cận

Trong cách biểu diễn này, mọi đỉnh mà nối tới một
đỉnh được kết thành một danh sách kế cận
(adjacency-list ) cho đỉnh đó.

program adjlist (input, output);


const maxV = 100;
type link = ↑node
node = record v: integer; next: link end;
var j, x, y, V, E: integer;
t, x: link;
adj: array[1..maxV] of link;

12
begin
readln(V, E);
new(z); z↑.next: = z;
for j: = 1 to V do adj[j]: = z;
for j: 1 to E do
begin
readln(v1, v2);
x: = index(v1); y: = index(v2);
new(t); t↑.v: = x; t↑.next: = adj[y];
adj[y]: = t; /* insert x to the first element of
y’s adjacency list */
new(t); t↑.v = y; t↑.next: = adj[x];
adj[x]:= t; /* insert y to the first element of
x’s adjacency list */
end;
end.
13
a b c d e f g h i j k l m

f a a f g a e i h k j j j

c e f e a l m l

b d d m

Hình 4.1c: Biểu diễn bằng tập danh


sách kế cận của đồ thị ở hình 4.1
14
Các phương pháp duyệt đồ thị

Duyệt hay tìm kiếm trên đồ thị: 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-search )
- duyệt theo chiều rộng trước (breadth-first-search).

15
Duyệt theo chiều sâu trước – giải thuật đệ quy
procedure dfs;
procedure visit(n:vertex);
begin
add n to the ready stack;
while the ready stack is not empty do
get a vertex from the stack, process it,
and add any neighbor vertex that has not been processed
to the stack.
if a vertex has already appeared in the stack, there is no
need to push
it to the stack, but it is moved to the top of the stack.
end;
begin
Initialize status;
for each vertex, say n, in the graph do
if the status of n is “not yet visited” then visit(n)
end;
16
Tìm kiếm theo chiều sâu trước – biểu diễn
danh sách kế cận
procedure list-dfs;
var id, k: integer;
val: array[1..maxV] of integer;
procedure visit (k: integer);
var t: link;
begin
id: = id + 1; val[k]: = id; /* change the status of k
to “visited” */
t: = adj[k]; / * find the neighbors of the vertex k */
while t <> z do
begin
if val[t ↑.v] = 0 then visit(t↑.v);
t: = t↑.next
end
end;
17
begin
id: = 0;
for k: = 1 to V do val[k]: = 0; /initialize
the status of all vetices */
for k: = 1 to V do
if val[k] = 0 then visit(k)
end;

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.

18
A A A A

F F F

A A A A

G G G G

E E E E

F F F F

A A A A

G G G G

D E D E D E D E

F F F F

A A A A

C G C G B C G B C G

D E D E D E D E

F F F F
19
Hình 4.2 Duyệt theo chiều sâu trước
Như vậy kết quả của giải thuật duyệt DFS trên đồ thị cho ở
hình 4.1a với tập danh sách kế cận cho ở hình 4.1c là
AFEGDCG
Lưu ý: thứ tự của các đỉnh trong các danh sách kế cận có ảnh
hưởng đến thứ tự duyệt của các đỉnh khi áp dụng DFS.

20
Độ phức tạp của duyệt theo chiều sâu trước

Tính chất 5.1.1 Duyệt theo chiều sâu trước một đồ


thị biểu diễn bằng các danh sách kế cận đòi hỏi thời
gian tỉ lệ V+ E.

Chứng minh: Chúng ta phải gán trị cho mỗi phần


tử của mảng val (do đó tỉ lệ với O(V)), và xét mỗi
nút trong các danh sách kết cận biểu diễn đồ thị (do
đó tỉ lệ với O(E)).

21
Duyệt theo chiều sâu trước – 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:
procedure visit(k: integer);
var t: integer;
begin
id: = id + 1; val[k]: = id;
for t: = 1 to V do
if a[k, t] then
if val[t] = 0 then visit(t)
end;
22
Độ phức tạp của duyệt theo chiều sâu trước –
biểu diễn ma trận

Tính chất 5.1.2 Duyệt theo chiều sâu trước một đồ


thị biểu diễn bằng ma trận kế cận tỉ lệ với V2.

Chứng minh: Bởi vì mỗi bit trong ma trận kế cận


của đồ thị đều phải kiểm tra.

23
Duyệt theo chiều sâu trước – không đệ quy
procedure list-dfs;
var id, k: integer; val: array[1..max V] of integer;
procedure visit(k: integer);
var t: link;
begin
push(k);
repeat
k: = pop; id:= id + 1; val[k]: = id; /* change the status of k to “visited” */
t =: adj[k]; /* find the neighbors of the vertex k */.
while t <> z do
begin
if val[t↑.v] = 0 then
begin
push(t↑.v); val[t↑.v]: = -1 /* change the status of t↑.v to “ready” */
end
else if val[t↑.v] = -1 then shift t↑.v to the top of the stack;
t: = t↑.next
end
until stackempty
end;
24
begin
id: = 0; stackinit;
for k: = 1 to V do val[k]: = 0; /* initialize the
status of all vertices */
for k: = 1 to V do
if val[k] = 0 then visit(k)
end;

Với giải thuật không đệ quy, ta cần dùng một stack được gọi
là ready stack.
Ghi chú:
val[k] = 0 nếu đỉnh k là “chưa được viếng thăm”,
val[k] = -1 nếu đỉnh k đang ở trong ready stack
val[k] là một trị dương nếu đỉnh k đã được viếng thăm.

25
A A

B C G

A A

B C G B C G

E D E

F F

Hình 4.3a DFS dựa vào stack

26
G E D
B B B B M
C C C C C L L
A F F F F F F H I J K K K

Hình 4.3b Nội dung của stack khi thực hiện duyệt theo chiều sâu trước

27
Duyệt theo chiều rộng trước
Khi duyệt đồ thị nếu ta dùng một queue thay vì một stack, 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
add n to the ready queue;
while the ready queue is not empty do
get a vertex from the queue, process it, and add any neighbor
vertex that has not been processed to the queue and
change their status to ready.
end;
begin
Initialize status;
for each vertex, say n, in the graph
if the status of n is “not yet visited” then visit(n)
end;
28
procedure list-bfs;
var id, k: integer; val: array[1..max V] of integer;
procedure visit(k: integer);
var t: link;
begin
put(k); /* put a vertex to the queue */
repeat
k: = get; /* get a vertex from the queue */
id: = id + 1; val[k]: = id; /* change the status of k to “visited” */
t: = adj[k]; /* find the neighbors of the vertex k */
while t <> z do
begin
if val[t ↑.v] = 0 then
begin
put(t↑.v); val [t↑.v]: = -1 /* change the vertex t↑.v to “ready” */
end;
t: = t↑.next
end
until queueempty
end;
29
begin
id: = 0; queue-initialze;
for k: = 1 to V do val[k]: = 0; /initialize the
status of all vertices */
for k: = 1 to V do
if val[k] = 0 then visit(k)
end;

Duyệt theo chiều sâu trước và duyệt theo chiều rộng trước
chỉ khác nhau ở chỗ giải thuật đầu dùng stack và giải thuật
sau dùng hàng đợi. Do đó, độ phức tạp tính toán của DFS và
BFS là như nhau.

30
I

D D D D D

E E E E

G G G G

B B B M M M

C C L L

F K

A J

Hình 4.4 Nội dung của hàng đợi khi thực hiện BFS

31
2. Đồ thị có trọng số

Chúng ta muốn mô hình hóa những bài toán thực tế sử dụng


những trọng số (weights ) hay chi phí (costs) được gắn vào
mỗi cạnh.

Có hai bài toán được giải quyết trong phần này:


- tìm một cách ít tốn chí phí nhất để nối tất cả các đỉnh
trong đồ thị.
- Tìm một lối đi với tổng chi phí ít nhất giữa hai đỉnh đã
cho.

Bài toán thứ nhất là bài toán cây bao trùm tối thiểu; bài toán
thứ hai là bài toán tìm đường đi ngắn nhất.

32
Cây bao trùm tối thiểu
Một cây bao trùm tối thiểu (minimum spanning tree ) của một
đồ thị có trọng số là một tập các cạnh chạm tất cả các đỉnh
sao cho tổng trọng số của các cạnh là nhỏ nhất.
Cây bao trùm tối thiểu không nhất thiết là duy nhất trong
một đồ thị.
8 7

b c d
Hình 4.5 Cây bao 9
4
trùm tối thiểu 11
2
14
a
i 4 e
6

8 7 10

h g f
1 2

33
Giải thuật Prim

Giải thuật Prim là giải thuật để giải bài toán cây bao trùm
tối thiểu. Giải thuật này cũng trình bày một chiến lược
để giải một bài toán tối ưu hóa: giải thuật tham lam
(greedy):
Tại mỗi bước của giải thuật, ta phải chọn một trong một
số khả năng lựa chọn. Chiến lược tham lam đề xuất việc
lựa chọn khả năng mà xem ra tốt nhất tại lúc đó.
Một chiến lược như vậy thường không đảm bảo đem lại lời
giải tối ưu toàn cục cho các bài toán.
Tuy nhiên đối với bài toán cây bao trùm tối thiểu, ta có thể
chứng minh rằng chiến lược tham lam có thể đem lại
cây bao trùm với tổng trọng số tối thiểu.

34
Xây dựng cây bao trùm tối thiểu

Giả sử chúng ta có một đồ thị vô hướng, liên thông


G = (V, E) với hàm gán trọng số w: E → R và muốn tìm
một cây bao trùm tối thiểu cho G.

Có một giải thuật tổng quát mà tạo dựng dần cây bao trùm
tối thiểu một lúc thêm một cạnh.

Giải thuật này duy trì một tập A mà luôn luôn là một tập
con của một cây bao trùm tối thiểu.

Tại mỗi bước, một cạnh (u, v) được chọn để thêm vào tập A
mà không vi phạm hệ thức bất biến A ∪ {(u, v)} luôn luôn là
một tập con của một cây bao trùm tối thiểu.

35
Ta gọi một cạnh như thế là cạnh an toàn cho tập A vì nó có
thể được thêm vào A một cách an toàn mà không vi phạm hệ
thức bất biến nêu trên.
procedure GENERIC_MST(G, w);
/* G is a weighted graph with the weight function w */
begin
A := ∅;
while A does not form a spanning tree do
begin
find an edge (u,v) that is safe for A;
add (u,v) to A.
end
/* the set A at this point is the result */
end;

36
Giải thuật Prim
Trong giải thuật Prim, tập A hình thành một cấu trúc cây. Một
cạnh an toàn được đưa vào A thường là cạnh có trọng số nhỏ
nhất nối cây A với một đỉnh không thuộc về cây.

Cây bao trùm khởi đi từ một nút rễ bất kỳ r và phát triển cho
đến khi cây phủ tất cả mọi đỉnh trong V. Tại mỗi bước, một
cạnh nhẹ (light edge) nối một đỉnh trong A với một cạnh trong
V - A.
Trong quá trình thực hiện giải thuật này, tất cả những đỉnh
mà không thuộc về cây A được đặt trong một hàng đợi có độ
ưu tiên Q dựa trên một trường là key. Với mỗi đỉnh v, key[v] là
cạnh có trọng số nhỏ nhất của những cạnh nối v với một đỉnh
trong cây. Theo qui ước key[v] = ∞ nếu không tồn tại một
cạnh như vậy.

37
Trường p[v] lưu tên của đỉnh cha của đỉnh v trong cây.
procedure MST-PRIM (G, w, r);
/* G is weighted graph with the weight function w, and r is an arbitrary
root vertex */
begin
Q: = V[G];
for each u ∈ Q do key[u]: = ∞;
key[r]: = 0; p[r]: = NIL;
while Q is not empty do
begin
u: = EXTRACT-MIN(Q);
for each v ∈ Q and w(u, v) < key[v] then
/ * update the key field of vertice v */
begin
p[v] := u; key[v]: = w(u, v)
end
end
end;

38
8 7 Hình 4.6. Một thí
4 b c d 8
dụ về giải thuật
2 4
Prim.
11
a i 14 e
7 6

1 2 10
8
h g f

8 7

4 b c d 8

2 4
11
a i 14 e
7 6

1 2 10
8
h g f

39
8 7

4 b c d 8

2 4
11
a i 14 e
7 6

1 2 10
8
h g f

8 7

4 b c d 8

2 4
11
a i 14 e
7 6

1 2 10
8
h g f

40
8 7

4 b c d 8

2 4
11
a i 14 e
7 6

1 2 10
8
h g f

8 7

4 b c d 8

2 4
11
a i 14 e
7 6

1 2 10
8
h g f

41
8 7

4 b c d 8

2 4
11
a i 14 e
7 6

1 2 10
8
h g f

8 7

4 b c d 8

2 4
11
a i 14 e
7 6

1 2 10
8
h g f

42
8 7

4 b c d 8

2 4
11
a i 14 e
7 6

1 2 10
8
h g f

Độ phức tạp của giải thuật Prim tùy thuộc vào cách
mà chúng ta thi công hàng đợi có độ ưu tiên.

43
Độ phức tạp của giải thuật Prim
Tính chất: Nếu Q được thi công như là một heap nhị phân thì
thời gian tính toán của giải thuật Prim là O(E lgV).
Nếu Q được thi công như là một heap nhị phân, chúng ta có
thể lập ra một heap trong bước khởi tạo với chi phí thời gian
là O(V).
Vòng lặp while được thực hiện V lần và vì mỗi thao tác
EXTRACT-MIN có độ phức tạp O(lgV), chi phí tính toán
cho tất cả các lệnh gọi EXTRACT-MIN là O(VlgV).
Vòng lặp for bên trong vòng lặp while được thực hiện tổng
cộng O(E) lần, vì tổng chiều dài của tất cả các danh sách kế
cận là 2E. Việc cập nhật khóa của đỉnh v trong heap tốn
O(lgV) lần. Như vậy, tổng chi phí tính toán của giải thuật
Prim là O(V lgV + 2E lgV) = O(E lgV).
44
3. Đồ thị có hướng

Các đồ thị có hướng là các đồ thị trong đó các cạnh nối với
các nút có hướng.

H I
B C G

D E J K

L M
Hình 4.7. Một thí dụ về đồ thị có hướng

45
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.

Thí dụ, đồ thị có hướng có thể được dùng để mô hình hóa


một đường dây sản xuất (assembly line).

Trong phần này, chúng ta xem xét các giải thuật sau:
- tính bao đóng truyền (transitive closure)
- sắp thứ tự topo (topological sorting)

46
Tính bao đóng truyền

Trong đồ thị có hướng, chúng ta quan tâm đến tập đỉnh


mà đến được từ một đỉnh nào đó bằng cách duyệt các
cạnh trong đồ thị theo một hướng đã được ấn định.

Một tác vụ mà ta muốn thực hiện là “thêm một cạnh từ x


đến y nếu tồn tại một cách nào đó để đi từ x đến y”
Đồ thị tạo ra bằng cách thêm tất cả các cạnh có tính
chất trên được gọi là bao đóng truyền của đồ thị.

Vì đồ thị bao đóng truyền thì thường là đồ thị dày, do đó


chúng ta dùng cách biểu diễn ma trận kế cận.

47
Giải thuật Warshall

Có một giải thuật đơn giản để tính bao đóng truyền của
một đồ thị được biểu diễn bằng ma trận kế cận.
for y : = 1 to V do
for x : = 1 to V do
if a[x, y] then
for j: = 1 to V do
if a[y, j] then a[x, j]: = true;

S. Warshall đề ra giải thuật này năm 1962, dựa trên một


quan sát đơn giản: “Nếu tồn tại một cách để đi từ nút x đến
nút y và cách để đi từ nút y đến nút j, thì sẽ có cách để đi từ
nút x đến nút j.”

48
Một thí dụ tính bao đóng truyền ( cho đồ thị hình 4.7)
ABCDEFGHI JKLM
A 1 1 0 0 0 11 0 0 00 0 0
B 0 1 0 0 0 00 0 0 00 0 0
C 1 0 1 0 0 00 0 0 00 0 0 Ma trận kế cận ứng
D 0 0 0 1 0 10 0 0 00 0 0 với bước khởi đầu
E 0 0 01 1 00 0 0 0 0 0 0 của giải thuật
F 0 0 0 01 10 0 0 0 0 0 0 Warshall
G 0 0 1 01 01 0 0 1 0 0 0
H 0 0 0 00 01 1 1 0 0 0 0
I 0 0 0 0 0 00 1 1 0 0 0 0
J 0 0 0 0 0 00 0 0 1 1 1 1
K 0 0 0 0 0 00 0 0 0 1 0 0
L 0 0 0 0 0 00 00 0 0 1 1
M 0 0 0 0 0 00 00 0 0 1 1
49
ABCDEFGH I JKLM
A1 1 1 1 1 1 1 0 0 11 1 1
B 01 0 0 0 0 0 0 0 00 0 0 Ma trận kế cận ứng với
C 11 1 1 1 1 1 0 0 11 1 1 bước cuối cùng của giải
D 00 0 1 1 1 0 0 0 00 0 0 thuật Warshall
E 0 0 01 1 1 0 0 0 0 0 0 0
F 0 0 01 1 1 0 0 0 0 0 0 0
G 1 1 111 1 1 0 0 1 1 1 1
Tính chất 5.3.1 Giải
H 1 1 11 11 1 1 1 1 1 1 1
thuật Warshall tính bao
I 1 1 1 1 11 1 1 1 1 1 1 1
đóng truyền với chi phí
J 1 1 1 1 11 1 0 0 1 1 11
O(V3).
K 0 0 0 0 00 0 0 0 01 0 0
L 1 1 11 11 1 0 0 1 1 11
M 1 1 11 11 1 0 0 1 1 11

50
Bài toán các lối đi ngắn nhất

Đối với đồ thị có trọng số (có hướng hoặc không) ta có thể


muối xây dựng một ma trận cho phép người ta tìm được lối đi
ngắn nhất từ x đến y đối với mọi cặp đỉnh. Đấy là bài toán
những lối đi ngắn nhất cho mọi cặp đỉnh (all-pairs shortest
path problem).
A 4 2
3
1 H I
B 1 C G
1 1
2
2 1
1
1 D E J K
5 2
F 3
2
L M
1

51
Giải thuật Floyd

Có thể dùng một phương pháp tương tự như phương pháp


Warshall, mà được đưa ra bởi R. W. Floyd:

for y : = 1 to V do
for x : = 1 to V do
if a [x, y] > 0 then
for j: = 1 to V do
if a [y, j] > 0 then
if (a[x, j] = 0) or (a[x, y] + a[y, j] < a [x, j])
then
a[x, j] = a[x, y] + a[y, j];

52
Một thí dụ dùng giải thuật Floyd (cho đồ thi hình 4.7)
ABCD EFGHI JKLM
A 0 1 0 0 0 24 0 0 00 0 0
Ma trận kế cận
B 0 0 0 0 0 00 0 0 00 0 0
ứng với bước
C 1 0 0 0 0 00 0 0 00 0 0
khởi đầu của
D 0 0 0 0 0 10 0 0 00 0 0
giải thuật Floyd
E 0 0 02 0 00 0 0 0 0 0 0
F 0 0 0 02 00 0 0 0 0 0 0
G 0 0 1 01 00 0 0 1 0 0 0
H 0 0 0 00 03 0 1 0 0 0 0
I 0 0 0 0 0 00 1 0 0 0 0 0
J 0 0 0 0 0 00 0 0 0 1 3 2
K 0 0 0 0 0 00 0 0 0 0 0 0
L 0 0 0 0 0 550 0 0 0 0 1
M 0 0 0 0 0 00 00 0 0 1 0
53
ABCDEFGH I JKLM Ma trận kế cận ứng
A 6 1 5 6 4 2 4 0 0 56 8 7 với bước cuối của
B 00 0 0 0 0 0 0 0 00 0 0 giải thuật Floyd
C 12 6 7 5 3 5 0 0 67 9 8
D 0 0 0 5 3 1 0 0 0 00 0 0 Tính chất 5.3.2 Giải
E 0 0 02 5 3 0 0 0 0 0 0 0 thuật Floyd để giải bài
F 0 0 04 2 5 0 0 0 0 0 0 0 toán những lối đi
G 2 3 131 4 6 0 0 1 2 4 3 ngắn nhất giữa những
H 5 6 46 47 3 2 1 4 5 7 6 cặp có độ phức tạp
I 6 7 5 7 5 8 4 1 2 5 6 8 7 tính toán O(V3).
J 10 11 9 11 9 12 8 0 0 9 1 3 2
K 0 0 0 0 0 0 0 0 0 0 0 0 0
L 7 8 68 6 9 5 0 0 6 7 2 1
M 8 9 7 9 7 10 6 0 0 7 8 1 2

54
Giải thích giải thuật Floyd
Giải thuật Floyd lặp V bước trên ma trận kế cận a.

Sau bước lặp thứ y, a[x, j] sẽ chứa chiều dài nhỏ nhất của
bất kỳ lối đi nào từ đỉnh x đến đỉnh j mà không đi qua
những đỉnh mang chỉ số lớn hơn y. Nghĩa là, x và j có thể
bất kỳ đỉnh nào nhưng những đỉnh trung gian trên lối đi
phải nhỏ hơn hay bằng y.

Tại bước lặp thứ y, ta tính các phần tử của ma trận a bằng
công thức sau:
ay[x,j] = min( ay-1[x,j], ay-1[x, y] + ay-1[y, j])
Chỉ số y chỉ trị của một phần tử trong ma trận a sau bước
lặp thứ y.

55
Công thức này được minh họa bằng hình vẽ sau đây.

ay-1[x,y] ay-1[y,j ]

j
x

ay-1[x,j ]

56
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 như sau:
u < v nếu có một lối đi từ u đến v trong G.

Quan hệ này có 3 tính chất:


(1) Với mỗi đỉnh trong V[G], not (u < u). (không phản xạ)
(2) nếu u < v, thì not( v < u) . (không đối xứng)
(3) nếu u < v và v < w, thì u < w. (Truyền)

57
Tập thứ tự riêng phần

Một quan hệ < trên tập S mà thỏa hai tính chất bất đối xứng
và truyền là một quan hệ thứ tự riêng phần (partial ordering)
của S, và một tập có trên nó một quan hệ thứ tự riêng phần
thì được gọi là tập thứ tự riêng phần (partially order set ).
Như vậy, bất kỳ đồ thị có hướng không chu trình có thể được
coi như là một tập thứ tự riêng phần.

58
Tập thứ tự riêng phần (tt.)
Mặt khác, giả sử S là một tập thứ tự riêng phần với quan hệ
thứ tự riêng phần <, thì S có thể được coi như là một đồ thị
có hướng mà các đỉnh là những phần tử trong tập S và các
cạnh được định nghĩa như sau:
(u,v) là một cạnh nếu u < v.
Hơn nữa, ta có thể chứng minh thêm rằng một tập thứ tự
riêng phần S, có thể được coi như là một đồ thị có hướng,
thì không có chu trình.

59
Xếp thứ tự tôpô

Cho G là một đồ thị có hướng không chu trình. Một


thứ tự tôpô (topological sort)T của G là một thứ tự
tuyến tính mà bảo toàn thứ tự riêng phần ban đầu
trong tập đỉnh V[G].

Nghĩa là: nếu u < v trong V (tức là nếu có một lối đi


từ u đến v trong G), thì u xuất hiện trước v trong thứ
tự tuyến tính T.

60
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

61
Sắp thứ tự tôpô1 (Phương pháp 1)
Có vài phương pháp để sắp thứ tự tôpô.
Phương pháp 1
Ý tưởng căn bản là tìm một nút không có nút đi sau (no
successor) loại bỏ nó ra khỏi đồ thị và đưa nó vào một danh
sách.
Lặp lại quá trình này cho đến khi đồ thị rỗng thì sẽ sinh ra một
danh sách. Đảo ngược danh sách này ta sẽ được thứ tự tôpô.
Algorithm:
while the graph has a node with no successors do
remove one such node from the graph and
add it to the end of a list.
if the loop terminates with the graph empty
then the list shows the reserse of a topological order.
else the graph contains a cycle.
62
Hình 4.8 Sắp thứ tự tôpô bằng phương pháp 1

H I
B C G

D E J K

L M

C B D E F I H G A M L K J
J K L M A G H I E F D B C

63
Phương pháp 2 (sắp xếp tôpô)
Phương pháp thứ hai là thực hiện theo kiểu tìm kiếm theo
chiều sâu trước và 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.
Algorithm:
Start with nodes with no predecessor, put them in the stack.
while the stack is not empty do
if the node at top of the stack has some successors
then
push all its successors onto the stack
else pop it from the stack and add it to the list.

64
1 2

1
0

9
8

3 5

8 6
5 5 5 4 4 4 Hình 4.9 Sắp thứ thự tôpô
3 3 3 3 3 10 10 10 10 bằng phương pháp 2.
2 2 2 2 2 2 2 2 2 2 2
1 1 1 1 1 1 1 1 1 1 1 1 1 9
7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7

65
Bài toán những lối đi ngắn nhất từ một đỉnh nguồn
Thử xét bài toán những lối đi ngắn nhất từ một đỉnh nguồn
(single-source shortest-paths problem):
Cho một đồ thị G = (V, E), chúng ta muốn tìm một lối đi ngắn
nhất từ một đỉnh nguồn nào đó s ∈ V đến mọi nút v ∈ V.

Biểu diễn những lối đi ngắn nhất


Chúng ta muốn tính không chỉ trọng số của những lối đi ngắn
nhất mà còn xác định những đỉnh trên những lối đi ngắn nhất.
Cho một đồ thị G = (V, E), ta duy trì cho mỗi đỉnh v ∈ V một
đỉnh đi trước p[v] mà là một đỉnh khác hay là NIL. Giải thuật
gán trị cho thuộc tính p sao cho dãy các đỉnh đi trước xuất
phát từ đỉnh v sẽ cho ra lối đi ngắn nhất từ s đến v.

66
Những lối đi ngắn nhất và sự nới lỏng
Giải thuật tìm những lối đi ngắn nhất thường dùng một kỹ thuật
được gọi là sự nới lỏng (relaxation).
Sự nới lỏng là một phương pháp mà liên tiếp giảm cận trên của
trọng số của lối đi ngắn nhất thực sự của mỗi đỉnh cho đến khi
cận trên bằng với trọng số lối đi ngắn nhất.
Như vậy, với mỗi đỉnh v ∈ V, ta duy trì thuộc tính d[v], mà là cận
trên của trọng số của một lối đi ngắn nhất từ đỉnh nguồn s đến v.
Ta gọi d[v] là ước lượng lối đi ngắn nhất (shortest path estimate).
Quá trình nới lỏng một cạnh (u, v) bao gồm việc thử xem ta có
thể cải thiện lối đi ngắn nhất đến v mà đang tìm thấy bằng cách đi
qua u và nếu như vậy, ta cập nhật d[v] và p[v]. Một bước nới lỏng
sẽ làm giảm ước lượng lối đi ngắn nhất d[v] và cập nhật thuộc
tính p[v].
67
2
u 5 9 v

Relax(u,v) 2
u v
u 5 6
v
5 7
2 Relax(u,v)

(a) u v
5 6
2

(b)

Hình 4.10: Sự nới lỏng của một cạnh

68
Giải thuật Dijkstra
Giải thuật này giải bài toán những lối đi ngắn nhất từ đỉnh
nguồn cho một đồ thị có hướng, có trọng số G = (V, E) trong
trường hợp các trọng số của các cạnh là trị không âm.
Giải thuật này duy trì một tập S của các đỉnh mà trọng số của
các lối đi ngắn nhất từ đỉnh nguồn đã được xác định. Nghĩa
là, với mọi đỉnh v ∈ S, ta có
d[v] = min(ước lượng lối đi ngắn nhất từ s đến v)
Giải thuật liên tiến chọn đỉnh u ∈ V – S với thuộc tính d nhỏ
nhất, đưa u vào S, và nới lỏng mọi cạnh đi ra từ u.
Trong giải thuật sau đây, ta dùng hàng đợi có độ ưu tiên Q
chứa tất cả các đỉnh trong V-S, lập khóa theo thuộc tính d. Và
giả định đồ thị G được diễn tả bằng các danh sách kế cận.

69
procedure dijkstra(G, w, s);
/* G is a graph, w is a weight function and s is the source node */
begin
for each vertex v ∈ V[G] do /* initialization */
begin d[v]: = ∞; p[s]: = NIL end;
d[s]: = 0; S: = ∅; Q: = V[G]
while Q is not empty do
begin
u: = EXTRACT-MIN (Q); S: = S ∪ {u};
for each vertex v ∈ Adj [u] do /* relaxation */
if d[v] > d [u] + w(u, v) then
begin d[v]: = d[u] + w(u, v); p[v]: = u end
end
end

70
Giải thuật Dijkstra
• Vì giải thuật Dijkstra luôn luôn chọn đỉnh “gần nhất” trong
V-S để đưa vào tập S, nó thực sự sử dụng chiến lược tham
lam.
• Giải thuật tham lam thường không đảm bảo đem lại lời giải
tối ưu trong trường hợp tổng quát, nhưng đối với bài toán
này, giải thuật Dijkstra thực sự đã đem lại những lối đi ngắn
nhất.

• Giải thuật Dijkstra tương tự như giải thuật Prim dùng để


tính cây bao trùm tối thiểu. Nó cũng dùng một hàng đợi có độ
ưu tiên để tìm một đỉnh “nhẹ nhất” bên ngoài một tập, đưa
đỉnh đó vào tập, và điều chỉnh lại trọng số của những đỉnh
còn ở lại bên ngoài tập.
71
Một thí dụ
u v u v

∞ ∞
10 ∞
10 10

0
s 0
s

5 5
∞ ∞ 5 ∞

x y x y

72
u v u v
1
1
8 14 8 13
10
9 10 9
3 3
0 4 s 4
s 2 0 2

7 6
7 6
5
5 7
5 7
2
2

x y x y

73
u v u v

1
8 9 1
8 9
10 9
3 10
6 3 9
2 6
0 0
4 2
7 s 7 4
s 5
5
5 7
5 7
2
2
y x y
x
(s, u): <s, x, u> (s, v): <s, x, u, v>
(s, x): <s, x> (s, y): <s, x, y, y>

74
Độ phức tạp của giải thuật Dijkstra
• Nếu hàng đợi có độ ưu tiên Q = V – S được thực hiện bởi
một mảng tuyến tính, mỗi thao tác EXTRACT-MIN tốn
O(V), và có tất cả |V| thao tác như vậy, do đó ta có một chi
phí tính toán cho thao tác này là O(V2).
• Mỗi đỉnh v ∈ V được đưa vào tập S đúng một lần, do đó mỗi
đỉnh trong các danh sách kế cận Adj[v] được xét đúng một
lần trong suốt tiến trình của giải thuật.
Vì tổng số đỉnh trong tất các các danh sách kế cận là |E|, có
tất cả |E| bước lặp cho vòng lặp for, với mỗi lần lặp tốn O(1).
Thời gian tính toán của giải thuật là
O(V2 + E) = O(V2).

75
Độ phức tạp của giải thuật Dijkstra
• Nếu hàng đợi có độ ưu tiên Q được hiện thực bởi một cấu
trúc heap, mỗi thao tác EXTRACT-MIN tốn chi phí O(lgV),
và có tất cả |V| thao tác này, do đó ta có tổng chi phí cho
thao tác EXTRACT-MIN là O(VlgV).

Phép gán d[v]: = d[u] + w(u, v) đòi hỏi một thao tác cập
nhật khóa của đỉnh v trong heap và nó tốn O(lgV). Có tất cả
|E| thao tác như vậy. Do đó tổng thời gian tính toán của giải
thuật là
O(V lgV + E lgV).

76

You might also like