Bài toán dãy ABC

Một phần của tài liệu phát triển các kỹ thuật nhánh cận và ứng dụng (Trang 45 - 61)

3. Hƣớng nghiên cứu của đề tài

2.5. Bài toán dãy ABC

Cho trƣớc một số nguyên dƣơng N (N < 100), hãy tìm một xâu chỉ gồm các ký tự A, B, C thoả mãn 3 điều kiện:

Có độ dài N

Hai đoạn con bất kỳ liền nhau đều khác nhau (đoạn con là một dãy ký tự liên tiếp của xâu)

Có ít ký tự C nhất.

Dữ liệu: tệp văn bản ABC.INP

o Chứa số nguyên dƣơng n < 100

Kết quả: tệp văn bản ABC.OUT

o Ghi xâu tìm đƣợc

ABC.INP ABC.OUT

10 ABACABCBAB

"C" Letter Count : 2

Đây là một bài tập liệt kê. Sau đây là cách áp dụng kĩ thuật nhánh cận: Nếu dãy X[1..n] thoả mãn 2 đoạn con bất kỳ liền nhau đều khác nhau, thì trong 4 ký tự liên tiếp bất kỳ bao giờ cũng phải có 1 ký tự “C”. Nhƣ vậy với một dãy con gồm k ký tự liên tiếp của dãy X thì số ký tự C trong dãy con đó bắt buộc phải ≥ k div 4.

Tại bƣớc thử chọn X[i], nếu ta đã có T[i] ký tự “C” trong đoạn đã chọn từ X[1] đến X[i], thì cho dù các bƣớc đệ quy tiếp sau làm tốt nhƣ thế nào

41

Số hóa bởi Trung tâm Học liệu – Đại học Thái Nguyên http://www.lrc-tnu.edu.vn/ chăng nữa, số ký tự “C” sẽ phải chọn thêm bao giờ cũng ≥ (n - i) div 4. Tức là nếu theo phƣơng án chọn X[i] nhƣ thế này thì số ký tự C trong dãy kết quả (khi chọn đến X[n]) cho dù có là Q tốt đến đâu cũng ≥ T[i] + (n - 1) div 4. Ta dùng con số này để đánh giá nhánh cận, nếu nó nhiều hơn số ký tự “C” trong BestSolution thì chắc chắn có làm tiếp cũng chỉ đƣợc một cấu hình tồi tệ hơn, ta bỏ qua ngay cách chọn này và thử phƣơng án khác.

2.5.3. Chương trình minh họa

Program ABC_STRING; const InputFile = 'ABC.INP'; OutputFile = 'ABC.OUT'; max = 100; var N, MinC: Integer;

X, Best: array[1..max] of ‘A’..’C’; T: array[0..max] of Integer;

f: Text;

function Same(i, l: Integer): Boolean; var j, k: Integer; begin j := i - l; for k := 0 to l - 1 do if X[i - k] <> X[j - k] then begin

Same := False; Exit; end;

Same := True; end;

function Check(i: Integer): Boolean; var l: Integer;

42

Số hóa bởi Trung tâm Học liệu – Đại học Thái Nguyên http://www.lrc-tnu.edu.vn/

begin

for l := 1 to i div 2 do if Same(i, l) then begin

Check := False; Exit; end; Check := True; end; procedure KeepResult; begin MinC := T[N]; Best := X; end;

procedure Attempt(i: Integer); var j: 'A’..’C’; begin for j := 'A' to 'C' do begin X[i] := j; if Check(i) then begin if j = 'C' then T[i] := T[i - 1] + 1 else T[i] := T[i - 1];

if T[i] + (N - i) div 4 < MinC then if i = N then KeepResult else Attempt(i + 1); end; end; end;

43

Số hóa bởi Trung tâm Học liệu – Đại học Thái Nguyên http://www.lrc-tnu.edu.vn/

procedure PrintResult; var i: Integer;

begin

for i := 1 to N do Write(f, Best[i]); WriteLn(f);

WriteLn(f, ,’C Letter Count : ‘, MinC); end;

begin

Assign(f, InputFile); Reset(f); ReadLn(f, N);

Close(f);

Assign(f, OutputFile); Rewrite(f); T[0] := 0; MinC := N; Attempt(1); PrintResult; Close(f); end.

Nếu ta thay bài toán là tìm xâu ít ký tự 'B' nhất mà vẫn viết chƣơng trình tƣơng tự nhƣ trên thì chƣơng trình sẽ chạy chậm hơn chút ít. Lý do: thủ tục Attempt ở trên sẽ thử lần lƣợt các giá trị 'A', 'B', rồi mới đến 'C'. Có nghĩa ngay trong cách tìm, nó đã tiết kiệm sử dụng ký tự 'C' nhất nên trong phần lớn các bộ dữ liệu nó nhanh chóng tìm ra lời giải hơn so với bài toán tƣơng ứng tìm xâu ít ký tự 'B' nhất. Chính vì vậy mà nếu nhƣ đề bài yêu cầu ít ký tự 'B' nhất ta cứ lập chƣơng trình làm yêu cầu ít ký tự 'C' nhất, chỉ có điều khi in kết quả, ta đổi vai trò 'B', 'C' cho nhau. Đây là một ví dụ cho thấy sức mạnh của thuật toán quay lui khi kết hợp với kỹ thuật nhánh cận, nếu viết quay lui thuần tuý hoặc đánh giá nhánh cận không tốt thì với N = 100, tôi cũng không đủ kiên nhẫn để đợi chƣơng trình cho kết quả (chỉ biết rằng > 3 giờ). Trong khi

44

Số hóa bởi Trung tâm Học liệu – Đại học Thái Nguyên http://www.lrc-tnu.edu.vn/ đó khi N = 100, với chƣơng trình trên chỉ chạy hết hơn 1 giây cho kết quả là xâu 27 ký tự 'C'.

2.6. Kết luận

Hiệu quả của thuật toán nhánh cận phụ thuộc rất nhiều vào việc xây dựng hàm tính cận. Việc xây dựng hàm tính cận dƣới lại phụ thuộc vào cách xây dựng thủ tục duyệt các phƣơng án của bài toán (đƣợc gọi là thủ tục phân nhánh). Trên đây chúng ta là cách xây dựng cận khá đơn giản cho các bài toán thông dụng của tối ƣu tổ hợp. Các chƣơng trình đƣợc cài đặt theo các thuật toán đó, tuy rằng làm việc tốt hơn nhiều so với duyệt toàn bộ, nhƣng cũng chỉ có thể áp dụng để giải các bài toán với kích thƣớc nhỏ. Muốn giải đƣợc các bài toán đặt ra với kích thƣớc lớn hơn cần có cách đánh giá cận tốt hơn. Trong chƣơng tiếp theo ta sẽ tìm hiểu cách áp dụng phát triển thuật kĩ thuật nhánh cận vào bài toán ngƣời du lịch để có thể giải bài toán ở kích thƣớc lớn hơn.

45

Số hóa bởi Trung tâm Học liệu – Đại học Thái Nguyên http://www.lrc-tnu.edu.vn/

CHƢƠNG 3. ỨNG DỤNG PHÁT TRIỂN NHÁNH CẬN

Kĩ thuật nhánh cận là một trong những phƣơng pháp giải chủ yếu của tối ƣu tổ hợp. Nhƣ trong chƣơng trƣớc đã thấy, tƣ tƣởng cơ bản của nó là trong quá trình tìm kiếm lời giải ta sẽ phân hoạch tập các phƣơng án của bài toán ra thành hai hay nhiều tập con đƣợc biểu diễn nhƣ là các nút của cây tìm kiếm và cố gắng bằng phép đánh giá cận cho các nút, tìm cách loại bỏ những nhánh của cây tìm kiếm (những tập con các phƣơng án của bài toán) mà ta biết chắc chắn là không chứa phƣơng án tối ƣu. Mặc dù trong tình huống tồi nhất thuật toán sẽ trở thành duyệt toàn bộ, nhƣng trong nhiều trƣờng hợp cụ thể, kỹ thuật đó cho phép rút ngắn đƣợc một cách đáng kể quá trình tìm kiếm. Chƣơng này sẽ trình bày một cách thể hiện khác những tƣ tƣởng của kĩ thuật nhánh cận vào việc xây dựng thuật toán giải bài toán ngƣời du lịch.

Xét bài toán ngƣời du lịch phát biểu trong mục trƣớc.

Gọi { : , 1,2,..., } là ma trận chi phí. Mỗi hành trình của ngƣời du

lịch Tx1 → Tx2 → … → Txn → Tx1.

có thể viết lại dƣới dạng:

(x1, x2), (x2, x3), …, (xn, x1),

trong đó mỗi thành phần (x(j-1), xj) sẽ đƣợc gọi là một cạnh của hành trình.

Trong bài toán ngƣời du lịch khi tiến hành tìm kiếm lời giải chúng ta sẽ phân tập các hành trình ra thành hai tập con: một tập gồm những hành trình chứa một cạnh (ỉ, j) nào đó còn tập kia gồm những hành trình không chứa cạnh này. Ta gọi việc làm đó là phân nhánh và mỗi tập con nói trên sẽ đƣợc gọi là một nhánh hay một nút của cây tìm kiếm. Việc phân nhánh đƣợc minh hoạ bởi cây tìm kiếm:

46

Số hóa bởi Trung tâm Học liệu – Đại học Thái Nguyên http://www.lrc-tnu.edu.vn/ Hình 3. Mô hình phân nhánh

Việc phân nhánh sẽ đƣợc thực hiện dựa trên một quy tắc Heuristic nào đó cho phép ta rút ngắn quá trình tìm kiếm phƣơng án tối ƣu. Sau khi phân nhánh ta sẽ tính cận dƣới của giá trị hàm mục tiêu trên mỗi một trong hai tập con nói trên. Việc tìm kiếm sẽ đƣợc tiếp tục trên tập con có giá trị cận dƣới nhỏ hơn. Thủ tục này sẽ đƣợc tiếp tục cho đến khi thu đƣợc một hành trình đầy đủ, tức là một phƣơng án của bài toán ngƣời du lịch. Khi đó ta chỉ cần xét những tập con các phƣơng án nào có cận dƣới nhỏ hơn giá trị hàm mục tiêu tại phƣơng án đã tìm đƣợc. Quá trình phân nhánh và tính cận trên tập các phƣơng án của bài toán thông thƣờng cho phép rút ngắn một cách đáng kể quá trình tìm kiếm do ta loại đƣợc khá nhiều tập con chắc chắn không chứa phƣơng án tối ƣu.

Một kỹ thuật cơ bản nữa của thuật toán là tính cận dƣới sẽ đƣợc xây dựng dựa trên thủ tục rút gọn mà chúng ta sẽ trình bày dƣới đây. Sau đó những bƣớc chính của thuật toán nhánh cận sẽ đƣợc mô tả thông qua một ví dụ số và cuối cùng ta sẽ trình bày sơ đồ nguyên tắc của thuật toán.

3.1. Thủ tục rút gọn.

Rõ ràng tổng chi phí của một hành trình của ngƣời du lịch sẽ chứa đúng một phần tử của mỗi dòng và đúng một phần tử của mỗi cột trong ma trận chi phí . Do đó, nếu ta trừ bớt mỗi phần tử của một dòng (hay cột) của ma trận c đi cùng một số thì độ dài của tất cả các hành trình sẽ cùng giảm đi , vì thế hành trình tối ƣu cũng

Tập tất cả các hành trình

Hành trình chứa (i,j)

Hành trình không chứa (i,j)

47

Số hóa bởi Trung tâm Học liệu – Đại học Thái Nguyên http://www.lrc-tnu.edu.vn/ sẽ không thay đổi. Vì vậy nếu ta tiến hành trừ bớt các phần tử của mỗi dòng và mỗi cột đi một hằng số sao cho thu đƣợc ma trận gồm các phần tử không âm mà trong mỗi dòng và mỗi cột của nó đều có ít nhất một số 0 thì tổng các hằng số trừ đó sẽ cho ta cận dƣới của mọi hành trình. Thủ tục trừ bớt này sẽ đƣợc gọi là thủ tục rút gọn, các hằng số trừ ở mỗi dòng (cột) sẽ đƣợc gọi là hằng số rút gọn theo dòng (cột), còn ma trận thu đƣợc sẽ gọi là ma trận rút gọn. Hàm số sau đây cho phép rút gọn một ma trận A kích thƣớc là k x k đồng thời tính tổng các hằng số rút gọn (để tiện trình bày, các tham số có mặt trong các hàm và các thủ tục PASCAL dƣới đây đƣợc giả thiết khai báo sao cho phù hợp):

function Reduce(A, k): real; (** Thủ tục rút gọn ma trận **) begin

sum := 0;

for i := 1 to k do (* k - kích thước của A *) begin

r[i]:= <phần tử nhỏ nhất trong dòng i>; if r[i] > 0 then begin

<Bớt mỗi phần tử của dòng i đi r[i]>; sum := sum + r[i];

end; end; for j := 1 to k do begin s[j] := <phần tử nhỏ nhất trong cột j>; if s[j] > 0 then begin

<Bớt mỗi phần tử của cột j đi s[j]>; sum := sum + s[j];

48

Số hóa bởi Trung tâm Học liệu – Đại học Thái Nguyên http://www.lrc-tnu.edu.vn/

end; end;

Reduce := sum; end;

Thí dụ: Ta có ma trận chi phí của bài toán ngƣời du lịch với n = 6 thành phố sau 1 2 3 4 5 6 r[i 1 ∞ 3 93 13 33 9 3 2 4 ∞ 77 42 21 16 4 3 45 17 ∞ 36 16 28 16 4 39 90 80 ∞ 56 7 7 5 28 46 88 33 ∞ 25 25 6 3 88 18 46 92 ∞ 3 s[j] 0 0 15 8 0 0

Đầu tiên trừ bớt mỗi phần tử của các dòng 1, 2, 3, 4, 5, 6 cho các hằng số rút gọn tƣơng ứng là 3, 4, 16, 7, 25, 3, sau đó trong ma trận thu đƣợc, trừ bớt các phần tử của các cột 3 và 4 cho các hằng số rút gọn tƣơng ứng là 15 và 8, ta thu đƣợc ma trận rút gọn sau 1 2 3 4 5 6 1 ∞ 0 75 2 30 6 2 0 ∞ 58 30 17 12 3 29 1 ∞ 12 0 12 4 32 83 58 ∞ 49 0 5 3 21 48 0 ∞ 0 6 0 85 0 35 89 ∞

Tổng các hằng số rút gọn là 81, vì vậy cân dƣới cho tất cả các hành trình là 81 (nghĩa là không thể tìm đƣợc hành trình có tổng chi phí nhỏ hơn 81).

Bây giờ, ta xét cách phân tập các phƣơng án ra thành hai tập. Giả sử là ta chọn cạnh (6, 3) để phân nhánh. Khi đó tập các hành trình sẽ đƣợc phân thành hai

49

Số hóa bởi Trung tâm Học liệu – Đại học Thái Nguyên http://www.lrc-tnu.edu.vn/ tập con, một tập là các hành trình chứa cạnh (6, 3), còn tập kia là các hành trình không chứa cạnh (6, 3). Vì biết cạnh (6, 3) là không đƣợc tham gia vào hành trình, nên ta có thể cấm việc đi theo cạnh này bằng cách đặt c63 = ∞. Ma trận thu đƣợc sẽ có thể rút gọn bằng cách bớt mỗi phần tử của cột 3 đi 48 và không bớt gì các phần tử của dòng 6. Nhƣ vậy ta thu đƣợc cận dƣới cho các hành trình không chứa cạnh (6, 3) là 81 + 48 = 129. Còn đối với tập các hành trình chứa cạnh (6, 3) ta phải loại dòng 6 và cột 3 khỏi ma trận tƣơng ứng với nó, bởi vì đã đi theo cạnh (6, 3) thì không thể đi từ 6 sang bất cứ nơi nào khác và cũng không đƣợc phép đi từ bất cứ đâu vào 3. Kết quả ta thu đƣợc ma trận với bậc giảm đi 1. Ngoài ra, do đã đi theo cạnh (6, 3) nên không đƣợc phép đi từ 3 đến 6 nữa, vì vậy ta cần cấm đi theo cạnh (3, 6) bằng cách đặt c36 = ∞. Cây tìm kiếm cho đến bƣớc này, có dạng cho trong hình 5 sau đây: 1 2 4 5 6 1 2 3 4 5 6 1 ∞ 0 2 30 6 1 ∞ 0 27 2 30 6 2 0 ∞ 30 17 12 2 0 ∞ 10 30 17 12 3 29 1 12 0 ∞ 3 29 1 ∞ 12 0 12 4 32 83 ∞ 49 0 4 32 83 10 ∞ 49 0 5 3 21 0 ∞ 0 5 3 21 0 0 ∞ 0 6 0 85 ∞ 35 89 ∞ Hình 4. Minh họa rút gọn hành trình

Cạnh (6, 3) đƣợc chọn để phân nhánh vì phân nhánh theo nó ta thu đƣợc cận dƣới của nhánh bên phải là lớn nhất so với việc phân nhánh theo các cạnh khác. Quy tắc này sẽ đƣợc sử dụng để phân nhánh ở mỗi đỉnh của cây tìm kiếm. Trong

Tập tất cả các hành trình Hành trình chứa (6,3) Hành trình không chứa (6,3) Cận dƣới = 81 Cận dƣới = 129 Cận dƣới = 81

50

Số hóa bởi Trung tâm Học liệu – Đại học Thái Nguyên http://www.lrc-tnu.edu.vn/ quá trình tìm kiếm chúng ta luôn đi theo nhánh bên trái trƣớc. Nhánh bên trái sẽ có ma trận rút gọn với bậc giảm đi một. Trong ma trận của nhánh bên phải ta thay một số bởi ∞, và có thể rút gọn thêm đƣợc ma trận này khi tính lại các hằng số rút gọn theo dòng và cột tƣơng ứng với cạnh phân nhánh, nhƣng kích thƣớc của ma trận vẫn giữ nguyên.

Do cạnh cần chọn để phân nhánh phải là cạnh làm tăng cận dƣới của nhánh bên phải lên nhiều nhất, nên để tìm nó ta sẽ chọn số không nào trong ma trận mà khi thay nó bởi ∞ sẽ cho ta tổng hằng số rút gọn theo dòng và cột chứa nó là lớn nhất. Ta có thủ tục sau đây để chọn cạnh phân nhánh (r,c):

3.2. Thủ tục chọn cạnh phân nhánh (r,c)

Đầu vào: Ma trận rút gọn A kích thƣớc k x k.

Đầu ra : Cạnh phân nhánh (r, c) và tổng hằng số rút gọn theo dòng r cột c là beta. procedure BestEdge(A,k,r,c,beta); [5] (* Thủ tục phân nhánh *) begin beta := -∞; for i := 1 to k do for j := 1 to k do

if a[i,j] = 0 then begin

minr := <phần tử nhỏ nhất trên dòng i khác với a[i,j]>;

mine := <phần tử nhỏ nhất trên cột j khác với a[i,j]>;

total := minr + mine;

if total > beta then begin beta := total;

r := i;(*chỉ số dòng của cạnh tốt nhất*)

51

Số hóa bởi Trung tâm Học liệu – Đại học Thái Nguyên http://www.lrc-tnu.edu.vn/

c := j; (* chỉ số cột của cạnh tốt nhất *)

end; end;

end;

Trong ma trận rút gọn 5x5 của nhánh bên trái ở hình 5, số không ở vị trí (4, 6) sẽ cho tổng hằng số rút gọn là 32 (theo dòng 4 là 32, cột 6 là 0). Đây là giá trị lớn nhất đối với các số không của ma trận này. Vì vậy, việc phân nhánh tiếp theo sẽ dựa vào cạnh (4, 6). Khi đó cận dƣới của nhánh bên phải tƣơng ứng với tập các hành trình đi qua cạnh (6,3) nhƣng không đi qua (4,6) sẽ là 81 + 32 = 113. Còn nhánh bên trái sẽ tƣơng ứng với ma trận 4 x 4, vì rằng ta phải loại bỏ dòng 4 và cột 6. Tình huống phân nhánh này đƣợc mô tả trong hình 6.

1 2 4 5 1 2 4 5 6 1 ∞ 0 2 30 1 ∞ 0 2 30 6 2 0 ∞ 30 17 2 0 ∞ 30 17 12 3 29 1 12 0 3 29 1 12 0 ∞ 5 3 21 0 ∞ 4 0 51 ∞ 17 ∞ 5 3 21 0 ∞ 0 Hình 5. Minh họa rút gọn hành trình 2

Tiếp tục, ta lại phân nhánh từ đỉnh bên trái bằng cách sử dụng cạnh (2, 1), vì số không ở vị trí này có tổng các hằng số rút gọn là 17 + 3 = 20 (theo

Tập các hành trình qua cạnh (6,3) Hành trình chứa (6,3), (4,6) Hành trình chứa (6,3) không chứa (4,6) Cận dƣới = 81 Cận dƣới = 113

Một phần của tài liệu phát triển các kỹ thuật nhánh cận và ứng dụng (Trang 45 - 61)

Tải bản đầy đủ (PDF)

(61 trang)