Sắp xếp TopoDAG trong Đồ thị định hướng không chứa chu trình

MỤC LỤC

ĐỒ THỊ ĐỊNH HƯỚNG KHễNG Cể CHU TRèNH VÀ SẮP XẾP TOPO

DAG là trường hợp riêng của đồ thị định hướng, nhưng tổng quát hơn khái niệm cây. Chẳng hạn, quan hệ thứ tự bộ phận trên một tập A có thể biểu diễn bởi DAG, trong đó mỗi phần tử của A là một đỉnh của đồ thị, và nếu a < b thì trong đồ thị sẽ có cung từ đỉnh a đến b. Do tính chất của quan hệ thứ tự bộ phận, đồ thị này không có chu trình, do đó nó là một DAG.

Trong quá trình thực hiện, một nhiệm vụ có thể chỉ được bắt đầu thực hiện khi một số nhiệm vụ khác đã hoàn thành (dễ thấy điều này ở các đề án thi công). Nếu nhiệm vụ A cần phải được hoàn thành trước khi nhiệm vụ B bắt đầu thực hiện, thì trong đồ thị sẽ có cung đi từ đỉnh A đến đỉnh B. Giả sử tại mỗi thời điểm ta chỉ thực hiện được một nhiệm vụ, làm xong một nhiệm vụ mới có thể bắt đầu làm nhiệm vụ khác.

Như vậy ta phải sắp xếp các nhiệm vụ để thực hiện sao cho thoả mãn các đòi hỏi về thời gian giữa các nhiệm vụ. Cho G = (V,E) là một DAG, ta cần sắp xếp các đỉnh của đồ thị thành một danh sách (chúng ta sẽ gọi là danh sách topo), sao cho nếu có cung (u,v) thì u cần phải đứng trước v trong danh sách đó. Trong mục 18.5.2, chúng ta đã chỉ ra rằng, có thể sử dụng kỹ thuật đi qua đồ thị theo độ sâu để phát hiện ra đồ thị là có chu trình hay không.

Sau đây ta sẽ sử dụng kỹ thuật đi qua đồ thị theo độ sâu để sinh ra một danh sách topo của đồ thị định hướng không có chu trình. Nhớ lại rằng đồ thị không có chu trình, thì trong rừng cây được tạo thành khi đi qua đồ thị theo độ sâu chỉ có ba loại cung: cung cây, cung tiến và cung xiên. Như vậy, S[u] là cách đánh số các đỉnh trong danh sách topo theo thứ tự ngược lại, Từ đó ta dễ dàng đưa ra thuật toán sắp xếp topo.

Thuật toán sắp xếp topo (TopoSort) sau đây sẽ sử dụng hàm đệ quy TPS(u), hàm này thực chất là hàm tìm kiếm theo độ sâu DFS(u), chỉ khác là thay cho việc đánh số thứ tự sau S[u], ta ghi u vào đầu danh sách topo.

ĐƯỜNG ĐI NGẮN NHẤT

Đường đi ngắn nhất từ một đỉnh nguồn

Thuật toán được trình bày sau đây là thuật toán Dijkstra (mang tên E. Dijkstra, người phát minh ra thuật toán). Ta xác định đường đi ngắn nhất từ đỉnh nguồn s tới các đỉnh còn lại qua các bước, mỗi bước ta xác định đường đi ngắn nhất từ nguồn tới một đỉnh. Tại mỗi bước ta sẽ chọn một đỉnh u không thuộc S mà D[u] nhỏ nhất và thêm u vào S, ta xem D[u] là độ dài đường đi ngắn nhất từ nguồn tới u (sau này ta sẽ chứng minh D[u] đúng là độ dài đường đi ngắn nhất từ nguồn tới u).

Bước trên đây được lặp lại cho tới khi S gồm tất cả các đỉnh của đồ thị, và lúc đó mảng D[u] sẽ lưu độ dài đường đi ngắn nhất từ nguồn tới u, với mọi u∈V. Trong thuật toán trên đây, chúng ta mới sử dụng mảng D để ghi lại độ dài đường đi ngắn nhất từ nguồn tới các đỉnh khác. Muốn lưu lại vết của đường đi, ta sử dụng thêm mảng P, trong đó P[v] = u nếu cung (u,v) nằm trên đường đi đặc biệt.

Giả sử rằng tại một thời điểm nào đó ta đã có D[a] là độ dài đường đi ngắn nhất từ nguồn tới a, với mọi đỉnh a ∈ S, và u là đỉnh được chọn bởi lệnh (2) trong thuật toán để bổ sung vào S. Từ đó bằng quy nạp, dễ dàng chứng minh được nhận xét sau: Nếu a là đỉnh bất kỳ trong S và b là đỉnh bất kỳ ngoài S thì D[b] <=. Giả sử ta có một đường đi bất kỳ từ nguồn s tới u, độ dài của nó được ký hiệu là d(s,u).

Như vậy ta đã chứng minh được D[u] nhỏ hơn hoặc bằng độ dài d(s,u) của đường đi bất kỳ từ nguồn s tới u. Trong thuật toán Dijkstra, tại mỗi bước ta cần chọn một đỉnh u không nằm trong S mà D[u] là nhỏ nhất (lệnh (2)), và sau đó với các đỉnh v còn lại không nằm trong S, D[v] có thể bị giảm đi bởi lệnh (3). Phép toán DecreaseKey chỉ cần thời gian O(log(|V|) khi ta cài đặt hàng ưu tiên P bởi cây thứ tự bộ phận (xem 12.2).

Tổng kết lại, thời gian chạy của thuật toán Dijkstra, nếu ta sử dụng hàng ưu tiên được cài đặt bởi cây thứ tự bộ phận, là O(|V|log|V| + |E|log|V|).

Đường đi ngắn nhất giữa mọi cặp đỉnh

Trong mỗi lần lặp đó, nhiều nhất là một phép toán DecreaseKey (lệnh(6)) được thực hiện. Gọi Ak(i,j) là độ dài đường đi ngắn nhất từ đỉnh i tới đỉnh j nhưng chỉ đi qua các đỉnh trong tập Sk. Trước hết ta có nhận xét rằng, nếu đỉnh k nằm trên đường đi ngắn nhất từ đỉnh i tới đỉnh j, thì đoạn đường từ i tới k và đoạn đường từ k tới j phải là đường đi ngắn nhất từ i tới k và từ k tới j tương ứng.

Muốn lưu lại vết của đường đi ngắn nhất giữa mọi cặp đỉnh, trong thuật toán Floyd ta sử dụng thêm mảng P, trong đó P[i][j] lưu đỉnh k nếu đường đi ngắn nhất từ i tới j tìm được bởi thuật toán Floyd đi qua đỉnh k.

CÂY BAO TRÙM NGẮN NHẤT

Thuật toán Prim

Tiếp tục phát triển cây T cho tới khi U = V, ta nhận được T là cây bao trùm. Để thấy thuật toán Prim làm việc như thế nào, ta hãy xét đồ thị trong hình 18.8a. Chúng ta sẽ chứng minh rằng, cây bao trùm T được xây dựng bởi thuật toán Prim là cây bao trùm ngắn nhất.

Giả sử T’ là một cây bao trùm ngắn nhất và T’ không chứa cạnh (u,v) của cây T. Chúng ta sẽ biến đổi cây T’ thành cây T1 chứa cạnh (u,v) sao cho T1 cũng là cây bao trùm ngắn nhất. Bây giờ chúng ta xét xem cần phải cài đặt tập V - U như thế nào để có thể thực hiện hiệu quả hành động (1) trong thuật toán Prim.

Sau đó trong mỗi bước lặp của thuật toán Prim, ta loại đỉnh v có khoá nhỏ nhất khởi hàng ưu tiên và thêm cạnh (near[v],v) vào tập T. Sau khi đỉnh v được bổ xung vào U thì với các đỉnh còn lại w trong hàng ưu tiên, khoá của nó có thể giảm, cần phải cập nhật. Chúng ta có thể mô tả thuật toán Prim một cách cụ thể hơn như sau.

Chúng ta có nhận xét rằng, dòng điều khiển của thuật toán Prim trên là giống hệt thuật toán Dijkstra, do đó ta có thể kết luận rằng, nếu sử dụng hàng ưu tiên trên được cài đặt bởi cây thứ tự bộ phận, thì thời gian chạy của thuật toán Prim cũng là O(|V|log|V| + |E|log|V|).

Hình 18.9. Phát triển cây T theo thuật toán Prim.
Hình 18.9. Phát triển cây T theo thuật toán Prim.

Thuật toán Kruskal

Thời gian chạy của thuật toán này phụ thuộc vào cách cài đặt họ các tập con không cắt nhau bởi các cây hướng lên (up-tree) (xem mục 13.3). Ta cần chứng minh rằng, cây T được xây dựng bởi thuật toán Kruskal là cây bao trùm ngắn nhất. Nếu cây T còn có cạnh không thuộc cây T1, thì bằng cách trên ta lại biến đổi T1 thành T2.

Áp dụng thuật toán đi qua đồ thị theo bề rộng và theo độ sâu cho đồ thị này khi xuất phát từ đỉnh a, đưa ra thứ tự các đỉnh được thăm cho mỗi cách duyệt. Sử dụng kỹ thuật đi qua đồ thị theo bề rộng, hãy đưa ra thuật toán để trả lời cho câu hỏi: đồ thị có liên thông không, nếu không thì đồ thị có mấy thành phần liên thôn và mỗi thành phần gồm các đỉnh nào?. Sử dụng kỹ thuật đi qua đồ thị theo độ sâu, hãy đưa ra thuật toán để cho biết đỉnh w có đạt tới từ đỉnh v không, và nếu có thì cho biết đường đi ngắn nhất từ v tới w.

Vẽ ra cây được tạo thành và cho biết cung nào là cung cây, cung tiến, cung ngược, cung xiên?. Hãy đưa vào hàm DFS( ) các lệnh để gắn nhãn cho các cung (mỗi cung có thể là cung cây, cung tiến, cung ngược hoặc cung xiên). Sử dụng kỹ thuật đi qua đồ thị theo độ sâu, hãy viết thuật toán để cho biết đồ thị có chu trình không, nếu có thì cần cho biết đó là các chu trình nào?.

Hãy đưa ra thuật toán tìm đường đi ngắn nhấy từ tất cả các đỉnh khác tới đỉnh đích v. Thuật toán tìm đừơng đi ngắn nhất Dijkstra chỉ áp dụng cho đồ thị có trọng số không âm. Hãy đưa ra một đồ thị có chứa cung với trọng số âm, nhưng không chứa chu trình âm mà thuật toán Dijkstra cho ra kết quả sai.

11.Giả sử ta cần tìm đường đi dài nhất từ một đỉnh nguồn tới các đỉnh còn lại của đồ thị.