Khi đồ thị G liên thông thì phép tìm kiếm theo chiều rộng hoặc chiều sâu từ một đỉnh bất kỳ sẽ thăm được tất cả các đỉnh còn lại của đồ thị. Trong trường hợp này, các cung của đồ thị G sẽ được phần làm 2 loại:
Tập T gồm tất cả các cung được dùng đến (được duyệt) qua trong phép tìm kiếm và tập B gồm các cung còn lại.
Tất cả các cung trong tập hợp T cùng với các đỉnh tương ứng sẽ tạo thành 1 cây bao gồm mọi đỉnh của G. Cây như vậy gọi là cây khung của G
a) b) c)
Hình 5.8. Đồ thị và 3 cây khung của nó
Tùy theo phép duyệt theo chiều rộng hoặc theo chiều sâu mà cây khung tương ứng sẽ được gọi là cây khung theo chiểu rộng hoặc cây khung theo chiều sâu.
Đối với cây bao trùm, chúng ta quan tâm tới hai bài toán sau:
Bài toán 1. cho đồ thị G=<V,E> là đồ thị vô hướng liên thông, hãy xây dựng 1 cây bao trùm của G
Bài toán 2. cho đồ thị G=<V,E> là đồ thị vô hướng liên thông có trọng số, hãy tìm cây bao trùm nhỏ nhất của G.
Tìm một cây bao trùm của đồ thị
Để tìm một cây bao trùm của đồ thị vô hướng liên thông, có thể sử dụng kỹ thuật tìm kiếm theo chiều rộng hoặc tìm kiếm theo chiều sâu để thực hiện. Giả sử chúng ta cần xây dựng cây bao trùm xuất phát từ một đỉnh u nào đó, trong cả hai trường hợp, mỗi khi chúng ta đến được đỉnh v nào đó thì cạnh (u,v) được kết nạp vào cây bao trùm. Hai kỹ thuật này được thể hiện trong hai thủ tục STREE_DFS(u) và STREE_BFS(u) như sau: Procedure stree_DFS(v);
lien thong G cho boi danh sach ke. Cac bien Chuaxet, Ke, T la toan cuc*) begin Chuaxet[v]:=false; For u ∈Ke(v) do If Chuaxet[u] then Begin T:=T (u,v); STREE_DFS(u); End; end; (* Main Program *) begin (* Initialition *) for u∈V do Chuaxet[u]:=true;
T := ∅; (* T la tap canh cua cay khung *)
STREE_DFS(root); ( root la dinh nao do cua do thi *) end.
Procedure Stree_BFS(v);
(* tim kiem theo chieu rong ap dung tim tap canh cua cau khung T cua do thi vo huong lien thong G cho boi danh sach Ke *)
begin Queue:=∅; Queue ⇐r; Chuaxet[r]:=false; While queue <> ∅do Begin V ⇐queue; For r ∈ Ke(v) do
If Chuaxet[u] then Begin Queue ⇐u; Chuaxet[u]:=false; T:= T (u,v); End; End; end; (* Main Program *); begin for u ∈V do Chuaxet[u]:=true;
T := ∅; (* T la tap canh cua cay khung *)
Stree_BFS(root); (* root la mot dinh tuy y cua do thi *) end.
Tìm cây bao trùm nhỏ nhất của đồ thị
Phát biểu bài toán: cho đồ thì G=<V,E> là đồ thị liên thông với tập đỉnh V={1,2,3,..,n} và tập cạnh E gồm m cạnh. Mỗi cạnh e của đồ thị được gán với một số không âm c(e) được gọi là độ dài của nó. H=<V,T> là một cây bao trùm của G, ta gọi độ dài c(H) của cây bao trùm H là tổng độ dài các cạnh của nó . Bài toán đặt ra là trong số các cây khung của đồ thị, tìm cây khung có độ dài nhỏ nhất của đồ thị. Chúng ta không thể liệt kê toàn bộ cây bao trùm và chọn cây có trọng số nhỏ nhất vì phương án này không khả thi - số cây bao trùm của đồ thị là rất lớn nn-2 , không thể áp dụng với đồ thị có số nút cỡ vài chục.
Thuật toán của bài toán như sau:
Bước 1: Thiết lập tập cạnh của cây bao trùm là ∅. Chọn cạnh e=(i,j) có độ dài nhỏ nhất bổ sung vào T.
Bước 2: Trong số các cạnh của E\T, tìm cạnh e=(i1,j1) có độ dài nhỏ nhất sao cho khi bổ sung cạnh đó vào T thì không tạo nên chu trình. Để thực hiện điều này chúng ta phải chọn cạnh có độ dài nhỏ nhất sao cho i1∈T và j1T hoặc j1∈T và i1T.
Bước 3: Kiểm tra xem T đã đủ n-1 cạnh chưa, nếu đủ thì T là cây bao trùm nhỏ nhất, ngược lại thì lặp lại bước 2.
Ví dụ: Tìm cây bao trùm nhỏ nhất của đồ thị trong hình:
Hình 5.9. Ví dụ tìm cây bao trùm nhỏ nhất của đồ thị
Bước 1: Đặt T=∅, bổ sung cạnh (3,5) có độ dài nhỏ nhất vào T. Bước 2: Sau 3 lần lặp đầu tiên, ta bổ sung vào các cạnh (4,5) và (4,6). Không thể bổ sung các cạnh (5,6) vì sẽ tạo nên chu trình vì đỉnh 5,6 đã có mặt trong T. Tiếp đó, chúng ta bổ sung cạnh (1,3) và (2,3) vào T.
Bước 3: Tập T đã đủ n-1 cạnh, như vậy cây bao trùm nhỏ nhất là T={(3,5) (4,5) (4,6) (1,3)(2,3) }
Thuật toán Kruskal
Thuật toán Kruskal xây dựng tập cạnh T của cây khung nhỏ nhất H=<V,T> như sau: 1. Sắp xếp các cạnh của đồ thị G theo thứ tự tăng dần của trọng số cạnh
2. Xuất phát từ tập T=∅, ở mỗi bước ta sẽ lần lượt duyệt trong danh sách các cạnh đã được sắp xếp, từ cạnh có trọng số nhỏ tới cạnh có trọng số lớn để bổ sung vào T sao cho không tạo thành chu trình với các cạnh đã có trong T trước đó.
3. Thuật toán kết thúc nếu chúng ta thu được tập T gồm n-1 cạnh Thủ tục minh họa thuật toán Kruskal:
T: = đồ thị rỗng
for i: = 1 to n 1 begin
e: = một cạnh bất kỳ của G với trọng số nhỏ nhất và không tạo ra chu trình trong T, khi ghép nó vài T.
T: = T với cạnh e đã được ghép vào. end {T là cây khung nhỏ nhất}.
Thuật toán Prim
Thuật toán Kruskal không hoạt động hiệu quả với trường hợp đồ thị có số cạnh khoảng m=n*(n1)/2 (m là số cạnh của đồ thị, n là số cạnh của cây khung nhỏ nhất T). Trong tình huống như vậy, thuật toán Prim hoạt động hiệu quả hơn, thuật toán Prim còn được gọi là thuật toán láng giềng gần nhất. Trong thuật toán này, bắt đầu từ định s tùy ý, nối đỉnh s với đỉnh y sao cho trọng số cạnh c[s,y] nhỏ nhất. Tiếp theo, từ đỉnh s hoặc đỉnh y tìm cạnh có độ dài nhỏ nhất, điều này dẫn đến đỉnh thứ 3 z và ta thu được cây bộ phận gồm 3 đỉnh 2 cạnh. Quá trình được lặp lại cho tới khi tìm được n-1 cạnh của cây bao trùm T. Đó là cây bao trùm nhỏ nhất.
Chú ý: Có nhiều hơn 1 cây khung nhỏ nhất ứng với một đồ thị liên thông và có trọng số Mô tả thuật toán Prim:
Procedure Prim (G: đồ thị liên thông có trọng số với n đỉnh).
T: = cạnh có trọng số nhỏ nhất. for i = 1 to n 2.
begin
e:= cạnh có trọng số tối thiểu liên thuộc với một đỉnh trong T và không tạo ra chu trình trong T nếu ghép nó vào T.
T: = T với e được ghép vào end {T là cây khung nhỏ nhất của G}.
Chú ý: Điểm khác nhau giữa thuật toán Kruskal và thuật toán Prim:
Trong thuật toán Prim, ta chọn các cạnh có trọng số nhỏ nhất, liên thông với các đỉnh đã thuộc cây và không tạo ra chu trình.
Trong thuật toán Kruskal, ta chọn các cạnh có trọng số tối thiểu mà không nhất thiết phải liên thuộc với các đỉnh của cây và không tạo ra chu trình.
Minh họa cài đặt thuật toán: #include <stdio.h>
#include <values.h> #define max 100 typedef struct Egde { int x,y;
};
//doc du lieu tu tap tin
void Doc_File(int A[max][max],int &n) { FILE*f = fopen("Input.txt","rb"); fscanf(f,”%d”,&n); for(int i =0;i<n;i++) { for(int j =0;j<n;j++) { fscanf(f,”%d”,&A[i][j]); } } fclose(f); }
//xuat du lieu ra tap tin
void Ghi_File(Egde L[max],int n,int Sum) { FILE*f = fopen("Output.txt","wb"); fprintf(f,"%d\n",Sum);
for(int i =0; i<n-1; i++)
fprintf(f,"%d - %d\n",L[i].x+1,L[i].y+1); fclose(f);
}
//thuat toan Prim
void Prim(int A[max][max], int n) { char D[max];
Egde L[max];
int min = MAXINT, Dem = 0, Sum = 0; for(int i=0; i<n; i++)
D[i]=0;
for(int j=1; j<n; j++)
if(min>A[0][j] && A[0][j]!=0){ min = A[0][j]; L[0].y = j; } L[0].x = 0; D[0] = 1; D[L[0].y] = 1; Sum+=min; Dem++; do { min = MAXINT; for( i=0; i<n; i++) if(D[i]==1)
for( j=0; j<n; j++)
if(A[i][j]>0 && min>A[i][j] && D[j]==0){ min = A[i][j]; L[Dem].x = i; L[Dem].y = j; } Sum+=min; D[L[Dem].y] = 1; Dem++; } while(Dem<n-1); Ghi_File(L,n,Sum); }
//chuong trinh chinh void main() { int A[max][max],n; Doc_File(A,n); Prim(A,n); }