Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị

Một đồ thị (graph) G gồm một tập V chứa các đỉnh của đồ thị, và tập E chứa

các cặp đỉnh khác nhau từ V. Các cặp đỉnh này được gọi là các cạnh của G. Nếu e

= (ν, µ) là một cạnh có hai đỉnh ν và µ, thì chúng ta gọi ν và µ nằm trên e, và e

nối với ν và µ. Nếu các cặp đỉnh không có thứ tự, G được gọi là đồ thị vô hướng

(undirected graph), ngược lại, G được gọi là đồ thị có hướng (directed graph).

Thông thường đồ thị có hướng được gọi tắt là digraph, còn từ graph thường mang

nghĩa là đồ thị vô hướng. Cách tự nhiên để vẽ đồ thị là biểu diễn các đỉnh bằng

các điểm hoặc vòng tròn, và các cạnh bằng các đường thẳng hoặc các cung nối các

đỉnh. Đối với đồ thị có hướng thì các đường thẳng hay các cung cần có mũi tên chỉ

hướng. Hình 13.1 minh họa một số ví dụ về đồ thị.

Đồ thị thứ nhất trong hình 13.1 có các thành phố là các đỉnh, và các tuyến bay

là các cạnh. Trong đồ thị thứ hai, các nguyên tử hydro và carbon là các đỉnh, các

liên kết hóa học là các cạnh. Hình thứ ba là một đồ thị có hướng cho biết khả

năng truyền nhận dữ liệu trên mạng, các nút của mạng (A, B, , F) là các đỉnh và

các đường nối các nút là có hướng. Đôi khi cách chọn tập đỉnh và tập cạnh cho đồ

thị phụ thuộc vào giải thuật mà chúng ta dùng để giải bài toán, chẳng hạn bài

toán liên quan đến quy trình công việc, bài toán xếp thời khóa biểu,

Đồ thị được sử dụng để mô hình hóa rất nhiều dạng quá trình cũng như cấu

trúc khác nhau. Đồ thị có thể biểu diễn mạng giao thông giữa các thành phố, hoặc

các thành phần của một mạch in điện tử và các đường nối giữa chúng, hoặc cấu

trúc của một phân tử gồm các nguyên tử và các liên kết hóa học. Những người

dân trong một thành phố cũng có thể được biểu diễn bởi các đỉnh của đồ thị mà

các cạnh là các mối quan hệ giữa họ. Nhân viên trong một công ty có thể được

biểu diễn trong một đồ thị có hướng mà các cạnh có hướng cho biết mối quan hệ

của họ với những người quản lý. Những người này cũng có thể có những mối quan

hệ “cùng làm việc” biểu diễn bởi các cạnh không hướng trong một đồ thị vô

hướng.

 

Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị trang 1

Trang 1

Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị trang 2

Trang 2

Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị trang 3

Trang 3

Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị trang 4

Trang 4

Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị trang 5

Trang 5

Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị trang 6

Trang 6

Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị trang 7

Trang 7

Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị trang 8

Trang 8

Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị trang 9

Trang 9

Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị trang 10

Trang 10

Tải về để xem bản đầy đủ

pdf 26 trang duykhanh 4260
Bạn đang xem 10 trang mẫu của tài liệu "Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị", để tải tài liệu gốc về máy hãy click vào nút Download ở trên

Tóm tắt nội dung tài liệu: Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị

Giáo trình Cấu trúc dữ liệu - Chương 13: Đồ thị
ược tạo bởi tất cả các đỉnh và một số cạnh của đồ thị G. Chúng 
ta gọi những cây như vậy là cây phủ (spanning tree) của G. Cũng như phần trước, 
chúng ta có thể xem một mạng trên một đồ thị G như là một bản đồ các tuyến 
bay, với mỗi đỉnh biểu diễn một thành phố và tải trọng trên một cạnh là giá vé 
bay từ thành phố này sang thành phố kia. Một cây phủ của G biểu diễn một tập 
các đường bay cho phép các hành khách hoàn tất một chuyến du lịch qua khắp 
các thành phố. Dĩ nhiên rằng, hành khách cần phải thực hiện một số tuyến bay 
nào đó một vài lần mới hoàn tất được chuyến du lịch. Điều bất tiện này được bù 
đắp bởi chi phí thấp. Nếu chúng ta hình dung mạng trong hình 13.11 như một hệ 
thống điều khiển tập trung, thì đỉnh nguồn tương ứng với sân bay trung tâm, và 
các đường đi từ đỉnh này là những hành trình bay. Một điều quan trọng đối với 
một sân bay theo hệ thống điều khiển tập trung là giảm tối đa chi phí bằng cách 
chọn lựa một hệ thống các đường bay mà tổng chi phí nhỏ nhất. 
Định nghĩa: Một cây phủ tối tiểu (minimal spanning tree) của một mạng liên 
thông là cây phủ mà tổng các tải trọng trên các cạnh của nó là nhỏ nhất. 
 Mặc dù việc so sánh hai cây phủ trong hình 13.12 là không khó khăn, nhưng 
cũng khó mà biết được còn có cây phủ nào có trị nhỏ hơn nữa hay không. Bài toán 
của chúng ta là xây dựng một phương pháp xác định một cây phủ tối tiểu của một 
mạng liên thông. 
Giáo trình Cấu trúc dữ liệu và Giải thuật 358
 Chương 13 – Đồ thị 
 Hình 13.12 – Hai cây phủ trong một mạng 
13.6.2. Phương pháp 
 Chúng ta đã biết giải thuật tìm cây phủ trong một đồ thị liên thông, do giải 
thuật tìm đường ngắn nhất đã có. Chúng ta có thể thay đổi chút ít trong giải 
thuật tìm đường ngắn nhất để có được phương pháp tìm cây phủ tối tiểu mà R. C. 
Prim đã đưa ra lần đầu vào năm 1957. 
 Trước hết chúng ta chọn ra một đỉnh bắt đầu, gọi là nguồn, và trong khi tiến 
hành phương pháp, chúng ta nắm giữ một tập X các đỉnh mà đường đi từ chúng 
đến đỉnh nguồn thuộc về cây phủ tối tiểu mà chúng ta đã tìm thấy. Chúng ta cũng 
cần nắm một tập Y gồm các cạnh nối các đỉnh trong X mà thuộc cây đang được 
xây dựng. Như vậy, chúng ta có thể hình dung rằng các đỉnh trong X và các cạnh 
trong Y đã tạo ra một phần của cây mà chúng ta cần tìm, cây này sẽ lớn lên 
thành cây phủ tối tiểu cuối cùng. Lúc khởi đầu, đỉnh nguồn là đỉnh duy nhất 
trong X, và Y là tập rỗng. Tại mỗi bước, chúng ta thêm một đỉnh vào X: đỉnh này 
được chọn sao cho nó có một cạnh nối với một đỉnh nào đó đã có trong X có tải 
trọng nhỏ nhất, so với các tải trọng của tất cả các cạnh khác nối các đỉnh còn 
nằm ngoài X với các đỉnh đã có trong X. Cạnh tối tiểu này sẽ được đưa vào Y. 
 Việc chứng minh giải thuật Prim đem lại cây phủ tối tiểu không thực dễ dàng, 
chúng ta tạm hoãn việc này lại sau. Tuy nhiên, chúng ta có thể hiểu về việc chọn 
lựa một đỉnh mới để thêm vào X và một cạnh mới để thêm vào Y . Tiêu chí Prim 
cho chúng ta một cách dễ dàng nhất để thực hiện việc kết nối này, và như vậy, 
theo tiêu chí tham lam, chúng ta nên sử dụng nó. 
 Khi hiện thực giải thuật Prim, chúng ta cần nắm giữ một danh sách các đỉnh 
thuộc X như là các phần tử của một mảng kiểu bool. Cách nắm giữ các cạnh 
trong Y sẽ dễ dàng nếu như chúng ta bắt chước cách lưu trữ các cạnh trong một 
đồ thị. 
 Giáo trình Cấu trúc dữ liệu và Giải thuật 359
 Chương 13 – Đồ thị 
 Chúng ta sẽ cần đến một mảng phụ neighbor để biết thêm rằng, đối với mỗi 
 đỉnh ν, đỉnh nào trong X có cạnh đến ν có tải trọng nhỏ nhất. Các tải trọng nhỏ 
 nhất này cũng chứa trong mảng distance tương ứng. Nếu một đỉnh ν không có 
 một cạnh nào nối được với một đỉnh nào đó trong X, trị của phần tử tương ứng 
 của nó trong distance sẽ là infinity. 
 Hình 13.13 – Ví dụ về giải thuật Prim 
 Mảng neighbor được khởi tạo bằng cách gán neighbor[v] đến đỉnh nguồn 
cho tất cả các đỉnh ν, và distance được khởi tạo bằng cách gán distance[v] 
bởi tải trọng của cạnh nối từ đỉnh nguồn đến ν hoặc infinity nếu cạnh này 
 không tồn tại. 
 Để xác định đỉnh nào sẽ được thêm vào tập X tại mỗi bước, chúng ta chọn đỉnh 
 ν trong số các đỉnh chưa có trong X mà trị tương ứng trong mảng distance là 
 nhỏ nhất. Sau đó chúng ta cần cập nhật lại các mảng để phản ánh đúng sự thay 
 đổi mà chúng ta đã làm đối với tập X như sau: với mỗi w chưa có trong X, nếu có 
 Giáo trình Cấu trúc dữ liệu và Giải thuật 360
 Chương 13 – Đồ thị 
một cạnh nối ν và w, chúng ta xem thử tải trọng của cạnh này có nhỏ hơn 
distance[w] hay không, nếu quả thực như vậy thì distance[w] cần được cập 
nhật lại bằng trị của tải trọng này, và neighbor[w] sẽ là ν. 
 Lấy ví dụ, chúng ta hãy xem xét mạng trong hình (a) của hình 13.13. Trạng 
thái ban đầu trong hình (b): Tập X (các đỉnh được tô màu) chỉ gồm đỉnh nguồn 0, 
và đối với mỗi đỉnh w, đỉnh được lưu trong neighbor[w] được chỉ bởi các mũi tên 
từ w. Trị của distance[w] là tải trọng của cạnh tương ứng. Khoảng cách từ 
nguồn đến đỉnh 1 là một trong những trị nhỏ nhất, nên 1 được thêm vào X như 
hình (c), và trong các phần tử của các mảng neighbor và distance chỉ có các 
phần tử tương ứng với đỉnh 2 và đỉnh 5 là được cập nhật lại. Đỉnh kế tiếp gần với 
các đỉnh trong X nhất là đỉnh 2, nó được thêm vào X như hình (d), trong này 
cũng đã chỉ ra các trị của các mảng đã được cập nhật lại. Các bước cuối cùng được 
minh họa trong hình (e), (f) và (g). 
13.6.3. Hiện thực 
 Để hiện thực giải thuật Prim, chúng ta cần chọn một lớp để biểu diễn cho 
mạng. Sự tương tự của giải thuật này so với giải thuật tìm đường ngắn nhất trong 
phần trước giúp chúng ta quyết định thiết kế lớp Network dẫn xuất từ lớp 
Digraph. 
 template 
 class Network: public Digraph { 
 public: 
 Network(); 
 void read(); // Định nghĩa lại để nhập thông tin về mạng. 
 void make_empty(int size = 0); 
 void add_edge(Vertex v, Vertex w, Weight x); 
 void minimal_spanning(Vertex source, 
 Network &tree) const; 
 }; 
 Chúng ta sẽ viết lại phương thức nhập read để đảm bảo rằng tải trọng của 
cạnh (ν, w) luôn trùng với tải trọng của cạnh (w, ν), với mọi ν và w, vì đây là một 
mạng vô hướng. Phương thức mới make_empty(int size) tạo một mạng có 
size đỉnh và không có cạnh. Phương thức khác, add_edge, thêm một cạnh có 
một tải trọng cho trước vào mạng. Chúng ta cũng đã giả sử rằng lớp Weight đã có 
đầy đủ các toán tử so sánh. Ngoài ra, người sử dụng cần khai báo trị lớn nhất có 
thể của Weight gọi là infinity. Phương thức minimal_spanning mà chúng ta 
sẽ viết ở đây sẽ tìm cây phủ tối tiểu và trả về qua tham biến tree. Tuy phương 
thức chỉ có thể tìm cây phủ khi thực hiện trên một mạng liên thông, nó cũng có 
thể tìm một cây phủ cho một thành phần liên thông có chứa đỉnh source trong 
mạng. 
 Giáo trình Cấu trúc dữ liệu và Giải thuật 361
Chương 13 – Đồ thị 
template 
void Network::minimal_spanning(Vertex source, 
 Network &tree) const 
/* 
post: Xác định cây phủ tối tiểu trong thành phần liên thông có chứa đỉnh source của mạng. 
*/ 
{ 
 tree.make_empty(count); 
 bool component[graph_size]; // Các đỉnh trong tập X. 
 Vertex neighbor[graph_size];// Phần tử thứ i chứa đỉnh trước của nó sao cho khoảng 
 // cách giữa chúng nhỏ nhất so với các khoảng cách từ 
 // các đỉnh trước khác đã có trong tập X đến nó. 
 Weight distance[graph_size];// Các khoảng cách nhỏ nhất tương ứng với từng phần tử 
 // trong mảng neighbor trên. 
 Vertex w; 
 for (w = 0; w < count; w++) { 
 component[w] = false; 
 distance[w] = adjacency[source][w]; 
 neighbor[w] = source; 
 } 
 component[source] = true; // Tập X chỉ có duy nhất đỉnh nguồn. 
 for (int i = 1; i < count; i++) { 
 Vertex v; // Mỗi lần lặp bổ sung thêm một đỉnh vào tập X. 
 Weight min = infinity; 
 for (w = 0; w < count; w++) if (!component[w]) 
 if (distance[w] < min) { 
 v = w; 
 min = distance[w]; 
 } 
 if (min < infinity) { 
 component[v] = true; 
 tree.add_edge(v, neighbor[v], distance[v]); 
 for (w = 0; w < count; w++) if (!component[w]) 
 if (adjacency[v][w] < distance[w]) { 
 distance[w] = adjacency[v][w]; 
 neighbor[w] = v; 
 } 
 } 
 else break; // Xong một thành phần liên thông trong đồ thị không liên thông. 
 } 
} 
 Vòng lặp chính trong hàm trên thực hiện n-1 lần, với n là số đỉnh, và trong 
vòng lặp chính còn có hai vòng lặp khác, mỗi vòng lặp này thực hiện n-1 lần. Vậy 
các vòng lặp thực hiện (n-1)2 lần. Các lệnh bên ngoài vòng lặp chỉ hết O(n), nên 
thời gian chạy của hàm là O(n2). 
13.6.4. Kiểm tra giải thuật Prim 
 Chúng ta cần chứng minh rằng, đối với đồ thị liên thông G, cây phủ S sinh ra 
bởi giải thuật Prim phải có tổng tải trọng trên các cạnh nhỏ hơn so với bất kỳ 
Giáo trình Cấu trúc dữ liệu và Giải thuật 362
 Chương 13 – Đồ thị 
một cây phủ nào khác của G. Giải thuật Prim xác định một chuỗi các cạnh s1, s2, 
 ..., sn tạo ra cây phủ S. Như hình 13.14, s1 là cạnh thứ nhất được thêm vào tập Y 
 trong giải thuật Prim, s2 là cạnh thứ hai được thêm vào, và cứ thế. 
 Để chứng minh S là một cây phủ tối tiểu, chúng ta chứng minh rằng nếu m là 
 một số nguyên, 0 ≤ m ≤ n, thì sẽ có một cây phủ tối tiểu chứa cạnh si với i ≤ m. 
Chúng ta sẽ chứng minh quy nạp trên m. Trường hợp cơ bản, khi m = 0, rõ ràng 
là đúng, vì bất kỳ cây phủ tối tiểu nào cũng đều phải chứa một tập rỗng các cạnh. 
Ngoài ra, khi chúng ta hoàn tất việc quy nạp, trường hợp cuối cùng với m = n chỉ 
ra rằng có một cây phủ tối tiểu chứa tất cả các cạnh của S, và chính là S. (Lưu ý 
rằng nếu thêm bất kỳ một cạnh nào vào một cây phủ thì cũng tạo ra một chu 
trình, nên bất kỳ cây phủ nào chứa mọi cạnh của S đều phải chính là S). Nói cách 
khác, khi chúng ta hoàn tất việc quy nạp, chúng ta đã chứng minh được rằng S 
chính là cây phủ tối tiểu. 
 Như vậy chúng ta cần trình bày các bước quy nạp bằng cách chứng minh rằng 
nếu m < n và T là một cây phủ tối tiểu chứa các cạnh s i với i ≤ m, thì phải có một 
 cây phủ tối tiểu U cũng chứa các cạnh trên và thêm một cạnh sm+1. Nếu sm+1 đã có 
 trong T, chúng ta có thể đơn giản cho U = T, như vậy chúng ta có thể giả sử rằng 
sm+1 không là một cạnh trong T. Chúng ta hãy xem hình (b) của hình 13.14. 
 Hình 13.14 – Kiểm tra giải thuật Prim 
 Chúng ta hãy gọi X là tập các đỉnh trong S thuộc các cạnh s1, s2, ..., sm và R là 
 tập các đỉnh còn lại trong S. Trong giải thuật Prim, cạnh sm+1 nối một đỉnh trong 
 Giáo trình Cấu trúc dữ liệu và Giải thuật 363
 Chương 13 – Đồ thị 
 X với một đỉnh trong R, và sm+1 là một trong các cạnh có tải trọng nhỏ nhất nối 
 giữa hai tập này. Chúng ta hãy xét ảnh hưởng của việc thêm cạnh sm+1 này vào T, 
 như minh họa trong hình (c). Việc thêm vào này phải tạo ra một chu trình C, do 
 mạng liên thông T rõ ràng là có chứa một đường đi có nhiều cạnh nối hai đầu của 
 cạnh sm+1. Chu trình C phải có chứa một cạnh t ≠ sm+1 nối tập X với tập R, do nếu 
 chúng ta di dọc theo đường đi khép kín C chúng ta phải đi vào tập X một số lần 
 bằng với số lần chúng ta đi ra khỏi nó. Chúng ta hãy xem hình (d). Giải thuật 
 Prim bảo đảm rằng tải trọng của sm+1 nhỏ hơn hoặc bằng tải trọng của t. Do đó, 
 cây phủ mới U trong hình (e), có được từ T bằng cách loại đi t và thêm vào sm+1, 
sẽ có tổng tải trọng không lớn hơn tải trọng của T. Như vậy chúng ta đã có được 
U là một cây phủ tối tiểu của G, mà U chứa các cạnh s1, s2, ..., sm, sm+1. Điều này 
 hoàn tất được quá trình quy nạp của chúng ta. 
 13.7. Sử dụng đồ thị như là cấu trúc dữ liệu 
 Trong chương này, chúng ta đã tìm hiểu chỉ một ít ứng dụng của đồ thị, nhưng 
 chúng ta đã bắt đầu thâm nhập vào những vấn đề sâu sắc của các giải thuật về đồ 
 thị. Nhiều giải thuật trong số đó, đồ thị xuất hiện như các cấu trúc toán học và đã 
 nắm bắt được các đặc trưng thiết yếu của bài toán, thay vì chỉ là những công cụ 
 tính toán cho ra được những lời giải của chúng. Lưu ý rằng trong chương này 
 chúng ta đã nói về các đồ thị như là các cấu trúc toán học, chứ không như các cấu 
 trúc dữ liệu, do chúng ta đã sử dụng chúng để đặc tả các vấn đề trong toán học, 
 và để viết các giải thuật, chúng ta đã hiện thực các đồ thị trong các cấu trúc dữ 
 liệu như danh sách hoặc bảng. Tuy vậy, rõ ràng là đồ thị tự bản thân nó có thể 
 được xem như các cấu trúc dữ liệu - các cấu trúc dữ liệu mà có chứa các mối quan 
 hệ giữa các dữ liệu phức tạp hơn những gì đã được mô tả trong một danh sách 
 hoặc một cây. Do tính tổng quát và mềm dẻo, đồ thị là cấu trúc dữ liệu rất hiệu 
 quả và đã tỏ rõ những giá trị của nó trong những ứng dụng cấp tiến như thiết kế 
 hệ quản trị cơ sở dữ liệu chẳng hạn. Tất nhiên, một công cụ mạnh như vậy càng 
nên được sử dụng mọi khi cần thiết, nhưng việc sử dụng nó cần phải được kết hợp 
với việc xem xét một cách cẩn thận để sức mạnh của nó không làm cho cho chúng 
ta bị rối. Cách an toàn nhất trong việc sử dụng một công cụ mạnh là dựa trên sự 
chính quy; nghĩa là, chúng ta chỉ sử dụng công cụ mạnh trong những phương 
pháp đã được định nghĩa một cách cẩn thận và dễ hiểu. Do tính tổng quát của đồ 
thị, việc tuân thủ nguyên tắc vừa nêu ra trong việc sử dụng nó không phải luôn dễ 
dàng. 
 Giáo trình Cấu trúc dữ liệu và Giải thuật 364

File đính kèm:

  • pdfgiao_trinh_cau_trsuc_du_lieu_chuong_13_do_thi.pdf