Cong ty Cong Nghe Tin hoc Nha truong http://www.vnschool.net
SẮP XẾP TOPO - MỘT BÀI TOÁN CỔ ĐIỂN
12/02/2008

1. Sắp xếp topo:

Sắp xếp topo (topological sorting) là một trong những bài toán có tính ứng dụng cao cả trong Tin học lẫn Toán học và đời sống thường ngày. Đây là quá trình sắp xếp một dãy các phần tử sao cho thứ tự mới vẫn đảm bảo được thứ tự cục bộ (một cách nôm na có nghĩa là thứ tự được xác định đối với một vài cặp phần tử chứ không phải là tất cả các phần tử) vốn có của chúng. Một số ví dụ sẽ minh hoạ điều này


Ví dụ 1:

i) Một đề án có thể được chia thành nhiều nhiều công việc nhỏ khác nhau, tuy nhiên trong đó có những công việc chỉ có thể thực hiện được sau khi một số công việc khác đã hoàn thành. Nếu việc v buộc phải hoàn thành trước việc w (khi ấy việc w mới có thể thực hiện), ta kí hiệu v w. Sắp xếp topo trong trường hợp này nghĩa là đưa ra một thứ tự thực hiện các công việc hợp lý để có thể hoàn thành đề án.

ii) Trong một số trường ĐH ở VN hiện nay, có những học phần gọi là học phần tiên quyết mà sinh viên buộc phải hoàn thành trước khi học các học phần khác. Nếu học phần v là học phần tiên quyết đối với học phần w, ta viết v w. Sắp xếp topo ở đây có nghĩa là đưa ra thứ tự học các học phần sao cho mọi học phần phải được học sau các học phần tiên quyết của nó.

Một thứ tự cục bộ trên tập S thực chất là một quan hệ giữa các phần tử trên tập S, và khi đó S được gọi là tập được sắp xếp cục bộ (1). Thông thường thứ tự cục bộ được kí hiệu là (2), và phải thoả các tính chất (xem như tiên đề) sau với mọi x, y, z S:

i) Tính phản xạ: x ≤ x,
ii) Tính phản xứng: nếu x ≤ y và y ≤ x thì x=y, và
iii) Tính bắc cầu: nếu x ≤ y, y ≤ z thì x ≤ z.

Ví dụ 2:

i) Quan hệ chia hết trên Z+ là một thứ tự cục bộ.
ii) Quan hệ “nhỏ hơn hay bằng” (≤) trên tập các số nguyên là một thứ tự cục bộ.
iii) Quan hệ “nhỏ hơn” (<) trên Z không phải là thứ tự cục bộ, vì nó phản xứng, bắc cầu nhưng không phản xạ. (bạn đọc có thể tự kiểm chứng một cách dễ dàng).

Trong một tập được sắp xếp cục bộ, kí hiệu x y cũng được dùng để chỉ x ≤ y mà x ≠ y.

Một cách hiển nhiên, ta giả sử tập S cần sắp xếp topo là tập hữu hạn. Do đó một thứ tự cục bộ có thể được biểu diễn bởi một đồ thị có hướng mà các đỉnh của đồ thị biểu diễn các phần tử của S, đồng thời các cạnh có hướng biểu diễn thứ tự giữa các phần tử.

Để tiện theo dõi, giả sử có 10 công việc cần thực hiện (đây chính là tập S) được đánh số từ 1 đến 10, với thứ tự cục bộ như sau:

Khi đó, đồ thị biểu diễn của tập S có dạng như hình 1:

Sắp xếp topo phải xây dựng được thứ tự toàn bộ từ thứ tự cục bộ đã cho (3). Một cách trực quan, đó là quá trình vẽ lại đồ thị ở hình 1 thành một đồ thị mới sao cho tất cả các đỉnh đều nằm trên một hàng và tất cả các cạnh đều hướng sang phải (xem hình 2)

Từ đồ thị trong hình 2, dễ dàng đưa ra một thứ tự thích hợp để thực hiện các công việc mà vẫn đảm bảo thứ tự cục bộ giữa chúng. Tất nhiên, vấn đề quan trọng là làm sao để “vẽ lại” đồ thị ở hình 1 thành đồ thị trong hình 2. Việc này khá đơn giản. Ta bắt đầu từ đỉnh mà không có cạnh nào lấy nó làm đỉnh cuối (tức là bậc vào của nó bằng 0). Luôn tồn tại ít nhất một đỉnh như thế(4). Đặt đỉnh này vào đầu danh sách mới (đồ thị mới) đồng thời loại bỏ nó khỏi đồ thị cũ. Đồ thị cũ vẫn còn là một tập được sắp cục bộ nên lặp lại quá trình cho đến khi hết tất cả các đỉnh. Do giả sử S là tập hữu hạn nên công việc sẽ kết thúc sau một số hữu hạn bước.

Mọi việc sẽ rõ ràng hơn khi ta nghiên cứu việc tổ chức dữ liệu và cài đặt thuật toán. Bài viết này sử dụng ngôn ngữ C++ để cài đặt, nhưng đương nhiên các đoạn mã đều có thể chuyển sang các ngôn ngữ khác một cách dễ dàng.

2. Cài đặt:

Để dễ trình bày, ta xem mỗi phần tử trong S là một công việc (như trong ví dụ 1i, mặc dù thuật toán trình bày ở đây có thể được áp dụng cho bất kì tập hợp nào cần sắp xếp topo). Nhận xét rằng mỗi một công việc trong S cần quản lý 3 thông tin, gồm: số hiệu công việc, số lượng công việc cần được thực hiện trước công việc ấy, và tập các công việc có thể thực hiện sau công việc ấy. Những thông tin này được tổ chức trong một cấu trúc tên là leader (xem hình 3). Để quản lí S, ta dùng một danh sách liên kết các leader, do đó mỗi leader sẽ có thêm một trường next để chỉ tới phần tử kế tiếp nó trong danh sách. Ngoài ra để quản lí các công việc có thể được thực hiện sau mỗi leader, ta dùng một danh sách liên kết các trailer (hình 4). Mỗi trailer đại diện cho một leader có thể thực hiện. Sau đây là mô tả cụ thể:

Trong leader, trường key là số hiệu công việc mà ta sẽ giả sử là được đánh số thứ tự theo kiểu nguyên (nhưng không nhất thiết liên tục từ 1 đến n), trường count dùng để chỉ số lượng công việc phải được hoàn thành trước khi thực hiện công việc này. Nếu count = 0 thì có nghĩa là công việc này có thể được thực hiện ngay mà không cần phải chờ. Chẳng hạn trong đồ thị ở hình 1, trường count của nút 4 có giá trị là 2, của nút 7 có giá trị là 0... Trường next của leader có kiểu là một con trỏ trỏ đến leader kế tiếp trong danh sách S. “Kế tiếp” ở đây chỉ đơn thuần mang nghĩa là phần tử tiếp theo trong S, để quản lí S một cách dễ dàng. Tiếp theo, trường trail có kiểu là con trỏ trỏ đến 1 danh sách liên kết đơn các trailer. Mỗi một trailer trong danh sách này đại diện cho một công việc phải thực hiện sau công việc này. Chẳng hạn trong hình 1, danh sách liên kết trail của nút 9 có 2 phần tử trailer, của nút 10 là danh sách rỗng (không có phần tử trailer nào).

Trong trailer, trường id có kiểu con trỏ trỏ đến 1 leader, đó chính là công việc tiếp theo mà trailer này đại diện. Trường next trong trailer trỏ đến phần tử trailer tiếp theo trong danh sách.

Chẳng hạn trong hình 1, leader số 1 (có trường key = 1) sẽ có 2 trailer, một trailer trỏ đến leader số 2, trailer còn lại trỏ đến leader số 3.

Sau đây là khai báo của các cấu trúc này:

Công việc sắp xếp topo sẽ trải qua hai giai đoạn. Đầu tiên là nhập dữ liệu từ thứ tự bộ phận của tập S. Việc này được thực hiện bằng cách nhập các cặp giá trị x, y với x, y là số hiệu công việc mà công việc x phải thực hiện trước công việc y (x y). Mỗi lần nhập một cặp x, y ta cũng đưa ngay vào danh sách liên kết S, đồng thời thiết lập các trailer phù hợp.

Ví dụ, với thứ tự bộ phận như lúc đầu, sau khi nhập x=1, y=2 ta được như hình 5:

Ở hình 6 là kết quả sau khi nhập tiếp x = 2, y = 4. Và cuối cùng hình 7 là kết quả sau khi nhập toàn bộ tất cả các cặp x, y.

Ở đây do thường xuyên phải duyệt, tìm kiếm trên toàn bộ danh sách nên tail được sử dụng như lính canh nhằm giảm số phép so sánh. Việc xây dựng danh sách S khá đơn giản. Giả sử dữ liệu cần sắp xếp chứa trong file topo.inp. File này có n dòng, mỗi dòng chứa 2 giá trị x, y cách nhau bởi khoảng trắng. Việc đọc dữ liệu sẽ được thực hiện thông qua hàm readData có mã giả như sau:

Danh sách S được trỏ bởi con trỏ head, phần tử lính canh trỏ bởi con trỏ tail, z là số lượng các leader trong toàn bộ danh sách. Việc thêm 2 leader có key là x, y được thực hiện bởi hàm addList một cách khá đơn giản. Do dùng tail làm lính canh nên chỉ cần duyệt trên danh sách và tạo các leader mới nếu các phần tử này chưa có trong danh sách. Để rõ hơn, xin xem code đầy đủ ở phần sau.

Công việc thứ 2 là từ danh sách liên kết đã có (hình 7), đưa ra thứ tự toàn bộ khải dĩ cho các công việc, tức là tiến hành sắp xếp topo trên danh sách. Để làm việc này, cần duyệt qua tất cả các leader, tách các leader có trường count = 0 ra thành 1 danh sách riêng (đây là những công việc có thể được thực hiện trước tiên mà không cần phải chờ), sau đó duyệt qua các trailer tương ứng để cập nhật lại trường count cho các leader trong danh sách trail. Cụ thể mã giả của hàm sắp xếp topo như sau:

Trong hàm topoSort, cứ mỗi khi in ra 1 key (ngụ ý rằng sẽ thực hiện việc có khoá key) thì hàm lại giảm z đi 1, mang nghĩa số công việc còn lại cần thực hiện trong S đã giảm đi 1. Tuy nhiên, ở cuối hàm này, nếu z không thể giảm về 0 (tức là sau khi thực hiện cả 2 vòng while mà vẫn chưa hết danh sách S) thì có nghĩa rằng không thể đưa ra một thứ tự toàn bộ để thực hiện các công việc.

Mọi chuyện sẽ trở nên rõ ràng khi xem xét mã lệnh đầy đủ ở phía dưới. Đây là toàn văn chương trình viết bằng C++, biên dịch trong môi trường VC++ 2005 (windows console application). Ngoài ra tác giả cũng đã chuyển chương trình này sang ngôn ngữ Pascal và VB mà độc giả quan tâm có thể download cả 3 chương trình ở http://www.mediafire.com/?ect33zgmzon. Mọi ý kiến góp ý xin gửi về địa chỉ mail của tác giả.

(1) Những vấn đề chi tiết về quan hệ, thứ tự... nằm ngoài phạm vi của bài viết này. Bạn đọc quan tâm có thể tham khảo trong những cuốn sách về Toán rời rạc, chẳng hạn “Discrete Mathematics and its applications” của Kenneth H. Rosen (đã có bản dịch tiếng Việt là “Toán học rời rạc ứng dụng trong Tin học” của Bùi Xuân Toại), hay quyển “Toán rời rạc” của GS. Nguyễn Hữu Anh...

(2) lưu ý rằng kí hiệu ≤ trong ngữ cảnh này được dùng để chỉ chung cho thứ tự cục bộ bất kì, chứ không phải là chỉ riêng quan hệ “nhỏ hơn hoặc bằng”.

(3) Một cách đầy đủ, sắp xếp topo trên một tập được định nghĩa là “quá trình thiết lập một thứ tự toàn cục từ thứ tự cục bộ của nó”.

(4) Việc chứng minh khá đơn giản. Trước hết nhận xét rằng đây là đồ thị cho một tập được sắp xếp cục bộ nên tính chất ii) và iii) của thứ tự cục bộ đảm bảo rằng đồ thị không có chu trình nào. Suy ra tồn tại ít nhất một đỉnh mà bậc vào của nó bằng 0.

Địa chỉ liên hệ: Phạm Hoài Vũ, 571/1/17A Phạm Văn Bạch, P. 15, Q. Tân Bình, Tp. HCM

Email: classical_boy1988@yahoo.com
URL của bài viết này::http://www.vnschool.net/modules.php?name=News&file=article&sid=1808
© Cong ty Cong Nghe Tin hoc Nha truong contact: sales@schoolnet.vn