4. BÀI TOÁN CÂY KHUNG NHỎ NHẤT

Bài toán cây khung nhỏ nhất của đồ thị là một trong số những bài toán tối ưu trên đồ thị tìm được ứng dụng trong nhiều lĩnh vực khác nhau của đời sống. Trong mục này chúng ta trình bày những thuật toán cơ bản để giải bài toán nào. Trước hết chúng ta phát biểu nội dung bài toán.

Cho G=(V,E) là đồ thị vô hướng liên thông với tập đỉnh V={ 1, 2, . . . ,n} và tập cạnh E gồm m cạnh. Mỗi cạnh E của đồ thị G được gán với một số không âm c(e), gọi là độ dài của nó. Giả sử H=(V,T) là cây khung của đồ thị G. Ta gọi độ dài c(H) của cây khung H là tổng độ dài các cạnh của nó:

C(H) = å c(e).
            eÎ T

Bài toán đặt ra là trong tất cả cây khung của đồ thị G hãy tìm cây khung với độ dài nhỏ nhất. Cây khung như vậy như vậy được gọi là cây khung nhỏ nhất của đồ thị và bài toán đặt ra được gọi là bài toán cây khung nhỏ nhất.

Để minh hoạ cho những ứng dụng bài toán cây khung nhỏ nhất, dưới đây, ta phát biểu hai mô hình thực tế tiêu biểu của nó.


Bài toán xây dựng hệ thống đường sắt.
Giả sử ta muốn xây dựng một hệ thống đường sắt nối n thành phố sao cho hành khách có thể đi từ bất kỳ một thành phố nào đến bất kỳ một trong các thành phố còn lại. Mặt khác trên quan điểm kinh tế đòi hỏi là chi phí xây dựng hệ thống đường phải nhỏ nhất. Rõ ràng đồ thị mà đỉnh là các thành phố còn các cạnh là các tuyến đường sắt nối các thành phố tương ứng với phương án xây dựng tối ưu phải là cây. Vì vây, bài toán đặt ra dẫn về bài toán tìm cây khung nhỏ nhất trên đồ thị đầy đủ n đỉnh, mỗi đỉnh tương ứng với một thành phố, với độ dài trên các các cạnh chính là chi phí xây dựng đường ray nối hai thành phố tương ứng (chú ý là trong bài toán này ta giả thiết là không xây dựng tuyến đường sắt có các nhà ga phân tuyến nằm ngoài các thành phố).
 

Bài toán nối mạng máy tính. Cần nối mạng một hệ thống gồm n máy tính đánh số từ 1 đến n. Biết chi phí nối máy i với máy j là c[i,j], i,j = 1, 2, . . . ,n ( thông thường chi phí này phụ thuộc vào độ dài cáp nối cần sử dụng). Hãy tìm cách nối mạng sao cho tổng chi phí nối mạng là nhỏ nhất.


Đ
ể giải bài toán cây khung nhỏ nhất, tất nhiên có thể liệt kê tất cả các cây khung của đồ thị và chọn trong số cây khung ấy cây khung nhỏ nhất. Phương pháp như vậy, trong trường hợp đồ thị đầy đủ, sẽ đòi hỏi thời gian cỡ nn-2 , và rõ ràng không thể thực hiện được ngay cả với những đồ thị với số đỉnh cỡ hàng chục. Rất may là đối với bài toán cây khung nhỏ nhất chúng ta đã có những thuật toán rất hiệu quả để giải chúng. Chúng ta xét hai trong số những thuật toán như vậy: Thuật toán Kruskal và Thuật toán Prim.

4.1. Thuật toán Kruskal

Thuật toán sẽ xây dựng tập cạnh T của cây khung nhỏ nhất H=(V,T) theo từng bước. Trước hết sắp xếp các cạnh của đồ thị G theo thứ tự không giảm của độ dài. Bắt đầu từ tập T=Æ , ở mỗi bước ta sẽ lần lượt duyệt trong danh sách cạnh đã sắp xếp, từ cạnh có độ dài nhỏ đến cạnh có độ dài lớn hơn, để tìm ra cạnh mà việc bổ sung nó vào tập T gồm n-1 cạnh. Cụ thể, thuật toán có thể mô tả như sau:

Procedure Kruskal;

Begin

T:=Æ;

While çTç < (n-1) and (E<>Æ) do

Begin

E:=E\{e};

if (T È { e} không chứa chu trình) then T:= TÈ { e} ;

End;

if (ç Tç < n-1) then Đồ thị không liên thông;

End;

 

Thí dụ 3.Tìm cây khung nhỏ nhất của đồ thị cho trong hình 3 dưới.

Bước khởi tạo. Đặt T:=Æ . Sắp xếp các cạnh của đồ thị theo thứ tự không giảm của độ dài ta có dãy:

(3,5) , (4,6) , (4,5) , (5,6) , (3,4) , (1,3) , (2,3) , (2,4) , (1,2)

dãy độ dài tương ứng của chúng

4, 8, 9, 14, 16, 17, 18, 20, 23.

Hình 3. Đồ thị và cây khung nhỏ nhất

Ở ba lần gặp đầu tiên ta lần lượt bổ sung vào tập T các cạnh (3,5) , (4,6) , (4,5). Rõ ràng nếu thêm cạnh (5,6) vào T thì sẽ tạo thành 2 cạnh (4,5), (4,6) đã có trong T chu trình. Tình huống tương tự cũng xảy ra đối với cạnh (3,4) là cạnh tiếp theo của dãy. Tiếp theo ta bổ sung cạnh (1,3), (2,3) vào T và thu được tập T gồm 5 cạnh:

T = { (3,5) , (4,6) , (4,5) , (1,3) , (2,3) }

Chính là tập cạnh của cây khung nhỏ nhất cần tìm.

Chứng minh tính đúng đắn của thuật toán.

Rõ ràng đồ thị thu được theo thuật toán có n-1 cạnh và không có chu trình, vì vậy theo định lý 1 nó là cây khung của đồ thị G. Như vậy, chỉ còn phải chỉ ra rằng T có độ dài nhỏ nhất. Giả sử tồn tại cây S của đồ thị G mà c(S) < c(T). Ký hiệu ek là cạnh đầu tiên trong dãy các cạnh của T xây dựng theo thuật toán vừa mô tả không thuộc S. khi đó đồ thị con của G sinh bởi cây S được bổ sung cạnh ek sẽ chứa một chu trình C duy nhất đi qua ek. Do chu trình C phải chứa cạnh e thuộc S nhưng không thuộc T nên đồ thị con thu được từ S bằng cách thay cạnh e của nó bởi cạnh ek (ký hiệu đồ thị là S’) sẽ là cây khung. Theo cách xây dựng c(ek) c(e) do đó c(S’) c(S), đồng thời số cạnh chung của S’ và T đã tăng thêm 1 so với số cạnh chung của S và T. Lặp lại quá trình trên từng bước một ta có thể biến đổi S thành T và trong mỗi bước tổng độ dài không tăng, tức là c(T) c(S). Mâu thuẫn thu được chúng tỏ T là cây khung nhỏ nhất.

Về việc lập trình thực hiện thuật toán.

Khối lượng tính toán nhiều nhất của thuật toán chính là ở bước sắp xếp các cạnh của đồ thị theo thứ tự không giảm của độ dài để lựa chọn cạnh bổ sung. Đối với đồ thị m cạnh cần phải thực hiện m log m phép toán để sắp xếp các cạnh của đồ thị thành dãy không giảm theo độ dài. Tuy nhiên, để xây dựng cây khung nhỏ nhất với n-1 cạnh, nói chung ta không cần phải sắp thứ tự toàn bộ các cạnh mà chỉ cần xét phần trên của dãy đó chứa r < m cạnh. Để làm việc đó ta có thể sử dụng các thủ tục sắp xếp dạng Vun đống (Heap Sort). Trong thủ tục này, để tạo đống đầu tiên ta mất cỡ O(m) phép toán, mỗi phần tử tiếp theo trong đống có thể tìm sau thời gian O(log m). Vì vậy, với cải tiến này thuật toán sẽ mất thời gian cỡ O(m+p) log m) cho việc sắp xếp các cạnh. Trong thực tế tính toán số p nhỏ hơn rất nhiều so với m.

Vấn đề thứ hai trong việc thể hiện thuật toán Kruskal là việc lựa chọn cạnh để bổ sung đòi hỏi phải có một thủ tục hiệu quả kiểm tra tập cạnh T È { e} có chứa chu trình hay không. Để ý rằng, các cạnh trong T ở các bước lặp trung gian sẽ tạo thành một rừng. Cạnh e cần khảo sát sẽ tạo thành chu trình với các cạnh trong T khi và chỉ khi cả hai đỉnh đầu của nó thuộc vào cùng một cây con của rừng nói trên. Do đó, nếu cạnh e không tạo thành chu trình với các cạnh trong T, thì nó phải nối hai cây khác nhau trong T. vì thế, để kiểm tra xem có thể bổ sung cạnh e vào T ta chỉ cần kiểm tra xem nó có nối hai cây khác nhau trong T hay không. Một trong các phương pháp hiệu quả để thực hiện việc kiểm tra này là ta sẽ phân hoạch tập các đỉnh của đồ thị ra thành các tập con không giao nhau, mỗi tập xác định bởi một cây con trong T(được hình thành ở các bước do việc bổ sung cạnh vào T). chẳng hạn, đối với đồ thị trong ví dụ 3, đầu tiên ta có sáu tập con 1 phần tử: { 1} , { 2} , { 3} , { 4} , { 5} , { 6} . Sau khi bổ sung cạnh (3,5), ta có các tập con { 1} , { 2} , { 3,5} , { 4} , { 6} . Ở bước thứ 3, ta chọn cạnh (4,5), khi đó hai tập con được nối lại và danh sách các tập con là { 1} , { 2} , { 3,4,5,6} . Cạnh có độ dài tiếp theo là (4,6), do hai đầu của nó thuộc vào cùng một tập con { 3,4,5,6} , nên nó sẽ tạo thành chu trình trong tập này. Vì vậy cạnh này không được chọn. Và thuật toán sẽ tiếp tục chọn cạnh tiếp theo để khảo sát …

Như vậy, để giải quyết vấn đề thứ hai này ta phải xây dựng hai thủ tục: Kiểm tra xem hai đầu u, v của cạnh e=(u,v) có thuộc vào hai tập con khác nhau hay không, và trong trường hợp câu trả lời là khẳng định, nối hai tập con tương ứng thành một tập. Chú ý rằng mỗi tập con trong phân hoạch có thể lưu trữ như là một cây có gốc, và khi đó mỗi gốc sẽ được sử dụng làm nhãn nhận biết tập con tương ứng.

Chương trình trên Pascal thực hiện thuật toán Kruskal với những nhận xét vừa nêu có thể viết như sau:

(* TÌM CÂY KHUNG NHỎ NHẤT THEO THUẬT TOÁN KRUSKAL CỦA ĐỒ THỊ CHO BỞI DANH SÁCH CẠNH *)

uses crt;

type

arrn=array[1. .50] of integer;

arrm= array[1...50] of integer;

var

n,m, minL:integer;

Dau, cuoi, W:arrm;

DauT, CuoiT, Father:arrn;

Connect:boolean;

Procedure Nhapdl;

Var

i:integer;

Fanme:string;

F:text;

Begin

Write(‘Cho ten file du lieu:’) readln(fname);

Assign(f,fname); reset(f);

Readln(f,n,m);

For i:=1 to m do readln(f, Dau[i], Cuoi[i], W[i]);

Close(f);

End;

Procedure Indulieu;

Var i:integer;

Begin

Writeln(‘So dinh ‘,n,’. So canh ’,m);

Writeln(‘Dinh dau Dinh cuoi Do dai’);

For i:=1 to m do

Writeln(Dau[i]:4, Cuoi[i}:10, W[i]:12);

End;

Procedure Heap(First, Last:integer);

Var j,k,t1,t2,t3 : integer;

Begin

J:=first;

While (j<=trunc(last/2)) do

Begin

If (2*j<last) and W[2*j+1]<W[2*j]) then K:= 2*j+1

Else k"=2*j;

If W[k]<W[j} then

Begin

T1:=Dau[j]; t1:=Cuoi[j]; t3:=W[j];

Dau[j]:=Dau[k];Cuoi[j]:=Cuoi[k]; W[j]:=W[k];

Dau[k]:=t1; Cuoi[k]:=t2; W[k]:=t3;

J:=k;

End

Else j:=Last;

End;

End;

Function Find(i:integer):integer;

Var Tro:integer;

Begin

Tro:=i;

While Father[Tro]>0 do Tro:=Father[Tro];

Find:=Tro;

End;

Procedure Union(i,j:integer);

Var x:integer;

Begin

x:=father[i]+father[j];

if father[i]>father[j] then

begin

father[i]:=f;

father[j]:=x;

end

else

begin

father[j]:=i;

father[i]:=x;

end;

End;

Procedure Kruskal;

Var

I, Last, u,v,r1,r2, Ncanh, Ndinh:integer;

Begin

(* Khoi tao mang Father danh dau cay con va khoi tao Heap *)

for i:= 1 to n do father[i]:=-1;

for i:=trunc(m/2) downto 1 do Heap(i,m);

last:=m; Ncanh:=0; Ndinh:=0;

MinL:=0;

Connect:=true;

While (Ndinh<n-1) and (Ncanh<m) do

Begin

Ncanh:=Ncanh+1;

u:=dau[1];

v:=Cuoi[1];

(* Kiem tra u va v co thuoc cung mot cay con *)

r1:=find(u);

r2:=find(v);

if r1<>r2 then

begin

(* Ket nap canh (u,v) vao cay khung *)

Ndinh:=Ndinh+1; Union(r1, r2);

DauT[Ndinh]:=u;

CuoiT[Ndinh]:=v;

MinL:=MinL+W[1];

end;

(* To chuc lai Heap *)

Dau[1]:=Dau[Last];

Cuoi[1]:=Cuoi[Last];

W[1]:=W[Last];

Last:=Last-1;

End;

If Ndinh<>n-1 then Connect:=false;

End;

Procedure Inketqua;

Var i:integer;

Begin

Writeln(‘******************************’);

Writeln(‘***** Ket qua tinh toan ********’);

Writeln(‘*******************************’);

Writeln(‘Do dai cua cay khung nho nhat: ‘,MinL);

Writeln(‘Cac canh cua cay khung nho nhat’);

For i:=1 to n-1 do

             Writeln(‘(‘,DauT[i]:2,’,’,CuoiT[i]:2,’)’);

Writeln(‘*******************************’);

End;

Begin

Clrscr;

Nhapdl;’

Indulieu;

Kruskal;

If connect then Inketqua

Else

Writeln(‘ Do thi khong lien thong’);

Readln;

End.

 

File dữ liệu của bài toán trong ví dụ 3 có dạng sau:

7 9

3 5 4

4 6 8

4 5 9

5 6 14

3 4 16

1 3 17

2 3 18

2 4 20

1 2 23

4.2. Thuật toán Prim

Thuật toán Kruskal làm việc kém hiệu quả với những đồ thị dày (đồ thị với số cạnh m» n(n-1)/2). Trong trường hợp đó thuật toán Prim tỏ ra hiệu quả hơn. Thuật toán Prim còn được gọi là phương pháp lân cận gần nhất. Trong phương pháp này bắt đầu từ một đỉnh tuỳ ý của đồ thị, đầu tiên ta nối s với đỉnh lân cận gần nó nhất, chẳng hạn là đỉnh y. Nghĩa là trong số các cạnh kề của đỉnh s, cạnh (s,y) có độ dài nhỏ nhất. Tiếp theo trong số các cạnh kề với hai đỉnh s hoặc y ta tìm cạnh có độ dài nhỏ nhất, cạnh này dẫn đến đỉnh thứ ba z, và ta thu được cây bộ phận gồm 3 đỉnh và 2 cạnh. Quá trình này sẽ tiếp tục cho đến khi ta thu được cây gồm n đỉnh và n-1 cạnh sẽ chính là cây khung nhỏ nhất cần tìm.

Giả sử đồ thị cho bởi ma trận trọng số C = { c[i,j], i, j= 1, 2, . . . , n} . trong quá trình thực hiện thuật toán, ở mỗi bước để có thể nhanh chóng chọn đỉnh và cạnh cần bổ sung vào cây khung, các đỉnh của đồ thị sẽ được gán cho các nhãn. Nhãn của một đỉnh v sẽ gồm hai phần và có dạng [d[v], near[v]], trong đó d[v] dùng để ghi nhận độ dài của cạnh có độ dài nhỏ nhất trong số các cạnh nối với đỉnh v với các đỉnh của cây khung đang xây dựng (ta sẽ gọi là khoảng cách từ đỉnh v đến tập đỉnh của cây khung), nói một cách chính xác

d[v]:= min { c[v,w] : w Î VH } ( = c[v,z]),

còn near[v] ghi nhận đỉnh của cây khung gần v nhất (near[v]:=z).

Thuật toán Prim được mô tả đầy đủ trong thủ tục sau:

 

Procedur Prim;

Begin

(* buoc khoi tao *)

chon s la mot dinh nao do cua do thi;

VH:={ s} ; T:=Æ ; d[s]:=0; near[s]:=s.

For vÎ V\VH do

Begin

D[v]:=c[s,v];

near[v]:=s;

End;

(* buoc lap *)

stop:=false;

while not stop do

begin

tim u Î V\VH thoa man:

d[u] =min { d[v]: uÎ V\VH } ;

VH:= VH È { u} ; T := T È { (u, near[u])} ;

If ç VH ç =n then

Begin

H=( VH,T) la cay khung nho nhat cua do thi;

Stop:=true;

End

Else

For vÎ V\ VH do

If d[v]>c[u,v] then

Begin

d[v]:=c[u,v];

near[v]:=u;

End;

end;

End;

 

Thí dụ 4. Tìm cây khung nhỏ nhất cho đồ thị xét trong ví dụ 3 theo thuật toán Prim. Ma trận trọng số của đồ thị có dạng

   

1

2

3

4

5

6

 

1

0

33

17

¥

¥

¥

 

2

33

0

18

20

¥

¥

C =

3

17

18

0

16

4

¥

 

4

¥

20

16

0

9

8

 

5

¥

¥

4

9

0

14

 

6

¥

¥

¥

8

14

0

 

Bảng dưới đây ghi nhãn của các đỉnh trong các bước lặp của thuật toán, đỉnh đánh dấu * là đỉnh được chọn để bổ sung vào cây khung (khi đó nhãn của nó không còn bị biến đổi trong các bước lặp tiếp theo, vì vậy ta đánh dấu – để ghi nhận điều đó):

Bước lặp

Đỉnh 1

Đỉnh 2

Đỉnh 3

Đỉnh 4

Đỉnh 5

Đỉnh 6

VH

T

Khởi tạo

[0,1]

[33,1]

[17,1]*

[¥ ,1]

[¥ ,1]

[¥ ,1]

1

Æ

1

-

[18,3]

-

[16,3]

[4,3]*

[¥ ,1]

1,3

(3,1)

2

-

[18,3]

-

[9,5]

-

[14,5]

1,3,5

(3,1), (5,3)

3

-

[18,3]

-

-

-

[8,4]

1,3,5,4

(3,1), (5,3), (4,5)

4

-

[18,3]*

-

-

-

-

1,3,5,4,6

(3,1), (5,3), (4,5), (6,4)

5

-

-

-

-

-

-

1,3,5,4,6,2

(3,1), (5,3), (4,5), (6,4), (2,3)

4.3. Một số bài toán dẫn về bài toán cây khung nhỏ nhất

Trong mục này ta nêu hai bài toán có thể dẫn về bài toán cây khung nhỏ nhất, và vì thể có thể giải được nhờ các thuật toán mô tả trong mục trước

a) Bài toán cây khung lớn nhất. Dễ dàng nhận thấy rằng trong các thuật toán trên ta không cần sử dụng đến đòi hỏi về dấu của độ dài. Vì thế có thể áp dụng chúng đối với đồ thị có các cạnh với độ dài có dấu tuỳ ý. Vì vậy, giả sử ta phải tìm cây lớn nhất (tức là có độ dài c(H) lớn nhất) thì chỉ cần đổi dấu tất cả các đđo và áp dụng một trong hai thuật toán vừa mô tả.

b) Bài toán tìm mạng điện với độ tin cậy lớn nhất. Cho lưới điện có n nút. Đường dây nối nút i với nút j có độ tin cậy 1>p[i ,j]>0, i, j=1, 2, . . . , n. Gọi G = (V, E) là đồ thị tương ứng với lưới điện này. Hãy tìm cây khung H của đồ thị G với độ tin cậy

P p(e)
eÎ H

lớn nhất.

Bài toán này dẫn về bài toán tìm cây khung với tổng độ dài nhỏ nhất trên đồ thị G với độ dài của mỗi cạnh e Î E là –log p(e). Thực vậy, giả sử H là cây khung nhỏ nhất trên đồ thị với độ dài – log p(e), ta có

- å log p(e) - å log p(e)
eÎ H                 e Î H’    

với mọi cây khung H’ của đồ thị G. Từ đó suy ra

å log p(e) å log p(e)
eÎ H             e Î H’     

do đó

log P p(e) log P p(e)
     eÎ H             eÎ H’

hay là

P p(e) P p(e)
eÎ H        eÎ H’  

với mọi cây khung H’. Vậy H là cây khung có độ tin cậy lớn nhất.