Các loại thuật toán căn bản trong lập trình được tổng hợp lại trong một file. Phù hợp cho các bạn chuyên ngành công nghệ thông tin. Các thuật toán được mô tả chi tiết. Toàn bộ ebook hơn 300 trang. Chúc các bạn hoc tốt.
PHẦN BÀI TỐN LIỆT KÊ Có số toán thực tế yêu cầu rõ: tập đối tượng cho trước có đối tượng thoả mãn điều kiện định Bài toán gọi tốn đếm Trong lớp tốn đếm, có tốn u cầu rõ cấu hình tìm thoả mãn điều kiện cho cấu hình Bài tốn u cầu đưa danh sách cấu hình có gọi tốn liệt kê Để giải toán liệt kê, cần phải xác định thuật tốn để theo xây dựng tất cấu hình quan tâm Có nhiều phương pháp liệt kê, chúng cần phải đáp ứng hai u cầu đây: • Khơng lặp lại cấu hình • Khơng bỏ sót cấu hình Có thể nói rằng, phương pháp liệt kê phương kế cuối để giải số tốn tổ hợp Khó khăn phương pháp bùng nổ tổ hợp dẫn tới đòi hỏi lớn khơng gian thời gian thực chương trình Tuy nhiên với phát triển máy tính điện tử, phương pháp liệt kê, nhiều toán tổ hợp tìm thấy lời giải Qua đó, ta nên biết nên dùng phương pháp liệt kê không phương pháp khác tìm lời giải Chính nỗ lực giải tốn thực tế không dùng phương pháp liệt kê thúc đẩy phát triển nhiều ngành toán học Chuyên đề §1 NHẮC LẠI MỘT SỐ KIẾN THỨC ĐẠI SỐ TỔ HỢP Cho S tập hữu hạn gồm n phần tử k số tự nhiên Gọi X tập số nguyên dương từ đến k: X = {1, 2, …, k} 1.1 CHỈNH HỢP LẶP Mỗi ánh xạ f: X → S Cho tương ứng với i ∈ X, phần tử f(i) ∈ S Được gọi chỉnh hợp lặp chập k S Nhưng X tập hữu hạn (k phần tử) nên ánh xạ f xác định qua bảng giá trị f(1), f(2), …, f(k) Ví dụ: S = {A, B, C, D, E, F}; k = Một ánh xạ f cho sau: i f(i) E C E Vậy đồng f với dãy giá trị (f(1), f(2), …, f(k)) coi dãy giá trị chỉnh hợp lặp chập k S Như ví dụ (E, C, E) chỉnh hợp lặp chập S Dễ dàng chứng minh kết sau quy nạp phương pháp đánh giá khả lựa chọn: Số chỉnh hợp lặp chập k tập gồm n phần tử n k 1.2 CHỈNH HỢP KHƠNG LẶP Khi f đơn ánh có nghĩa với ∀i, j ∈ X ta có f(i) = f(j) ⇔ i = j Nói cách dễ hiểu, dãy giá trị f(1), f(2), …, f(k) gồm phần tử thuộc S khác đơi f gọi chỉnh hợp không lặp chập k S Ví dụ chỉnh hợp khơng lặp (C, A, E): i f(i) C A E Số chỉnh hợp không lặp chập k tập gồm n phần tử là: n Pk = n(n − 1)(n − 2) (n − k + 1) = n! (n − k)! 1.3 HOÁN VỊ Khi k = n Một chỉnh hợp không lặp chập n S gọi hốn vị phần tử S Ví dụ: hoán vị: 〈A, D, C, E, B, F〉 S = {A, B, C, D, E, F} i f(i) A D C E B F ĐHSPHN 1999-2004 Bài toán liệt kê Để ý k = n số phần tử tập X = {1, 2, …, n} số phần tử S Do tính chất đơi khác nên dãy f(1), f(2), …, f(n) liệt kê hết phần tử S Như f toàn ánh Mặt khác giả thiết f chỉnh hợp không lặp nên f đơn ánh Ta có tương ứng 1-1 phần tử X S, f song ánh Vậy nên ta định nghĩa hốn vị S song ánh {1, 2, …, n} S Số hoán vị tập gồm n phần tử = số chỉnh hợp không lặp chập n = n! 1.4 TỔ HỢP Một tập gồm k phần tử S gọi tổ hợp chập k S Lấy tập k phần tử S, xét tất k! hoán vị tập Dễ thấy hốn vị chỉnh hợp khơng lặp chập k S Ví dụ lấy tập {A, B, C} tập tập S ví dụ thì: 〈A, B, C〉, 〈C, A, B〉, 〈B, C, A〉, … chỉnh hợp khơng lặp chập S Điều tức liệt kê tất chỉnh hợp không lặp chập k tổ hợp chập k tính k! lần Vậy số tổ hợp chập k tập gồm n phần tử Lê Minh Hoàng ⎛n⎞ n! =⎜ ⎟ k!(n − k)! ⎝ k ⎠ Chuyên đề §2 PHƯƠNG PHÁP SINH (GENERATION) Phương pháp sinh áp dụng để giải tốn liệt kê tổ hợp đặt hai điều kiện sau thoả mãn: Có thể xác định thứ tự tập cấu hình tổ hợp cần liệt kê Từ biết đượccấu hình cấu hình cuối thứ tự Xây dựng thuật tốn từ cấu hình chưa phải cấu hình cuối, sinh cấu hình Phương pháp sinh mơ tả sau: 〈Xây dựng cấu hình đầu tiên〉; repeat 〈Đưa cấu hình có〉; 〈Từ cấu hình có sinh cấu hình còn〉; until 〈hết cấu hình〉; Thứ tự từ điển Trên kiểu liệu đơn giản chuẩn, người ta thường nói tới khái niệm thứ tự Ví dụ kiểu số có quan hệ: < 2; < 3; < 10; …, kiểu ký tự Char có quan hệ 'A' < 'B'; 'C' < 'c'… Xét quan hệ thứ tự toàn phần “nhỏ bằng” ký hiệu “≤“ tập hợp S, quan hệ hai thoả mãn bốn tính chất: Với ∀a, b, c ∈ S Tính phổ biến: Hoặc a ≤ b, b ≤ a; Tính phản xạ: a ≤ a Tính phản đối xứng: Nếu a ≤ b b ≤ a bắt buộc a = b Tính bắc cầu: Nếu có a ≤ b b ≤ c a ≤ c Trong trường hợp a ≤ b a ≠ b, ta dùng ký hiệu “, khỏi phải định nghĩa) Ví dụ quan hệ “≤” số nguyên kiểu vô hướng, liệt kê quan hệ thứ tự toàn phần Trên dãy hữu hạn, người ta xác định quan hệ thứ tự: Xét a[1 n] b[1 n] hai dãy độ dài n, phần tử a b có quan hệ thứ tự “≤” Khi a ≤ b Hoặc a[i] = b[i] với ∀i: ≤ i ≤ n Hoặc tồn số nguyên dương k: ≤ k < n để: a[1] = b[1] a[2] = b[2] ĐHSPHN 1999-2004 Bài toán liệt kê … a[k-1] = b[k-1] a[k] = b[k] a[k+1] < b[k+1] Trong trường hợp này, ta viết a < b Thứ tự gọi thứ tự từ điển dãy độ dài n Khi độ dài hai dãy a b không nhau, người ta xác định thứ tự từ điển Bằng cách thêm vào cuối dãy a dãy b phần tử đặc biệt gọi phần tử ∅ để độ dài a b nhau, coi phần tử ∅ nhỏ tất phần tử khác, ta lại đưa xác định thứ tự từ điển hai dãy độ dài Ví dụ: 〈1, 2, 3, 4〉 < 〈5, 6〉 〈a, b, c〉 < 〈a, b, c, d〉 'calculator' < 'computer' 2.1 SINH CÁC DÃY NHỊ PHÂN ĐỘ DÀI N Một dãy nhị phân độ dài n dãy x[1 n] x[i] ∈ {0, 1} (∀i : ≤ i ≤ n) Dễ thấy: dãy nhị phân x độ dài n biểu diễn nhị phân giá trị nguyên p(x) nằm đoạn [0, 2n - 1] Số dãy nhị phân độ dài n = số số tự nhiên ∈ [0, 2n - 1] = 2n Ta lập chương trình liệt kê dãy nhị phân theo thứ tự từ điển có nghĩa liệt kê dãy nhị phân biểu diễn số nguyên theo thứ tự 0, 1, …, 2n-1 Ví dụ: Khi n = 3, dãy nhị phân độ dài liệt kê sau: p(x) x 000 001 010 011 100 101 110 111 Như dãy 00…0 dãy cuối 11…1 Nhận xét dãy x = x[1 n] dãy có khơng phải dãy cuối cần liệt kê dãy nhận cách cộng thêm ( theo số có nhớ) vào dãy Ví dụ n = 8: Dãy có: 10010000 Dãy có: 10010111 cộng thêm 1: cộng thêm 1: +1 ⎯⎯⎯⎯ Dãy mới: 10010001 +1 ⎯⎯⎯⎯ Dãy mới: 10011000 Như kỹ thuật sinh cấu hình từ cấu hình mô tả sau: Xét từ cuối dãy đầu (xét từ hàng đơn vị lên), tìm số gặp Lê Minh Hoàng Chuyên đề Nếu thấy thay số số đặt tất phần tử phía sau vị trí Nếu khơng thấy thì tồn dãy số 1, cấu hình cuối Dữ liệu vào (Input): nhập từ file văn BSTR.INP chứa số nguyên dương n ≤ 100 Kết (Output): ghi file văn BSTR.OUT dãy nhị phân độ dài n BSTR.INP BSTR.OUT 000 001 010 011 100 101 110 111 P_1_02_1.PAS * Thuật toán sinh liệt kê dãy nhị phân độ dài n {$MODE DELPHI} (*This program uses 32-bit Integer [-231 231 - 1]*) program Binary_Strings; const InputFile = 'BSTR.INP'; OutputFile = 'BSTR.OUT'; max = 100; var x: array[1 max] of Integer; n, i: Integer; f: Text; begin Assign(f, InputFile); Reset(f); ReadLn(f, n); Close(f); Assign(f, OutputFile); Rewrite(f); FillChar(x, SizeOf(x), 0); {Cấu hình ban đầu x=00 0} repeat {Thuật toán sinh} for i := to n Write(f, x[i]); {In cấu hình tại} WriteLn(f); i := n; {x[i] phần tử cuối dãy, lùi dần i gặp số i = dừng} while (i > 0) and (x[i] = 1) Dec(i); if i > then {Chưa gặp phải cấu hình 11…1} begin x[i] := 1; {Thay x[i] số 1} FillChar(x[i + 1], (n - i) * SizeOf(x[1]), 0); {Đặt x[i+1] = x[i+2] = … = x[n] := 0} end; until i = 0; {Đã hết cấu hình} Close(f); end 2.2 LIỆT KÊ CÁC TẬP CON K PHẦN TỬ Ta lập chương trình liệt kê tập k phần tử tập {1, 2, …, n} theo thứ tự từ điền Ví dụ: với n = 5, k = 3, ta phải liệt kê đủ 10 tập con: 1.{1, 2, 3} 2.{1, 2, 4} 3.{1, 2, 5} 4.{1, 3, 4} 5.{1, 3, 5} 6.{1, 4, 5} 7.{2, 3, 4} 8.{2, 3, 5} 9.{2, 4, 5} 10.{3, 4, 5} Như tập (cấu hình khởi tạo) {1, 2, …, k} Cấu hình kết thúc {n - k + 1, n - k + 2, …, n} Nhận xét: Ta in tập cách in phần tử theo thứ tự tăng dần Biểu diễn tập dãy x[1 k] x[1] < x[2] < … < x[k] Ta nhận thấy giới ĐHSPHN 1999-2004 Bài toán liệt kê hạn (giá trị lớn nhận) x[k] n, x[k-1] n - 1, x[k-2] n - 2… Tổng quát: giới hạn x[i] = n - k + i; Còn tất nhiên, giới hạn x[i] (giá trị nhỏ x[i] nhận) x[i-1] + Như ta có dãy x đại diện cho tập con, x cấu hình kết thúc có nghĩa tất phần tử x đạt tới giới hạn trình sinh kết thúc, khơng ta phải sinh dãy x tăng dần thoả mãn vừa đủ lớn dãy cũ theo nghĩa khơng có tập k phần tử chen chúng thứ tự từ điển Ví dụ: n = 9, k = Cấu hình có x = 〈1, 2, 6, 7, 8, 9〉 Các phần tử x[3] đến x[6] đạt tới giới hạn nên để sinh cấu hình ta khơng thể sinh cách tăng phần tử số x[6], x[5], x[4], x[3] lên được, ta phải tăng x[2] = lên thành x[2] = Được cấu hình x = 〈1, 3, 6, 7, 8, 9〉 Cấu hình thoả mãn lớn cấu hình trước chưa thoả mãn tính chất vừa đủ lớn muốn ta lại thay x[3], x[4], x[5], x[6] giới hạn Tức là: x[3] := x[2] + = x[4] := x[3] + = x[5] := x[4] + = x[6] := x[5] + = Ta cấu hình x = 〈1, 3, 4, 5, 6, 7〉 cấu hình Nếu muốn tìm tiếp, ta lại nhận thấy x[6] = chưa đạt giới hạn trên, cần tăng x[6] lên x = 〈1, 3, 4, 5, 6, 8〉 Vậy kỹ thuật sinh tập từ tập có x xây dựng sau: Tìm từ cuối dãy lên đầu gặp phần tử x[i] chưa đạt giới hạn n - k + i Nếu tìm thấy: Tăng x[i] lên Đặt tất phần tử phía sau x[i] giới hạn Nếu khơng tìm thấy tức phần tử đạt giới hạn trên, cấu hình cuối Input: file văn SUBSET.INP chứa hai số nguyên dương n, k (1 ≤ k ≤ n ≤ 100) cách dấu cách Output: file văn SUBSET.OUT tập k phần tử tập {1, 2, …, n} SUBSET.INP 53 Lê Minh Hoàng SUBSET.OUT {1, 2, 3} {1, 2, 4} {1, 2, 5} {1, 3, 4} {1, 3, 5} {1, 4, 5} {2, 3, 4} {2, 3, 5} {2, 4, 5} {3, 4, 5} Chuyên đề P_1_02_2.PAS * Thuật toán sinh liệt kê tập k phần tử {$MODE DELPHI} (*This program uses 32-bit Integer [-231 231 - 1]*) program Combination; const InputFile = 'SUBSET.INP'; OutputFile = 'SUBSET.OUT'; max = 100; var x: array[1 max] of Integer; n, k, i, j: Integer; f: Text; begin Assign(f, InputFile); Reset(f); ReadLn(f, n, k); Close(f); Assign(f, OutputFile); Rewrite(f); for i := to k x[i] := i; {Khởi tạo x := (1, 2, …, k)} repeat {In cấu hình tại} Write(f, '{'); for i := to k - Write(f, x[i], ', '); WriteLn(f, x[k], '}'); {Sinh tiếp} i := k; {Xét từ cuối dãy lên tìm x[i] chưa đạt giới hạn n - k + i} while (i > 0) and (x[i] = n - k + i) Dec(i); if i > then {Nếu chưa lùi đến có nghĩa chưa phải cấu hình kết thúc} begin Inc(x[i]); {Tăng x[i] lên 1, Đặt phần tử đứng sau x[i] giới hạn nó} for j := i + to k x[j] := x[j - 1] + 1; end; until i = 0; {Lùi đến tận có nghĩa tất phần tử đạt giới hạn - hết cấu hình} Close(f); end 2.3 LIỆT KÊ CÁC HOÁN VỊ Ta lập chương trình liệt kê hốn vị {1, 2, …, n} theo thứ tự từ điển Ví dụ với n = 4, ta phải liệt kê đủ 24 hoán vị: 1.1234 7.2134 13.3124 19.4123 2.1243 8.2143 14.3142 20.4132 3.1324 9.2314 15.3214 21.4213 4.1342 10.2341 16.3241 22.4231 5.1423 11.2413 17.3412 23.4312 6.1432 12.2431 18.3421 24.4321 Như hoán vị 〈1, 2, …, n〉 Hoán vị cuối 〈n, n-1, …, 1〉 Hoán vị sinh phải lớn hoán vị tại, phải hoán vị vừa đủ lớn hốn vị theo nghĩa khơng thể có hoán vị khác chen chúng thứ tự Giả sử hoán vị x = 〈3, 2, 6, 5, 4, 1〉, xét phần tử cuối cùng, ta thấy chúng xếp giảm dần, điều có nghĩa cho dù ta có hốn vị phần tử nào, ta hoán vị bé hoán vị Như ta phải xét đến x[2] = 2, thay giá trị khác Ta thay giá trị nào?, hốn vị nhỏ hơn, khơng thể có x[1] = (phần tử sau khơng chọn vào giá trị mà phần tử trước chọn) Còn lại giá trị 4, 5, Vì cần hoán vị vừa đủ lớn nên ta chọn x[2] = Còn giá trị (x[3], x[4], x[5], x[6]) lấy tập {2, 6, 5, 1} Cũng tính vừa ĐHSPHN 1999-2004 Bài tốn liệt kê đủ lớn nên ta tìm biểu diễn nhỏ số gán cho x[3], x[4], x[5], x[6] tức 〈1, 2, 5, 6〉 Vậy hoán vị 〈3, 4, 1, 2, 5, 6〉 Ta có nhận xét qua ví dụ này: Đoạn cuối hoán vị xếp giảm dần, số x[5] = số nhỏ đoạn cuối giảm dần thoả mãn điều kiện lớn x[2] = Nếu đổi chỗ x[5] cho x[2] ta x[2] = đoạn cuối xếp giảm dần Khi muốn biểu diễn nhỏ cho giá trị đoạn cuối ta cần đảo ngược đoạn cuối Trong trường hợp hốn vị 〈2, 1, 3, 4〉 hoán vị 〈2, 1, 4, 3〉 Ta coi hốn vị 〈2, 1, 3, 4〉 có đoạn cuối giảm dần, đoạn cuối gồm phần tử (4) Vậy kỹ thuật sinh hoán vị từ hốn vị xây dựng sau: Xác định đoạn cuối giảm dần dài nhất, tìm số i phần tử x[i] đứng liền trước đoạn cuối Điều đồng nghĩa với việc tìm từ vị trí sát cuối dãy lên đầu, gặp số i thỏa mãn x[i] < x[i+1] Nếu tìm thấy số i Trong đoạn cuối giảm dần, tìm phần tử x[k] nhỏ thoả mãn điều kiện x[k] > x[i] Do đoạn cuối giảm dần, điều thực cách tìm từ cuối dãy lên đầu gặp số k thoả mãn x[k] > x[i] (có thể dùng tìm kiếm nhị phân) Đảo giá trị x[k] x[i] Lật ngược thứ tự đoạn cuối giảm dần (từ x[i+1] đến x[k]) trở thành tăng dần Nếu khơng tìm thấy tức toàn dãy giảm dần, cấu hình cuối Input: file văn PERMUTE.INP chứa số nguyên dương n ≤ 100 Output: file văn PERMUTE.OUT hoán vị dãy (1, 2, …, n) PERMUTE.INP PERMUTE.OUT 123 132 213 231 312 321 P_1_02_3.PAS * Thuật toán sinh liệt kê hoán vị {$MODE DELPHI} (*This program uses 32-bit Integer [-231 231 - 1]*) program Permutation; const InputFile = 'PERMUTE.INP'; OutputFile = 'PERMUTE.OUT'; max = 100; var n, i, k, a, b: Integer; x: array[1 max] of Integer; f: Text; procedure Swap(var X, Y: Integer); {Thủ tục đảo giá trị hai tham biến X, Y} var Temp: Integer; begin Temp := X; X := Y; Y := Temp; end; Lê Minh Hoàng 10 Chuyên đề begin Assign(f, InputFile); Reset(f); ReadLn(f, n); Close(f); Assign(f, OutputFile); Rewrite(f); for i := to n x[i] := i; {Khởi tạo cấu hình đầu: x[1] := 1; x[2] := 2; …, x[n] := n} repeat for i := to n Write(f, x[i], ' '); {In cấu hình hoán vị tại} WriteLn(f); i := n - 1; while (i > 0) and (x[i] > x[i + 1]) Dec(i); if i > then {Chưa gặp phải hoán vị cuối (n, n-1, …, 1)} begin k := n; {x[k] phần tử cuối dãy} while x[k] < x[i] Dec(k); {Lùi dần k để tìm gặp x[k] lớn x[i]} Swap(x[k], x[i]); {Đổi chỗ x[k] x[i]} a := i + 1; b := n; {Lật ngược đoạn cuối giảm dần, a: đầu đoạn, b: cuối đoạn} while a < b begin Swap(x[a], x[b]); {Đảo giá trị x[a] x[b]} Inc(a); {Tiến a lùi b, tiếp tục a, b chạm nhau} Dec(b); end; end; until i = 0; {Toàn dãy dãy giảm dần - không sinh tiếp - hết cấu hình} Close(f); end Bài tập: Bài Các chương trình xử lý khơng tốt trường hợp tầm thường, trường hợp n = chương trình liệt kê dãy nhị phân chương trình liệt kê hốn vị, trường hợp k = chương trình liệt kê tổ hợp, khắc phục điều Bài Liệt kê dãy nhị phân độ dài n coi liệt kê chỉnh hợp lặp chập n tập phần tử {0, 1} Hãy lập chương trình: Nhập vào hai số n k, liệt kê chỉnh hợp lặp chập k {0, 1, …, n -1} Hướng dẫn: thay hệ số hệ số n Bài Hãy liệt kê dãy nhị phân độ dài n mà cụm chữ số “01” xuất lần Bài Nhập vào danh sách n tên người Liệt kê tất cách chọn k người số n người Bài Liệt kê tất tập tập {1, 2, …, n} Có thể dùng phương pháp liệt kê tập dùng phương pháp liệt kê tất dãy nhị phân Mỗi số dãy nhị phân tương ứng với phần tử chọn tập Ví dụ với tập {1, 2, 3, 4} dãy nhị phân ĐHSPHN 1999-2004 306 Chuyên đề Enlarge; end; end; procedure Result; {In kết quả} var i, j, Count, W: Integer; f: Text; begin Assign(f, OutputFile); Rewrite(f); WriteLn(f, 'Optimal assignment:'); W := 0; Count := 0; for i := to m begin j := matchX[i]; if c[i, j] < maxC then begin Inc(Count); WriteLn(f, Count:3, ') x[', i, '] - y[', j, '] ', c[i, j]); W := W + c[i, j]; end; end; WriteLn(f, 'Cost: ', W); Close(f); end; begin Enter; Init; Solve; Result; end ĐHSPHN 1999-2004 Lý thuyết đồ thị 307 §13 BÀI TỐN TÌM BỘ GHÉP CỰC ĐẠI TRÊN ĐỒ THỊ 13.1 CÁC KHÁI NIỆM Xét đồ thị G = (V, E), ghép đồ thị G tập cạnh đơi khơng có đỉnh chung Bài tốn tìm ghép cực đại đồ thị tổng quát phát biểu sau: Cho đồ thị G, phải tìm ghép cực đại G (bộ ghép có nhiều cạnh nhất) Với ghép M đồ thị G, ta gọi: Những cạnh thuộc M gọi cạnh ghép hay cạnh đậm Những cạnh không thuộc M gọi cạnh chưa ghép hay cạnh nhạt Những đỉnh đầu mút cạnh đậm gọi đỉnh ghép, đỉnh lại gọi đỉnh chưa ghép Một đường (đường khơng có đỉnh lặp lại) gọi đường pha bắt đầu cạnh nhạt cạnh đậm, nhạt nằm nối tiếp xen kẽ Một chu trình (chu trình khơng có đỉnh lặp lại) gọi Blossom qua đỉnh, bắt đầu kết thúc cạnh nhạt dọc chu trình, cạnh đậm, nhạt nằm nối tiếp xen kẽ Đỉnh xuất phát chu trình (cũng đỉnh kết thúc) gọi đỉnh sở (base) Blossom Đường mở đường pha bắt đầu đỉnh chưa ghép kết thúc đỉnh chưa ghép Ví dụ: Với đồ thị G ghép M Hình 90: Đường (8, 1, 2, 5, 6, 4) đường pha Chu trình (2, 3, 4, 6, 5, 2) Blossom Đường (8, 1, 2, 3, 4, 6, 5, 7) đường mở Đường (8, 1, 2, 3, 4, 6, 5, 2, 1, 9) có cạnh đậm/nhạt xen kẽ đường pha (và tất nhiên khơng phải đường mở) khơng phải đường Hình 90: Đồ thị G ghép M Ta dễ dàng suy tính chất sau: Lê Minh Hồng 308 Chun đề Đường mở Blossom đường độ dài lẻ với số cạnh nhạt nhiều số cạnh đậm cạnh Trong Blossom, đỉnh đỉnh sở đỉnh ghép đỉnh ghép với đỉnh phải thuộc Blossom Vì Blossom chu trình nên Blossom, đỉnh đỉnh sở tồn hai đường pha từ đỉnh sở đến nó, đường kết thúc cạnh đậm đường kết thúc cạnh nhạt, hai đường pha hình thành cách dọc theo chu trình theo hai hướng ngược Như ví dụ Hình 90, đỉnh có hai đường pha đỉnh sở tới: (2, 3, 4) đường pha kết thúc cạnh đậm (2, 5, 6, 4) đường pha kết thúc cạnh nhạt 13.2 THUẬT TOÁN EDMONDS (1965) Cơ sở thuật toán định lý (C.Berge): Một ghép M đồ thị G cực đại không tồn đường mở M Thuật toán Edmonds: M := ∅; for (∀ đỉnh u chưa ghép) if 〈Tìm đường mở xuất phát từ u〉 then 〈Dọc đường mở: Loại bỏ cạnh đậm khỏi M; Thêm vào M cạnh nhạt〉; 〈Trả M ghép cực đại G〉; Điều khó thuật tốn Edmonds phải xây dựng thuật tốn tìm đường mở xuất phát từ đỉnh chưa ghép Thuật tốn xây dựng cách kết hợp thuật tốn tìm kiếm đồ thị với phép chập Blossom Xét đường pha xuất phát từ đỉnh x chưa ghép Những đỉnh đến từ x đường pha kết thúc cạnh nhạt gán nhãn “nhạt” (gọi tắt đỉnh nhạt), đỉnh đến từ x đường pha kết thúc cạnh đậm gán nhãn “đậm” (gọi tắt đỉnh đậm) Với Blossom, ta định nghĩa phép chập (shrink) phép thay đỉnh Blossom đỉnh Những cạnh nối đỉnh thuộc Blossom tới đỉnh v khơng thuộc Blossom thay cạnh nối đỉnh chập với v giữ ngun tính đậm/nhạt Có thể kiểm chứng nhận xét: sau phép chập, cạnh đậm đảm bảo ghép đồ thị mới: ĐHSPHN 1999-2004 Lý thuyết đồ thị 309 Shrink Shrink Blossom Blossom = đỉnh sở blossom = đỉnh chập từ blossom Hình 91: Phép chập Blossom Thuật tốn tìm đường mở xuất phát từ đỉnh x phát biểu sau Trước hết đỉnh xuất phát x gán nhãn đậm Tiếp theo thuật toán tìm kiếm đồ thị x, theo nguyên tắc: từ đỉnh đậm phép cạnh nhạt từ đỉnh nhạt cạnh đậm Mỗi thăm tới đỉnh, ta gán nhãn đậm/nhạt cho đỉnh tiếp tục thao tác tìm kiếm đồ thị bình thường Cũng trình tìm kiếm, phát thấy cạnh nhạt nối hai đỉnh đậm, ta dừng lại gán nhãn tiếp gặp tình trạng đỉnh có hai nhãn đậm/nhạt, trường hợp này, Blossom phát (xem tính chất Blossom) bị chập thành đỉnh, thuật toán bắt đầu lại với đồ thị trả lời câu hỏi: “có tồn đường mở xuất phát từ x hay khơng?" Nếu đường mở tìm khơng qua đỉnh chập ta việc tăng cặp dọc theo đường mở Nếu đường mở có qua đỉnh chập ta lại nở đỉnh chập thành Blossom để thay đỉnh chập đường mở đoạn đường xuyên qua Blossom: Expand Hình 92: Nở Blossom để dò đường xun qua Blossom Lê Minh Hoàng Expand 310 Chuyên đề Lưu ý Blossom bị chập, Blossom ảnh hưởng tới trình tìm đường mở phải chập để đảm bảo đường mở tìm đường Tuy nhiên việc cài đặt trực tiếp phép chập Blossom nở đỉnh rắc rối, đòi hỏi chương trình với độ phức tạp O(n4) Dưới ta trình bày phương pháp cài đặt hiệu với độ phức tạp O(n3), phương pháp cài đặt không phức tạp, yêu cầu phải hiểu rõ chất thuật tốn 13.3 THUẬT TỐN LAWLER (1973) Trong thuật tốn Edmonds, sau chập Blossom thành đỉnh đỉnh hồn tồn lại nằm Blossom bị chập tiếp Thuật toán Lawler quan tâm đến đỉnh chập cuối cùng, đại diện cho Blossom (Outermost Blossom), đỉnh chập cuối định danh (đánh số) đỉnh sở Blossom ngồi Cũng thao tác chập/nở nói mà ta cần mở rộng khái niệm Blossom, coi Blossom tập đỉnh nở từ đỉnh chập không đơn chu trình pha Xét Blossom B có đỉnh sở đỉnh r Với ∀v∈B, v ≠ r, ta lưu lại hai đường pha từ r tới v, đường kết thúc cạnh đậm đường kết thúc cạnh nhạt, có hai loại vết gãn cho đỉnh v (hai vết cập nhật trình tìm đường): S[v] đỉnh liền trước v đường pha kết thúc cạnh đậm, không tồn đường pha loại S[v] = T[v] đỉnh liền trước v đường pha kết thúc cạnh nhạt, khơng tồn đường pha loại T[v] = Bên cạnh hai nhãn S T, đỉnh v có thêm: Nhãn b[v] đỉnh sở Blossom chứa v Hai đỉnh u v thuộc Blossom ⇔ b[u] = b[v] Nhãn match[v] đỉnh ghép với đỉnh v Nếu v chưa ghép match[v] = Khi thuật tốn tìm đường mở đỉnh x chưa ghép phát biểu sau: Bước 1: (Init) Hàng đợi Queue dùng để chứa đỉnh đậm chờ duyệt, ban đầu gồm đỉnh đậm x Với đỉnh u, khởi gán b[u] = u match[u] = với ∀u Gán S[x] ≠ 0; với ∀u≠x, gán S[u] = Với ∀v: gán T[v] = Bước 2: (BFS) Lặp lại bước sau hàng đợi rỗng: Với đỉnh đậm u lấy từ Queue, xét cạnh nhạt (u, v): Nếu v chưa thăm: ĐHSPHN 1999-2004 Lý thuyết đồ thị 311 Nếu: v đỉnh chưa ghép → Tìm thấy đường mở kết thúc v, dừng v đỉnh ghép → thăm v → thăm match[v] đẩy match[v] vào Queue Lưu vết: Cập nhật hai nhãn S T Nếu v thăm Nếu v đỉnh nhạt b[v] = b[u] ⇒ bỏ qua Nếu v đỉnh đậm b[v] ≠ b[u] ta phát blossom chứa u v, đó: Phát đỉnh sở: Truy vết đường ngược từ hai đỉnh đậm u v theo hai đường pha nút gốc, chọn lấy đỉnh a đỉnh đậm chung gặp q trình truy vết ngược Khi Blossom phát có đỉnh sở a Gán lại vết: Gọi (a = i[1], i[2], …, i[p] = u) (a = j[1], j[2], …, j[q] = v) hai đường pha dẫn từ a tới u v Khi (a = i[1], i[2], …, i[p] = u, j[q] = v, j[q1], …, j[1] = a) chu trình pha từ a tới u v quay trở a Bằng cách dọc theo chu trình theo hai hướng ngược nhau, ta gán lại tất nhãn S T đỉnh chu trình Lưu ý không gán lại nhãn S T cho đỉnh k mà b[k] = a, với đỉnh k có b[k] ≠ a bắt buộc phải gán lại nhãn S T theo chu trình S[k] T[k] trước có hay chưa Chập Blossom: Xét đỉ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 đỉnh đậm (có nhãn S[v] ≠ 0) mà chưa duyệt tới (chưa đẩy vào Queue) đẩy v vào Queue chờ duyệt tiếp bước sau Bước 3: Nếu bước tìm đường mở trả đường mở, bước khơng tìm thấy đường mở hàng đợi rỗng kết luận khơng tìm thấy đường mở Sau số ví dụ trường hợp từ đỉnh đậm u xét cạnh nhạt (u, v): Trường hợp 1: v chưa thăm chưa ghép: S:2 u x T:3 u v x 2 T:1 T:1 ⇒ Tìm thấy đường mở Trường hợp 2: v chưa thăm ghép Lê Minh Hoàng S:2 v 312 Chuyên đề S:2 u x S:2 T:3 S:4 v u x v T:1 T:1 ⇒ Thăm v lẫn match[v], gán nhãn T[v] S[match[v]] Trường hợp 3: v thăm, đỉnh đậm thuộc blossom với u T:3 S:5 T:7 S:4 u x T:1 S:2 T:3 S:7 v T:5 S:6 b[.] = ⇒ Không xét, bỏ qua Trường hợp 4: v thăm, đỉnh đậm b[u] ≠ b[v] T:3 S:4 T:3 S:5 T:7 S:4 a x T:1 S:2 x T:3 S:6 T:1 S:2 T:3 S:7 T:5 S:6 ⇒ Phát Blossom, tìm đỉnh sở a = 3, gán lại nhãn S T dọc chu trình pha Đẩy hai đỉnh đậm 4, vào hàng đợi, Tại bước sau, duyệt tới đỉnh 6, tìm thấy đường mở kết thúc 8, truy vết theo nhãn S T tìm đường (1, 2, 3, 4, 5, 7, 6, 8) Tư tưởng phương pháp Lawler dùng nhãn b[v] thay cho thao tác chập trực tiếp Blossom, dùng nhãn S T để truy vết tìm đường mở, tránh thao tác nở Blossom Phương pháp dựa nhận xét: Mỗi tìm đường mở, đường mở xuyên qua Blossom ngồi chắn phải vào Blossom từ nút sở thoát khỏi Blossom cạnh nhạt 13.4 CÀI ĐẶT Ta cài đặt phương pháp Lawler với khuôn dạng Input/Output sau: Input: file văn GMATCH.INP ĐHSPHN 1999-2004 Lý thuyết đồ thị 313 Dòng 1: Chứa hai số n, m số cạnh số đỉnh đồ thị cách dấu cách (n ≤ 1000) m dòng tiếp theo, dòng chứa hai số u, v tượng trưng cho cạnh (u, v) đồ thị Output: file văn GMATCH.OUT, ghi ghép cực đại tìm 10 GMATCH.INP 10 11 12 16 24 28 34 36 56 59 10 78 79 GMATCH.OUT 1) 2) 3) 4) 10 5) Chương trình sửa đổi chút mơ hình cài đặt dựa vào nhận xét: v đỉnh đậm v = x match[v] đỉnh nhạt, để kiểm tra đỉnh v có phải đỉnh đậm hay khơng ta kiểm tra biểu thức: (v = x) or (match[v] 0) and (T[match[v]] 0) = TRUE Nếu v đỉnh đậm S[v] = match[v] Các biến sử dụng với vai trò sau: match[v] đỉnh ghép với đỉnh v b[v] đỉnh sở Blossom chứa v T[v] đỉnh liền trước v đường pha từ đỉnh xuất phát tới v kết thúc cạnh nhạt, T[v] = trình BFS chưa xét tới v InQueue[v] biến Boolean, InQueue[v] = True ⇔ v đỉnh đậm đẩy vào Queue để chờ duyệt start finish: Nơi bắt đầu kết thúc đường mở P_4_13_1.PAS * Phương pháp Lawler áp dụng cho thuật toán Edmonds {$MODE DELPHI} (*This program uses 32-bit Integer [-231 231 - 1]*) program MatchingInGeneralGraph; const InputFile = 'GMATCH.INP'; OutputFile = 'GMATCH.OUT'; max = 1000; var a: array[1 max, max] of Boolean; match, Queue, b, T: array[1 max] of Integer; InQueue: array[1 max] of Boolean; n, Front, Rear, start, finish: Integer; procedure Enter; var i, m, u, v: Integer; f: Text; begin Assign(f, InputFile); Reset(f); Lê Minh Hoàng 314 Chuyên đề FillChar(a, SizeOf(a), False); ReadLn(f, n, m); for i := to m begin ReadLn(f, u, v); a[u, v] := True; a[v, u] := True; end; Close(f); end; procedure Init; {Khởi tạo ghép rỗng} begin FillChar(match, SizeOf(match), 0); end; procedure InitBFS; {Thủ tục gọi để khởi tạo trước tìm đường mở xuất phát từ start} var i: Integer; begin {Hàng đợi gồm đỉnh đậm start} Front := 1; Rear := 1; Queue[1] := start; FillChar(InQueue, SizeOf(InQueue), False); InQueue[start] := True; {Các nhãn T khởi gán = 0} FillChar(T, SizeOF(T), 0); {Nút sở outermost blossom chứa i khởi tạo i} for i := to n b[i] := i; finish := 0; {finish = nghĩa chưa tìm thấy đường mở} end; procedure Push(v: Integer); {Đẩy đỉnh đậm v vào hàng đơi} begin Inc(Rear); Queue[Rear] := v; InQueue[v] := True; end; function Pop: Integer; {Lấy đỉnh đậm khỏi hàng đợi, trả kết hàm} begin Pop := Queue[Front]; Inc(Front); end; {Khó phương pháp Lawler thủ tục này: Thủ tục xử lý gặp cạnh nhạt nối hai đỉnh đậm p, q} procedure BlossomShrink(p, q: Integer); var i, NewBase: Integer; Mark: array[1 max] of Boolean; {Thủ tục tìm nút sở cách truy vết ngược theo đường pha từ p q} function FindCommonAncestor(p, q: Integer): Integer; var InPath: array[1 max] of Boolean; begin FillChar(InPath, SizeOf(Inpath), False); repeat {Truy vết từ p} p := b[p]; {Nhảy tới nút sở Blossom chứa p, phép nhảy để tăng tốc độ truy vết} InPath[p] := True; {Đánh dấu nút đó} if p = start then Break; {Nếu truy đến nơi xuất phát dừng} p := T[match[p]]; {Nếu chưa đến start truy lùi tiếp hai bước, theo cạnh đậm theo cạnh nhạt} until False; ĐHSPHN 1999-2004 Lý thuyết đồ thị 315 repeat {Truy vết từ q, tương tự p} q := b[q]; if InPath[q] then Break; {Tuy nhiên chạm vào đường pha p dừng ngay} q := T[match[q]]; until False; FindCommonAncestor := q; {Ghi nhận đỉnh sở mới} end; procedure ResetTrace(x: Integer); {Gán lại nhãn vết dọc đường pha từ start tới x} var u, v: Integer; begin v := x; while b[v] NewBase {Truy vết đường pha từ start tới đỉnh đậm x} begin u := match[v]; Mark[b[v]] := True; {Đánh dấu nhãn blossom đỉnh đường đi} Mark[b[u]] := True; v := T[u]; if b[v] NewBase then T[v] := u; {Chỉ đặt lại vết T[v] b[v] nút sở mới} end; end; begin {BlossomShrink} FillChar(Mark, SizeOf(Mark), False); {Tất nhãn b[v] khởi tạo chưa bị đánh dấu} NewBase := FindCommonAncestor(p, q); {xác định nút sở} ResetTrace(p); ResetTrace(q); {Gán lại nhãn} if b[p] NewBase then T[p] := q; if b[q] NewBase then T[q] := p; {Chập blossom ⇔ gán lại nhãn b[i] cho đỉnh i blossom b[i] bị đánh dấu} for i := to n if Mark[b[i]] then b[i] := NewBase; {Xét đỉnh đậm i chưa đưa vào Queue nằm Blossom mới, đẩy i Queue để chờ duyệt sau} for i := to n if not InQueue[i] and (b[i] = NewBase) then Push(i); end; {Thủ tục tìm đường mở} procedure FindAugmentingPath; var u, v: Integer; begin InitBFS; {Khởi tạo} repeat {BFS} u := Pop; {Rút đỉnh đậm u khỏi hàng đợi} {Xét đỉnh v kề u qua cạnh nhạt mà v không nằm blossom với u} for v := to n if (a[u, v]) and (match[u] v) and (b[u] b[v]) then if (v = start) or (match[v] 0) and (T[match[v]] 0) then {Nếu v đỉnh đậm} BlossomShrink(u, v) {thì gán lại vết, chập blossom } else if T[v] = then {Nếu v đỉnh nhạt chưa thăm tới} if match[v] = then {Nếu v chưa ghép nghĩa tìm đường mở kết thúc v, thoát} begin T[v] := u; finish := v; Exit; end else {Nếu v ghép ghi vết đường đi, thăm v, thăm ln match[v] đẩy match[v] vào Queue} begin T[v] := u; Push(match[v]); Lê Minh Hoàng 316 Chuyên đề end; until Front > Rear; end; procedure Enlarge; {Nới rộng ghép đường mở start, kết thúc finish} var v, next: Integer; begin repeat v := T[finish]; next := match[v]; match[v] := finish; match[finish] := v; finish := next; until finish = 0; end; procedure Solve; {Thuật toán Edmonds} var u: Integer; begin for u := to n if match[u] = then begin start := u; {Với đỉnh chưa ghép start} FindAugmentingPath; {Tìm đường mở start} if finish then Enlarge; {Nếu thấy nới rộng ghép theo đường mở này} end; end; procedure Result; {In ghép tìm được} var u, count: Integer; f: Text; begin Assign(f, OutputFile); Rewrite(f); count := 0; for u := to n if match[u] > u then {Vừa tránh in lặp cạnh (u, v) (v, u), vừa loại đỉnh không ghép (match[.]=0)} begin Inc(count); WriteLn(f, count, ') ', u, ' ', match[u]); end; Close(f); end; begin Enter; Init; Solve; Result; end 13.5 ĐỘ PHỨC TẠP TÍNH TỐN Thủ tục BlossomShrink có độ phức tạp O(n) Thủ tục FindAugmentingPath cần không n lần gọi thủ tục BlossomShrink, cộng thêm chi phí thuật tốn tìm kiếm theo chiều rộng, có độ phức tạp O(n2) Phương pháp Lawler cần không n lần gọi thủ tục FindAugmentingPath nên có độ phức tạp tính tốn O(n3) ĐHSPHN 1999-2004 Lý thuyết đồ thị 317 Cho đến nay, phương pháp tốt để giải tốn tìm ghép tổng quát đồ thị biết đến Micali Vazizani (1980), có độ phức tạp tính tốn O thể tham khảo tài liệu khác Lê Minh Hồng ( ) n.m Bạn có TÀI LIỆU ĐỌC THÊM Dưới hai sách nói kinh điển mà hầu hết tài liệu thuật tốn trích dẫn nhiều từ hai sách Các bạn nên tìm cách để đọc Title: The Art of Computer Programming, 3rd edition Author: Donald E Knuth Volume 1: Fundamental Algorithms, ISBN: 0-201-89683-4 Volume 2: Seminumerical Algorithms, ISBN: 0-201-89684-2 Volume 3: Sorting and Searching, ISBN: 0-201-89685-0 Volume 4: Combinatorial Algorithms (in preparation) Volume 5: Syntactic Algorithms (in preparation) Publisher: Addison-Wesley, 1998 Title: Introduction to Algorithms, 2nd edition, ISBN: 0262032937 Authors: Thomas H.Cormen, Charles E.Leiserson, Ronald L.Rivest Publisher: The MIT Press, 2001 Ngoài bạn tham khảo thêm sách sau đây: Alfred V Aho, Jeffrey D Ullman, John E Hopcroft Data Structures and Algorithms, ISBN: 0201000237, Addison Wesley, 1983 Robert Sedgewick Algorithms 2nd edition, ISBN: 0201066734, Addison Wesley, 1988 Mikhail J Atallah Ed Algorithms and Theory of Computation Handbook, ISBN: 0849326494, CRC Press, 1998 ... Bài Các chương trình xử lý khơng tốt trường hợp tầm thường, trường hợp n = chương trình liệt kê dãy nhị phân chương trình liệt kê hốn vị, trường hợp k = chương trình liệt kê tổ hợp, khắc phục... thực tế đòi hỏi phải có tổng hợp, pha trộn nhiều thuật tốn, nhiều kỹ thuật có lời giải tốt Không lạm dụng kỹ thuật không xem thường phương pháp bắt tay vào giải toán tin học Thuật tốn quay lui khơng... nguồn gốc tên gọi thuật toán quay lui” Bài tập: Bài Một số chương trình xử lý khơng tốt trường hợp tầm thường (n = k = 0), khắc phục lỗi Bài Viết chương trình liệt kê chỉnh hợp lặp chập k n phần