Tải bản đầy đủ (.pdf) (74 trang)

Ứng dụng lý thuyết đồ thị giải một số bài toán trong thực tế

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (2.07 MB, 74 trang )

MC LC
Lời mở đầu

6

Chương 1: Cơ sở lý thuyết ........................

8

1.1 Một số khái niệm cơ bản .....................................

8

1.1.1 Khái niệm đồ thị................................

8

1.1.2 Các loại đồ thị ........................................

8

1.1.3 Biểu diễn đồ thị..................................

8

1.2 đồ thị hai phía .................................................................................

10

1.2.1 Định nghĩa ........................................................................


10

1.2.2 ứng dụng...............

10

Chuơng 2: bài toán tìm bộ ghép cực đại trên đồ thị
và các thuật toán .

11

2.1 bài toán tìm bộ ghép cực đại trên đồ thị hai phía.

11

2.1.1 Bài toán ghép đôi không trọng và các khái niệm ..

11

2.1.2 Thuật toán đường mở

12

2.2 Bài toán tìm bộ ghép cực đại với tổng trọng số cực đại hoặc cực
tiểu trên đồ thị hai phía ..............................................................

12

2.2.1 Bài toán phân công ....


12

2.2.2 Thuật toán tìm cặp ghép với tổng trọng số trên các cạnh
là lớn

nhất hoặc nhỏ nhất .

13

2.2.3 Thuật toán Hung-ga-ri ......

14

2.2.4 Phương pháp đối ngẫu Kuhn-Munkres .............

16

2.2.5 Nâng cấp ...........................................................................

18

2.3 bài toán tìm bộ ghép cực đại trên đồ thị tổng quát ..........................

20

2.3.1 Các khái niệm

20

2.3.2 Thuật toán Edmonds .


21

2.3.3 Thuật toán Lawler .....

24

Chương 3: Giới thiệu ngôn ngữ delphi .

3

27


3.1 Khái quát về ngôn ngữ Delphi

27

3.1.1 Delphi l gì?.......................................................................

27

3.1.2 Cấu trúc chương trình Delphi v Unit

29

3.2 Form và các thành phần giao diện...

30


3.2.1 Xây dựng ứng dụng từ những thnh phần công cụ VCL

30

3.2.2 Form..

31

3.2.3 Các thnh phần điều khiển của Windows..

32

3.3 Ngôn ngữ Object Pascal..

36

3.3.1. Các kiểu dữ liệu đơn giản.

36

3.3.2 Các kiểu dữ liệu có cấu trúc .

38

3.3.3 Các câu lệnh cấu trúc.

39

3.4 Lập trình ứng dụng cơ sở dữ liệu.


41

3.4.1 Các cách kết nối với cơ sở dữ liệu.

41

3.4.2 Hạt nhân BDE...

43

3.4.3 Giới thiệu thnh phần VCL Component dùng phát triển
dụng cơ sở dữ liệu.

43

3.4.4 Sử dụng v truy vấn bảng dữ liệu...

44

Chương 4: các bài toán ứng dụng

46

4.1 Bài toán điều hành Taxi...

46

4.1.1 Phát biểu bài toán

46


4.1.2 Phân tích bài toán .............................................................

46

4.1.3 Chương trình ....................................................................

49

4..2 Bài toán xếp lớp học theo học chế tín chỉ...

62

4.2.1 Tìm hiểu về mô hình đào tạo theo học chế tín chỉ.............

62

4.2.2 Phát biểu bài toán ..............................................................

62

4.2.3 Phân tích bài toán...

63

4..2.4 Chương trình ..............................................................................

66

ứng


4


KÕt luËn ...............................................................................................

75

Tµi liÖu tham kh¶o ……………………………………………...

76

5


Lời mở đầu

Lý thuyết đồ thị là ngành khoa học được phát triển từ rất lâu nhưng lại có
nhiều ứng dụng hiện đại. Những ý tưởng cơ bản của lý thuyết đồ thị được nhà
toán học Thuỵ Sỹ tên là Leonhard Euler đưa ra từ thế kỷ 18. Ông đã dùng lý
thuyết đồ thị để giải quyết bài toán cầu Konigsberg nổi tiếng.
Đồ thị cũng được dùng để giải nhiều bài toán thuộc những lĩnh vực rất
khác nhau như: người ta có thể dùng đồ thị để biểu diễn sự cạnh tranh các loài
trong môi trường sinh thái, dùng đồ thị ai có ảnh hưởng lên ai trong một tổ chức
nào đó và cũng có thể dùng đồ thị để biểu diễn các kết cục của cuộc thi đấu thể
thao, hay cũng có thể dùng đồ thị để giải các bài toán như bài toán tính số các tổ
hợp khác nhau của các chuyến xe giữa hai thành phố trong một mạng giao thông,
bài toán đi tham quan tất cả các phố của một thành phố sao cho mỗi phố đi qua
đúng một lần, hay bài toán tìm số các màu cần thiết để tô các vùng khác nhau của
một bản đồ... Đồ thị với các trọng số được gán cho các cạnh của nó có thể dùng

để giải các bài toán như bài toán tìm đường đi ngắn nhất giữa hai thành phố trong
một mạng giao thông, bài toán phân công lao động sao cho tổng lợi nhuận thu
được là lớn nhất...
Chính vì đồ thị có thể được sử dụng để giải quyết nhiều bài toán thuộc
nhiều lĩnh vực khác nhau một cách dễ dàng và phổ biến như vậy nên đồ thị nắm
giữ một vai trò hết sức quan trọng trong cuộc sống, đặc biệt là trong lĩnh vực
Công nghệ Thông tin, dựa vào đồ thị và các thuật toán trên đồ thị người ta có thể
xây dựng nên các phần mềm hữu ích phục vụ cho các lĩnh vực khác có được kết
quả bài toán cần giải quyết một cách nhanh chóng và chính xác.
Hiểu được tầm quan trọng của đồ thị trong ngành học mà mình theo đuổi
và say mê, được sự quan tâm, tận tình chỉ bảo, động viên của cô giáo Ths
Trương Hà Hải, em đã chọn nghiên cứu đề tài ứng dụng lý thuyết đồ thị giải
một số bài toán trong thực tế với mong muốn sau khi hoàn thành đề tài này em
sẽ khám phá được nhiều hơn các ứng dụng của đồ thị trong thực tế và một số
thuật toán đặc biệt hiệu quả ứng dụng trên đồ thị để giải quyết các bài toán phục
vụ nhu cầu thực tế.

6


Em xin gửi lời cảm ơn chân thành tới cô giáo Ths Trương Hà Hải cùng
các thầy giáo, cô giáo khác đã tận tình chỉ bảo để em hoàn thành đề tài này. Em
cũng xin gửi lời cảm ơn tới các bạn sinh viên lớp K1B đã có những ý kiến đóng
góp để chương trình của em được hoàn thiện hơn.
Mặc dù đã hết sức cố gắng nhưng chắc chắn đề tài của em không tránh
khỏi những thiếu sót. Em rất mong nhận được sự góp ý của các thầy cô giáo và
các bạn để đề tài của em được hoàn thiện hơn.
Em xin chân thành cảm ơn !

Thái Nguyên, ngày 21 tháng 03 năm 2006


7


Chương 1
Cơ sở lý thuyết
1.1 Một số khái niệm cơ bản
1.1.1 Khái niệm đồ thị
Đồ thị là một cấu trúc rời rạc gồm các đỉnh và các cạnh nối các đỉnh đó.
Đồ thị được ký hiệu là G = (V, E), trong đó V là tập đỉnh và E là tập cạnh.
1.1.2 Các loại đồ thị
Định nghĩa 1: Một đơn đồ thị G = (V, E) gồm một tập không rỗng V mà
các phần tử của nó gọi là các đỉnh và một tập E mà các phần tử của nó gọi là các
cạnh, đó là các cặp không thứ tự của các đỉnh phân biệt.
Định nghĩa 2: Một đa đồ thị G = (V, E) gồm một tập các đỉnh V, một tập
các cạnh E và một hàm f từ E tới {{u,v} | u,v V, u v}. Các cạnh e1 và e2 được
gọi là song song hay cạnh bội nếu f(e1) = f(e2).
Định nghĩa 3: Một giả đồ thị G = (V, E) gồm một tập các đỉnh V, một tập
các cạnh E và một hàm f từ E tới {{u,v} | u,v V}. Một cạnh là một khuyên nếu
f(e) = {u} với một đỉnh u nào đó.
Định nghĩa 4: Một đồ thị có hướng G = (V, E) gồm một tập các đỉnh V và
một tập các cạnh E là các cặp có thứ tự của các phần tử V.
Định nghĩa 5: Một đa đồ thị có hướng G = (V, E) gồm một tập các đỉnh
V, một tập các cạnh E và một hàm f từ E tới {(u,v) | u,v V}. Các cạnh e1 và e2 là
các cạnh bội nếu f(e1) = f(e2).
1.1.3 Biểu diễn đồ thị
a. Danh sách liền kề
Sử dụng danh sách liền kề để biểu diễn đồ thị không có cạnh bội. Danh
sách này chỉ rõ các đỉnh nối với mỗi đỉnh của đồ thị.


8


1
5
2
4

3

Đỉnh

Đỉnh liền kề

1

2, 3,4, 5

2

1, 3, 4, 5

3

1, 2, 4

4

1, 2, 3


5

1, 2

b. Ma trận liền kề
Giả sử G = (V, E) là một đồ thị đơn trong đó |V| = n và các đỉnh được liệt
kê tuỳ ý v1, ..., v n. Ma trận liền kề A của G ứng với danh sách các đỉnh này là ma
trận không một cấp n*n có phần tử hàng i, cột j bằng 1 nếu vi và vj liền kề nhau,
và bằng 0 nếu chúng không được nối với nhau.

1

1

2

3

4

5

1

0

1

1


1

1

2

1

0

1

1

1

3

1

1

0

1

0

4


1

1

1

0

0

5

1

1

0

0

0

5
2
3

4

c. Ma trận liên thuộc
Giả sử G = (V, E) là một đồ thị vô hướng, v1, v2, ..., v n là tập các đỉnh còn

e1, e2, ..., em là tập các cạnh của nó. Khi đó ma trận liên thuộc theo thứ tự trên của
V và E là ma trận M = [mij] trong đó:
mij = 1 nếu cạnh ej nối với đỉnh vi
mij = 0 nếu cạnh ej không nối với đỉnh vi

9


e6
V1

e1

V5

e4

e5

e7
e3

V2

e2

V3

V4


e8

e1

e2

e3

e4

e5

e6

e7

e8

v1

1

0

0

0

1


1

1

0

v2

1

1

1

1

0

0

0

0

v3

0

0


0

1

0

1

0

1

v4

0

0

1

0

0

0

1

1


v5

0

1

0

0

1

0

0

0

1.2 Đồ thị hai phía
1.2.1 Định nghĩa
Một đơn đồ thị vô hướng G = (V, E) được gọi là đồ thị hai phía nếu tập các
đỉnh V có thể phân thành hai tập con không rỗng, rời nhau X và Y sao cho mỗi
cạnh của đồ thị nối một đỉnh của X với một đỉnh của Y.
Khi đó, người ta còn ký hiệu G là ( X U Y, E) và gọi một tập (giả sử là tập
X) là tập các đỉnh trái và tập còn lại là tập các đỉnh phải của đồ thị hai phía G.
Các đỉnh thuộc X còn gọi là các X_đỉnh, các đỉnh thuộc Y gọi là các Y_đỉnh.

Y

X


1.2.2 ứng dụng
Đồ thị hai phía được ứng dụng phổ biến nhất trong việc giải các bài toán
phân công công việc trong thực tế.

10


Chương 2
Bài toán tìm bộ ghép cực đại trên đồ thị
Và các thuật toán
2.1 bài toán tìm bộ ghép cực đại trên đồ thị hai phía
2.1.1 Bài toán ghép đôi không trọng và các khái niệm
Cho một đồ thị hai phía G = ( X U Y, E) ở đây X là tập các đỉnh trái và Y
là tập các đỉnh phải của G. X = { x[1], x[2], ..., x[m] }, Y = { y[1], y[2], ..., y[n] }
Một bộ ghép của G là tập hợp các cạnh của G đôi một không có đỉnh
chung. Bài toán ghép đôi là tìm một bộ ghép lớn nhất (nghĩa là có số cạnh lớn
nhất) của G.
Xét một bộ ghép M của G
Các đỉnh trong M gọi là các đỉnh đậm, các đỉnh khác là đỉnh nhạt.
Các cạnh trong M gọi là các cạnh đậm, các cạnh khác là cạnh nhạt.
Nếu định hướng lại các cạnh của đồ thị thành cung, những cạnh nhạt được định
hướng từ X sang Y, những cạnh đậm định hướng từ Y về X. Trên đồ thị định
hướng đó: Một đường đi xuất phát từ một X_đỉnh là đỉnh nhạt gọi là đường pha,
một đường đi từ một X_đỉnh là đỉnh nhạt tới một Y_đỉnh là đỉnh nhạt gọi là
đường mở.
Một cách dễ hiểu, có thể quan niệm như sau:
Một đường pha là một đường đi đơn trong G bắt đầu bằng một X_đỉnh là đỉnh
nhạt, đi theo một cạnh nhạt sang Y rồi đến một cạnh đậm về X, rồi lại đến một
cạnh nhạt sang Y... cứ xen kẽ nhau như vậy.

Một đường mở là một đường pha. Bắt đầu từ một X_đỉnh là đỉnh nhạt kết thúc
bằng một Y_đỉnh là đỉnh nhạt.
Ví dụ: Với đồ thị hai phía trong hình vẽ dưới đây và bộ ghép M = {(x[1],
y[1]), (x[2], y[2])}
x[3] và y[3] là những đỉnh nhạt, các đỉnh khác là đỉnh đậm.
Đường (x[3], y[2], x[2], y[1]) là đường pha
Đường (x[3], y[2], x[2], y[1], x[1], y[3]) là đường mở

11


1

1

2

2

3

3

X

Y

2.1.2 Thuật toán đường mở
Thuật toán đường mở để tìm một bộ ghép lớn nhất phát biểu như sau:
Bước 1:

Bắt đầu từ một bộ ghép bất kỳ M (thông thường bộ ghép được khởi gán
bằng bộ ghép rỗng hay được tìm bằng thuật toán tham lam).
Bước 2:
Tìm một đường mở.
Bước 3:
Nếu bước 2 tìm được đường mở thì mở rộng bộ ghép M: Trên đường mở,
loại bỏ những cạnh đã ghép (cạnh đậm) khỏi M và thêm vào M những cạnh chưa
ghép (cạnh nhạt). Sau đó lặp lại bước 2.
Nếu bước 2 không tìm được đường mở thì thuật toán kết thúc.
Người ta đã chứng minh được chi phí thời gian thực hiện giải thuật này
trong trường hợp xấu nhất sẽ là O(n3) đối với đồ thị dày và O(n(n+m)logn) đối
với đồ thị thưa.
2.2 bài toán tìm bộ ghép cực đại với tổng trọng số cực
đại hoặc cực tiểu trên đồ thị hai phía
2.2.1 Bài toán phân công
Đây là dạng bài toán phát biểu như sau: Một xí nghiệp có N công nhân và
dây chuyền sản xuất có M máy. Nếu công nhân i đứng ở máy j thì sẽ tạo lợi
nhuận cho nhà máy là C(i,j). Hãy bố trí công nhân sao cho không quá một công

12


nhân ở một máy và mỗi công nhân làm không quá một máy mà tổng lợi nhuận là
lớn nhất.
Điều kiện M <= N.
2.2.2 Thuật toán tìm cặp ghép với tổng trọng số trên các cạnh là lớn nhất
hoặc nhỏ nhất
* Thuật toán tìm cặp ghép với tổng trọng số trên các cạnh là lớn nhất:
Bước 1:
+ Tạo đồ thị hai phía (X là phía máy có M đỉnh, Y là phía người có N đỉnh). Có

cung (i,j) với trọng số C[i,j] nếu công nhân i làm việc trên máy j tạo lợi nhuận
C[i,j].
+ Tạo nhãn ban đầu chấp nhận được Fx và Fy theo quy tắc:
Fx[i] = Max( C[i,j], Vj: 1 <= j <= M)
Fy[j] = 0, Vj: 1 <= j <= M
(FX, FY gọi là chấp nhận được nếu thoả mãn bất đẳng thức Fx[i] + Fy[j] >=
C[i,j])
Nếu Fx[i] + Fy[j] = C[i,j] thì ta coi cạnh (i,j) là cạnh đậm (là đã ghép máy
i cho công nhân j), các cạnh còn lại là cạnh nhạt. Hai đầu cạnh đậm là đỉnh đậm,
các đỉnh còn lại là đỉnh nhạt.
Vậy bằng cách tạo nhãn ban đầu như trên chúng ta có cặp ghép M ban đầu
(ghép được một số máy với công nhân tạo lợi nhuận trên các máy đó).
Bước 2:
For (i:=1 --> m)
Begin
Nếu còn đỉnh nhạt i của X (nghĩa là còn công nhân chưa được xếp việc)
thì:
Lặp
Tìm dây chuyền (bằng logic hoặc đệ qui).
Nếu không có dây chuyền thì sửa nhãn.
Ngược lại tăng cặp ghép trên dây chuyền này.
Cho đến khi tìm được dây chuyền;
End;

13


Trong thuật toán trên phải thực hiện các thao tác: tìm dây chuyền, sửa
nhãn và tăng cặp ghép.
Sửa nhãn: Phải thực hện khi dây chuyền không kết thúc được bằng đỉnh nhạt bên

Y mà kết thúc bằng đỉnh đậm bên X (ta gọi là dây chuyền dở dang). Trên các
cung nối một đỉnh bên X đã nạp vào dây chuyền tới các đỉnh j thuộc Y chưa
thuộc dây chuyền, chọn giá trị bé nhất trong các giá trị: Fx[i] + Fy[j] C[i,j]. Giá
trị này được chọn làm lượng sửa nhãn (kí hiệu là m). Sửa nhãn theo cách như sau:
Nhãn các đỉnh của X thuộc dây chuyền sẽ giảm đi một lượng là m, nhãn các đỉnh
của Y thuộc dây chuyền sẽ tăng thêm một lượng là m, để đảm bảo Fx[i] + Fy[j]
C[i,j] >= 0. Sau khi các đỉnh thuộc dây chuyền dở dang đã được sửa nhãn thì nó
có khả năng mới kết hợp với các đỉnh j bên Y tạo nên một dây chuyền hoàn chỉnh
(vì sẽ xuất hiện những cặp (i,j) mới mà Fx[i] + Fy[j] = C[i,j]).
Tăng cặp ghép: Đổi màu các cung, bắt đầu từ cung nhạt cuối cùng của dây
chuyền đổi ngược dần về cung nhạt đầu tiên của dây chuyền.
Tìm dây chuyền: Có thể tìm kiếm theo chiều sâu hoặc tìm kiếm theo chiều rộng.
Yêu cầu dây chuyền xuất phát từ một đỉnh nhạt của X, kết thúc bằng một đỉnh
nhạt của Y, đồng thời các cung nhạt và đậm liên tiếp xen kẽ nhau (do cung đầu
và cung cuối đều nhạt, nên số cung nhạt lớn hơn số cung đậm là 1).
* Nếu là bài toán tìm tổng nhỏ nhất thì đổi dấu C[i,j] và cuối cùng đổi dấu tổng
hoặc khởi trị C[i,j] = vô cùng, nhãn ban đầu V i X: Fx[i] = Min { C[i,j] V j
Y}, Fy[j] = 0, V j Y. Lượng sửa nhãn m = Min { C[i,j] Fx[i] Fy[j] }
2.2.3 Thuật toán Hung-ga-ri
Bài toán: Có N người (số hiệu i = 1, 2, ..., N) và N công việc (số hiệu j = 1,
2, ..., N). Để giao việc j cho người i thực hiện cần chi phí là Cij không âm. Vấn đề
là cần phân cho người nào làm việc gì (mỗi người chỉ làm một việc, mỗi việc chỉ
do một người làm) sao cho chi phí tổng cộng là nhỏ nhất.
Một trong các phương pháp giải bài toán này là tìm cặp ghép có tổng trọng
số trên các cung đạt giá trị nhỏ nhất trên đồ thị hai phía. Nhưng cũng có thể giải
bằng phương pháp Hung-ga-ri được xây dựng trên cơ sở các định lý sau đây:

14



Định lí 1: Nếu ma trận chi phí C(N,N) có N số 0 mà không có số 0 nào cùng
hàng hoặc cùng cột thì có phương án tối ưu là: Nếu số 0 ở (i,j) thì phân công
người i làm việc j.
Định lí 2: Nếu thay ma trận C(N,N) bởi ma trận C(N,N) bằng cách trừ mỗi phần
tử trong một hàng (hoặc cột) cho phần tử nhỏ nhất của hàng đó (hoặc cột đó) thì
phương án tối ưu không thay đổi khi tìm phương án này trên C(N,N).
Thuật toán
Bước 1:
Thực hiện trên ma trận chi phí C: Trừ mỗi hàng cho phần tử nhỏ nhất trên
hàng đó. Trừ mỗi cột cho phần tử nhỏ nhất trên cột đó. Kết quả thu được ma trận
C có tính chất: trên mỗi hàng, trên mỗi cột có ít nhất một số 0 (ta kí hiệu những
ô số 0 này là T).
Bước 2:
Lần lượt từ hàng 1 đến hàng N, trên mỗi hàng tìm ô T đầu tiên thuộc cột
chưa có ô Đ thì gán lại kí hiệu T của ô này thành Đ. Khi xét đến hàng thứ N:
+ Nếu có đủ N ô Đ thì dừng và kết luận tập các ô Đ sẽ cho lời giải: người i
sẽ được phân công làm công việc j nếu ô Đ nằm ở hàng i cột j.
+ Nếu số lượng ô Đ nhỏ hơn N thì chuyển sang bước 3.
Bước 3:
Lần lượt từ hàng 1 đến hàng N, tìm hàng đầu tiên không chứa ô Đ (giả sử
đó là hàng i0). Tìm ô T trên hàng i0, chẳng hạn ở cột j0. Xuất phát từ ô T(i0,j0) này
ta xây dựng một dây chuyền các ô kế tiếp nhau theo cột rồi theo hàng xen kẽ các
ô T và Đ. Giả sử đã có dây chuyền T(i0,j0), Đ( i1,j0), T(i1,j1), ..., ta tìm tiếp theo
cách thức sau:
+ (3-A): Nếu đã tìm đến ô T(ik,jk) ta tìm ô T trong cột jk không cùng dòng
với ô Đ nào đã có trong dây chuyền đang xét. Nếu tìm thấy T ở dòng ik+1 thì gán
lại kí hiệu T của ô này thành Đ được ô Đ(ik+1,jk) và thêm nó vào dây chuyền rồi
chuyển tới bước (3-B), trái lại ta được một dây chuyền bắt đầu là ô T(i0,j0) và kết
thúc tại ô T(ik,jk), ta đổi kí hiệu màu các ô trên dây chuyền (T thành Đ và ngược
lại nghĩa là tăng luồng trên dây chuyền). Nếu đủ N ô Đ thì dừng, trái lại trở về

đầu bước 3 tìm hàng tiếp theo không chứa ô Đ.

15


+ (3-B): Nếu đã đến ô Đ(ik+1,jk), ta tìm trên hàng ik+1 ô T không cùng cột
với bất kì ô nào đã có mặt trong dây chuyền đang xét. Nếu tìm được trên cột jk+1
thì thêm ô T(ik+1,jk+1) vào dây chuyền rồi chuyển tới (3-A). Trong trường hợp
ngược lại thì dây chuyền xuất phát từ T(i0,j0) và kết thúc tại Đ(ik+1,jk) và không thể
tăng luồng trên dây chuyền này được (vì số ô T bằng số ô Đ). Ta sẽ loại khỏi dây
chuyền các ô T(ik,jk) và Đ(ik+1,jk) đồng thời đánh dấu cột jk là cột trở ngại, lui về
ô Đ(ik,jk-1) tiếp tục mở rộng dây chuyền nếu trên hàng ik còn có ô T khác với ô
T(ik,jk) không nằm trên cột trở ngại và các cột đã có mặt trong dây chuyền đang
xét, nghĩa là ta lặp lại bước (3-B) với hàng ik.
Nếu không tiếp tục mở rộng được dây chuyền theo cách trên nữa thì tìm ô
T trên dòng i0 ở cột j0 không là cột trở ngại. Nếu ô T như thế tồn tại thì bắt đầu
lặp lại (3-A), ngược lại chuyển sang bước 4.
Bước 4:
Xác định các hàng trở ngại là các hàng không thể chứa ô Đ ở cột không
trở ngại.
Bước 5:
Tìm số nhỏ nhất trên các ô không là ô Đ, thuộc hàng không trở ngại và
cột không trở ngại của ma trận chi phí C, gọi số này là m. Trừ mọi phần tử
thuộc hàng và cột không trở ngại với m, cộng mọi phần tử thuộc hàng và cột
trở ngại với m.
Quay về bước 2 với ma trận mới thu được là C đóng vai trò là C.
2.2.4 Phương pháp đối ngẫu Kuhn-Munkres
Phương pháp đối ngẫu Kuhn-Munkres đi tìm hai dãy số Fx[1..k] và
Fy[1..k] thoả mãn:
C[i,j] Fx[i] Fy[j] >= 0

Tập các cạnh (x[i], y[j]) thoả mãn C[i,j] Fx[i] Fy[j] = 0 chứa trọn một bộ
ghép đầy đủ k cạnh, đây chính là bộ ghép cần tìm.
Rõ ràng nếu tìm được hai dãy số thoả mãn trên thì ta chỉ việc thực hiện hai
thao tác:
+ Với mỗi đỉnh x[i], trừ tất cả trọng số của những cạnh liên thuộc với x[i] đi một
lượng Fx[i].

16


+ Với mỗi đỉnh y[j], trừ tất cả trọng số của những cạnh liên thuộc với y[j] đi một
lượng Fy[j].
(Hai thao tác này tương đương với việc trừ tất cả trọng số của các cạnh (x[i], y[j])
đi một lượng Fx[i] + Fy[j] tức là C[i,j] := C[i,j] Fx[i] Fy[j])
Thì dễ thấy đồ thị mới tạo thành sẽ gồm có các cạnh trọng số không âm và những
cạnh có trọng số bằng 0 của đồ thị chứa trọn một bộ ghép đầy đủ.
Vậy phương pháp đối ngẫu Kuhn-Munkres đưa việc biến đổi đồ thị G
(biến đổi ma trận C) về việc biến đổi hai dãy số Fx và Fy. Việc trừ một lượng
delta vào trọng số tất cả các cạnh liên thuộc với x[i] tương đương với việc tăng
Fx[i] lên một lượng delta. Việc cộng một lượng delta vào trọng số tất cả các cạnh
liên thuộc với y[j] tương đương với việc giảm Fy[j] đi một lượng delta. Khi cần
biết trọng số cạnh (x[i], y[j]) là bao nhiêu sau các bước biến đổi, thay vì viết
C[i,j], ta viết C[i,j] Fx[i] Fy[j].
Sơ đồ cài đặt phương pháp đối ngẫu Kuhn-Munkres có thể viết như sau:
Bước 1: Khởi tạo:
M := O;
Việc khởi tạo các Fx, Fy có thể có nhiều cách miễn sao C[i,j] Fx[i] Fy[j] >=
0, đơn giản nhất có thể đặt tất cả các Fx[.], Fy[.] bằng 0.
Bước 2: Với mọi đỉnh x* X, ta tìm cách ghép x* như sau:
Bắt đầu từ đỉnh x*, thử tìm đường mở bắt đầu ở x* bằng thuật toán tìm

kiếm trên đồ thị (BFS hoặc DFS). Có hai khả năng xảy ra:
+ Hoặc tìm được đường mở thì dọc theo đường mở, ta loại bỏ những cạnh đã ghép
khỏi M và thêm vào M những cạnh chưa ghép.
+ Hoặc không tìm được đường mở thì xác định được:
VisitedX = { Tập những X_đỉnh có thể đến được từ x* bằng một đường
pha}
VisitedY = { Tập những Y_đỉnh có thể đến được từ x* bằng một đường
pha}
Đặt delta = min { C[i,j] Fx[i] Fy[j] | Vx[i] VisitedX, Vy[j]
VisitedY}
Với Vx[i] VisitedX: Đặt Fx[i] := Fx[i] + delta;

17


Với Vy[j] VisitedY: Đặt Fy[j] := Fy[j] - delta;
Lặp lại thủ tục tìm đường mở xuất phát tại x* cho tới khi tìm ra đường mở.
Bước 3:
Lưu ý rằng bước 2 luôn tìm ra đường mở vì đồ thị đã được làm cho trở nên
cân bằng (| X | = | Y |) và đầy đủ, ta chỉ việc trả về đường mở tìm được.
Đáng lưu ý ở phương pháp Kuhn-Munkres là phương pháp này không làm
thay đổi ma trận C ban đầu. Điều đó thực sự hữu ích trong trường hợp trọng số
của cạnh (x[i], y[j]) không được cho một cách tường minh bằng giá trị C[i,j] mà
lại cho bằng hàm c(i,j): trong trường hợp này, việc trừ hàng/cộng cột trực tiếp trên
ma trận chi phí C là không thể thực hiện được.
2.2.5 Nâng cấp
Dựa vào sơ đồ cài đặt thuật toán Kuhn-Munkres ở trên, ta có thể đánh giá
độ phức tạp tính toán lý thuyết của cách cài đặt này:
Thuật toán tìm kiếm theo chiều rộng được sử dụng để tìm đường mở có độ
phức tạp O(k2), mỗi lần xoay trọng số cạnh mất một chi phí thời gian cỡ O(k2).

Vậy, mỗi lần tăng cặp cần tối đa k lần dò đường và k lần xoay trọng số cạnh, mất
một chi phí thời gian cỡ O(k3). Thuật toán cần k lần tăng cặp nên độ phức tạp tính
toán trên lý thuyết của phương pháp này cỡ O(k4). Có thể cải tiến mô hình cài đặt
để được một thuật toán với độ phức tạp O(k3) dựa trên những nhận xét sau:
Nhận xét 1:
Quá trình tìm kiếm theo chiều rộng bắt đầu từ một đỉnh x* chưa ghép cho
ta một cây pha gốc x*. Nếu tìm được đường mở thì dừng lại và tăng cặp ngay, nếu
không thì xoay trọng số cạnh và bắt đầu tìm kiếm lại để được một cây pha lớn
hơn cây pha cũ.
Nhận xét 2:
Việc xác định trọng số nhỏ nhất của cạnh nối một X_đỉnh trong cây pha
với một Y_đỉnh ngoài cây pha có thể kết hợp ngay trong bước dựng cây pha mà
không làm tăng cấp phức tạp tính toán. Để thực hiện được điều này, ta sử dụng kỹ
thuật sau:

18


Với mọi y[j] Y, gọi d[j] là khoảng cách từ y[j] đến cây pha gốc x*. Ban
đầu d[j] được khởi tạo bằng trọng số cạnh (x*,y[j]) (cây pha ban đầu chỉ có đúng
một đỉnh x*).
Trong bước tìm đường bằng BFS, mỗi lần rút một đỉnh x[i] ra khỏi Queue,
ta xét những đỉnh y[j] Y chưa thăm và đặt lại d[j]mới := min(d[j]cũ, trọng số cạnh
(x[i], y[j])) sau đó mới kiểm tra xem (x[i],y[j]) có phải là cạnh có trọng số bằng 0
hay không để tiếp tục các thao tác như trước. Nếu quá trình BFS không tìm ra
đường mở thì giá trị xoay delta chính là giá trị nhỏ nhất trong các d[j] dương. Ta
bớt được một đoạn chương trình tìm giá trị xoay có độ phức tạp O(k2). Công việc
tại mỗi bước xoay chỉ là tìm giá trị nhỏ nhất trong các d[j] dương và thực hiện
phép cộng, trừ trên hai dãy đối ngẫu Fx và Fy, nó có độ phức tạp tính toán O(k).
Tối đa có k lần xoay để tìm đường mở nên tổng chi phí thời gian thực hiện các lần

xoay cho tới khi tìm ra đường mở cỡ O(k2). Lưu ý rằng đồ thị đang xét là đồ thị
hai phía đầy đủ nên khi xoay các trọng số cạnh bằng giá trị xoay delta, tất cả các
cạnh nối từ X_đỉnh trong cây pha tới Y_đỉnh ngoài cây pha đều bị giảm trọng số
đi delta, chính vì vậy sau mỗi bước xoay, ta phải trừ tất cả các d[j]> 0 đi delta để
giữ được tính hợp lý của các d[j].
Nhận xét 3:
Ta có thể tận dụng kết quả của quá trình tìm kiếm theo chiều rộng ở bước
trước để nới rộng cây pha cho bước sau mà không phải dựng cây pha lại từ đầu.
Khi không tìm thấy đường mở, bước xoay trọng số cạnh sẽ được thực hiện.
Sau khi xoay, ta sẽ thăm luôn những đỉnh y[j] Y chưa thăm tạo với một X_đỉnh
đã thăm một cạnh có trọng số bằng 0, nếu tìm thấy đường mở thì dừng ngay, nếu
không tìm thấy thì đẩy tiếp những đỉnh thuộc tập X ghép với đỉnh y[j] vào hàng
đợi và lặp lại thuật toán tìm kiếm theo chiều rộng bắt đầu từ những đỉnh này. Vậy
nếu xét tổng thể, mỗi lần tăng cặp ta chỉ thực hiện một lần dựng cây pha, tức là
tổng chi phí thời gian của những lần thực hiện giải thuật tìm kiếm trên đồ thị sau
mỗi lần tăng cặp chỉ còn là O(k2).
Nhận xét 4:
Thủ tục tăng cặp dựa trên đường mở có độ phức tạp O(k).

19


Từ 3 nhận xét trên, phương pháp đối ngẫu Kuhn-Munkres có thể cài đặt
bằng một chương trình có độ phức tạp tính toán O(k3) bởi nó có k lần tăng cặp và
chi phí cho mỗi lần là O(k2)
2.3 Bài toán tìm bộ ghép cực đại trên đồ thị tổng quát
2.3.1 Các khái niệm
Xét đồ thị G = (V, E), một bộ ghép trên đồ thị G là một tập các cạnh đôi
một không có đỉnh chung.
Bài toán tìm bộ ghép cực đại trên đồ thị tổng quát phát biểu như sau:

Cho một đồ thị G, phải tìm một bộ ghép cực đại trên G (bộ ghép có nhiều
cạnh nhất).
Với một bộ ghép M của đồ thị ta gọi:
+ Những cạnh thuộc M được gọi là cạnh đã ghép hay cạnh đậm.
+ Những cạnh không thuộc M được gọi là cạnh chưa ghép hay cạnh nhạt.
+ Những đỉnh đầu mút của các cạnh đậm được gọi là đỉnh đã ghép, những đỉnh
còn lại gọi là đỉnh chưa ghép.
+ Một đường đi cơ bản (đường đi không có đỉnh lặp lại) được gọi là đường pha
nếu nó bắt đầu bằng một cạnh nhạt và tiếp theo là các cạnh đậm, nhạt nằm nối
tiếp xen kẽ nhau.
+ Một chu trình cơ bản (chu trình không có đỉnh trong lặp lại) được gọi là một
Blossom nếu nó đi qua ít nhất 3 đỉnh, bắt đầu và kết thúc bằng cạnh nhạt và dọc
trên chu trình, các cạnh đậm, nhạt nằm nối tiếp xen kẽ nhau. Đỉnh xuất phát của
chu trình (cũng là đỉnh kết thúc) được gọi là đỉnh cơ sở của Blossom.
+ Đường mở là một đường pha bắt đầu ở một đỉnh chưa ghép và kết thúc ở một
đỉnh chưa ghép.
Ví dụ:
Với đồ thị G và bộ ghép M trong hình vẽ dưới đây
Đường (8, 1, 2, 5, 6, 4) là một đường pha
Chu trình (2, 3, 4, 6, 5, 2) là một Blossom
Đường (8, 1, 2, 3, 4, 6, 5, 7) là một đường mở

20


Đường (8, 1, 2, 3, 4, 6, 5, 2, 1, 9) tuy có các cạnh đậm/nhạt xen kẽ nhưng
không phải đường pha (và tất nhiên không phải đường mở) vì đây không phải là
đường đi cơ bản.

8


1

3

4

5

6

2

9

7
Ta dễ dàng suy ra được các tính chất sau:
+ Đường mở cũng như Blossom đều là đường đi độ dài lẻ với số cạnh nhạt nhiều
hơn số cạnh đậm đúng 1 cạnh.
+ Trong mỗi Blossom, những đỉnh không phải là đỉnh cơ sở đều là đỉnh đã ghép
và đỉnh ghép với đỉnh đó cũng phải thuộc Blossom.
+ Vì Blossom là một chu trình nên trong mỗi Blossom, những đỉnh không phải
đỉnh cơ sở đều tồn tại hai đường pha từ đỉnh cơ sở đi đến nó, một đường kết thúc
bằng cạnh đậm và một đường kết thúc bằng cạnh nhạt, hai đường pha này được
hình thành bằng cách đi dọc theo chu trình theo hai hướng ngược nhau. Như ví dụ
trên, đỉnh 4 có hai đường pha từ đỉnh cở sở 2 đi tới: (2, 3, 4) là đường pha kết thúc
bằng cạnh đậm và (2, 5, 6, 4) là đường pha kết thúc bằng cạnh nhạt.
2.3.2 Thuật toán Edmonds (1965)
Cơ sở của thuật toán là định lý (C.Berge): Một bộ ghép M của đồ thị G là
cực đại khi và chỉ khi không tồn tại đường mở đối với M.

Thuật toán Edmonds:
M:=O;
For ( V đỉnh u chưa ghép ) do
If (Tìm đường mở xuất phát từ u) then

21


M những cạnh nhạt>;
<Trả về M là bộ ghép cực đại trên G>;
Điều khó nhất trong thuật toán Edmonds là phải xây dựng thuật toán tìm
đường mở xuất phát từ một đỉnh chưa ghép. Thuật toán đó được xây dựng bằng
cách kết hợp một thuật toán tìm kiếm trên đồ thị với phép chập Blossom.
Xét những đường pha xuất phát từ một đỉnh x chưa ghép. Những đỉnh có
thể đến được từ x bằng một đường pha kết thúc là cạnh nhạt được gán nhãn
nhạt (gọi tắt là đỉnh nhạt), những đỉnh có thể đến được từ x bằng một đường
pha kết thúc là cạnh đậm được gán nhãn đậm (gọi tắt là đỉnh đậm).
Với một Blossom, ta định nghĩa phép chập (shrink) là phép thay thế các
đỉnh trong Blossom bằng một đỉnh duy nhất. Những cạnh nối giữa một đỉnh thuộc
Blossom tới một đỉnh v nào đó không thuộc Blossom được thay thế bằng cạnh nối
giữa đỉnh chập này với v và giữ nguyên tính đậm/nhạt. Có thể kiểm chứng được
nhận xét: sau mỗi phép chập, các cạnh đậm vẫn được đảm bảo là bộ ghép trên đồ
thị mới.

Blosso
m

Blosso
m


= đỉnh cơ sở của Blossom

= đỉnh chập từ Blossom

Hình vẽ: Phép chập Blossom

22


Thuật toán tìm đường mở xuất phát từ đỉnh x có thể phát biểu như sau:
Trước hết đỉnh xuất phát x được gán nhãn đậm.
Tiếp theo là thuật toán tìm kiếm trên đồ thị bắt đầu từ x, theo nguyên tắc: từ đỉnh
đậm chỉ được phép đi tiếp theo cạnh nhạt và từ đỉnh nhạt chỉ được đi tiếp theo
cạnh đậm. Mỗi khi thăm tới một đỉnh, ta gán nhãn đậm/nhạt cho đỉnh đó và tiếp
tục thao tác tìm kiếm trên đồ thị như bình thường. Cũng trong quá trình tìm kiếm,
mỗi khi phát hiện thấy một cạnh nhạt nối hai đỉnh đậm, ta dừng lại ngay vì nếu
gán nhãn tiếp sẽ gặp tình trạng một đỉnh có cả hai nhãn đậm/nhạt, trong trường
hợp này, Blossom được phát hiện và bị chập thành một đỉnh, thuật toán được bắt
đầu lại với đồ thị mới cho tới khi trả lời được câu hỏi: có tồn tại đường mở xuất
phát từ x hay không?.
Nếu đường mở không đi qua đỉnh chập nào thì ta chỉ việc tăng cặp dọc
theo đường mở. Nếu đường mở có đi qua một đỉnh chập thì ta lại nở đỉnh chập đó
ra thành Blossom để thay đỉnh chập này trên đường mở bằng một đoạn đường
xuyên qua Blossom.

Hình vẽ: nở Blossom để dò đường xuyên qua Blossom
Lưu ý rằng không phải Blossom nào cũng bị chập, chỉ những Blossom ảnh
hưởng tới quá trình tìm đường mở mới phải chập để đảm bảo rằng đường mở tìm
được là đường đi cơ bản.


23


2.3.3 Thuật toán Lawler (1973)
Trong thuật toán Edmonds, sau khi chập mỗi Blossom thành một đỉnh thì
đỉnh đó hoàn toàn lại có thể nằm trên một Blossom mới và bị chập tiếp. Thuật
toán Lawler chỉ quan tâm tới đỉnh chập cuối cùng, đại diện cho Blossom ngoài
nhất, đỉnh chập cuối cùng này được định danh bằng đỉnh cơ sở của Blossom
ngoài nhất.
Cũng chính vì thao tác chập/nở nói trên mà ta cần mở rộng khái niệm
Blossom, có thể coi một Blossom là một tập đỉnh nở ra từ một đỉnh chập chứ
không đơn thuần là một chu trình pha cơ bản nữa.
Xét một Blossom B có đỉnh cơ sở là đỉnh r. Với V v B, v = r, ta lưu lại
hai đường pha từ r tới v, một đường kết thúc bằng cạnh đậm và một đường kết
thúc bằng cạnh nhạt, như vậy có hai loại vết gán cho mỗi đỉnh v (hai vết này được
cập nhật trong quá trình tìm đường):
+ S[v] là đỉnh liền trước v trên đường pha kết thúc bằng cạnh đậm, nếu không tồn
tại đường pha loại này thì S[v] = 0.
+ T[v] là đỉnh liền trước v trên đường pha kết thúc bằng cạnh nhạt, nếu không tồn
tại đường pha loại này thì T[v] = 0.
Bên cạnh hai nhãn S và T, mỗi đỉnh v còn có thêm:
+ Nhãn b[v] là đỉnh cơ sở của Blossom chứa v. Hai đỉnh u và v thuộc cùng một
Blossom

b[u] = b[v].

+ Nhãn match[v] là đỉnh ghép với đỉnh v. Nếu v chưa ghép thì match[v] = 0.
Khi đó, thuật toán tìm đường mở bắt đầu từ đỉnh x chưa ghép có thể
phát biểu như sau:

Bước 1: (Init)
+ Hàng đợi Queue dùng để chứa những đỉnh đậm chờ duyệt, ban đầu chỉ
gồm một đỉnh đậm x.
+ Với mọi đỉnh u khởi gán b[u] = u và match[u] = 0 với Vu.
+ Gán S[x] = 0; với Vu = x, gán S[u] = 0
+ Với Vv: gán T[v] = 0.
Bước 2: (BFS)
Lặp lại các bước sau cho tới khi hàng đợi rỗng:

24


Với mỗi đỉnh đậm u lấy ra từ Queue, xét những cạnh nhạt (u, v):
+ Nếu v chưa thăm:
Nếu:
. v là đỉnh chưa ghép
. v là đỉnh đã ghép

Tìm thấy đường mở kết thúc ở v, dừng.
thăm v

thăm luôn match[v] và đẩy

match[v] vào Queue.
Lưu vết: Cập nhật hai nhãn S và T.
+ Nếu v đã thăm:
-

Nếu v là đỉnh nhạt hoặc b[v] = b[u] ==> bỏ qua


-

Nếu v là đỉnh đậm và b[v] = b[u] ta phát hiện được Blossom mới chứa
u và v, khi đó:
. Phát hiện đỉnh cơ sở: Truy vết đường đi ngược từ hai đỉnh đậm u

và v theo hai đường pha về nút gốc, chọn lấy đỉnh a là đỉnh đậm chung gặp
đầu tiên trong quá trình truy vết ngược. Khi đó Blossom mới phát hiện sẽ
có đỉnh cơ sở là a.
. Gán lại vết: Gọi (a = i[1], i[2],.., i[p] = u) và (a = j[1], j[2],.., j[q] =
v) lần lượt là hai đường pha dẫn từ a tới u và v. Khi đó (a = i[1], i[2],.., i[p]
= u, j[q] = v, j[q-1],.., j[1] = a) là một chu trình pha đi từ a tới u và v rồi
quay trở về a. Bằng cách đi dọc theo chu trình này theo hai hướng ngược
nhau, ta có thể gán lại tất cả các nhãn S và T của những đỉnh trên chu
trình. Lưu ý rằng không được gán lại các nhãn S và T cho những đỉnh k mà
b[k] = a, và với những đỉnh k có b[k] = a thì bắt buộc phải gán lại nhãn S
và T theo chu trình này bất kể S[k] và T[k] trước đó đã có hay chưa.
. Chập Blossom: Xét những đỉnh v mà b[v] {b[i[1]], b[i[2]], ... ,
b[i[p]], b[j[1]], b[j[2]], ... , b[j[q]]}, gán lại b[v] = a. Nếu v là đỉnh đậm (có
nhãn S[v] = 0) mà chưa được duyệt tới (chưa bao giờ được đẩy vào Queue)
thì đẩy v vào Queue chờ duyệt tiếp tại những bước sau.
Bước 3:
Nếu bước 2 tìm được đường mở thì trả về đường mở, nếu bước 2 không tìm
thấy đường mở và thoát ra do hàng đợi rỗng thì kết luận không tìm thấy đường
mở.

25


Tư tưởng chính của phương pháp Lawler là dùng các nhãn b[v] thay cho

thao tác chập trực tiếp Blossom, dùng các nhãn S và T để truy vết tìm đường mở,
tránh thao tác nở Blossom. Phương pháp này dựa trên một nhận xét: Mỗi khi tìm
ra đường mở, nếu đường mở đó xuyên qua một Blossom ngoài nhất thì chắc chắn
nó phải đi vào Blossom này từ nút cơ sở và thoát ra ngoài bằng một cạnh nhạt.

26


Chương 3
Giới thiệu ngôn ngữ delphi
Delphi có tiền thân l ngôn ngữ Pascal. Có thể nói Delphi l một trong tứ đại
thiên vương của thế giới ngôn ngữ lập trình hiện nay đó l C/C++, Delphi,
Visual Basic v Java. Trong số đó chỉ có Delphi l có khả năng sánh ngang v
vượt trội ngôn ngữ lập trình C/C++. Trong chương này chúng ta sẽ tìm hiểu một
cách khái quát về ngôn ngữ lập trình Delphi.
Những vấn đề sẽ được đề cập trong chương này:
Khái quát về ngôn ngữ Delphi
Form và các thành phần giao diện
3.1 Khái quát về ngôn ngữ Delphi
3.1.1 Delphi l gì?
Delphi có hạt nhân l trình biên dịch Pascal rất nổi tiếng v quen thuộc.
Delphi l một bước đột phá tiếp theo của trình biên dịch Pascal, được hãng
Borland phát triển liên tục kể từ khi Anders Hejlsberg viết ra trình biên dịch
Turbo Pascal đầu tiên cách đây 17 năm. Kế thừa mọi đặc tính của Pascal, trình
biên dịch của Delphi l sự tổng hợp tinh hoa về kinh nghiệm của Pascal qua hơn
một thập kỷ, cộng với kỹ thuật biên dịch được tối ưu hoá dựa trên kiến trúc 32-bit
của bộ xử lý. Ngôn ngữ Delphi mang tính ổn định, sự uyển chuyển, độ tin cậy
cao.
Delphi l môi trường ứng dụng tức thời RAD (Rapid Application
Divelopment) bao gồm ốac công cụ phát triển hệ thống v cơ sở dữ liệu dnh cho

Microsoft Windows 95/98 v Windows NT. Delphi kết hợp sự tiện dụng của môi
trường phát triển trực quan, tốc độ v sức mạnh của trình biên dịch 32-bit, khả
năng quản lý cơ sở dữ liệu chặt chẽ với hạt nhân (thư viện BDE) uyển chuyển có
thể truy suất nhiều loại cơ sở dữ liệu khác nhau. Mặc dù bản thân các đặc tính
trên không phải l đặc trưng riêng của Delphi nhưng nhìn chung Delphi đã tích
hợp các công nghệ riêng rẽ để tạo nên một môi trường phát triển ton diện, cần
thiết v rất hữu ích cho ngnh công nghiệp phần mềm hiện nay.

27


×