Tải bản đầy đủ - 0 (trang)
Thuật toán quay lui

Thuật toán quay lui

Tải bản đầy đủ - 0trang

- Mỗi cấu hình được xây dựng bằng cách xây dựng từng phần tử,

- Mỗi phần tử được chọn bằng cách thử tất cả các khả năng.

Giả sử cấu hình cần liệt kê có dạng x1..n, khi đó thuật tốn quay lui sẽ xét tất cả các giá trị

x1 có thể nhận, thử cho x1 nhận lần lượt các giá trị đó. Với mỗi giá trị thử gán cho x 1, thuật

toán sẽ xét tất cả các giá trị x2 có thể nhận, lại thử cho x2 nhận lần lượt các giá trị đó. Với

mỗi giá trị thử gán cho x2 lại xét tiếp các khả năng chọn x 3, cứ tiếp tục như vậy… Mỗi khi

ta tìm được đầy đủ một cấu hình thì liệt kê ngay cấu hình đó.

b. Mơ hình :

procedure Attempt(i);

begin

for «mọi giá trị v có thể gán cho x[i]» do

begin

«Thử cho x[i] := v»;

if «x[i] là phần tử cuối cùng trong cấu hình» then

«Thơng báo cấu hình tìm được»

else

begin

«Ghi nhận việc cho x[i] nhận giá trị V (nếu cần)»;

Attempt(i + 1); //Gọi đệ quy để chọn tiếp x[i+1]

«Nếu cần, bỏ ghi nhận việc thử x[i] := V để thử giá trị khác»;

end;

end;

end;

Thuật toán quay lui sẽ bắt đầu bằng lời gọi Attempt(1).

Tên gọi thuật toán quay lui là dựa trên cơ chế duyệt các cấu hình: Mỗi khi thử chọn một

giá trị cho xi, thuật tốn sẽ gọi đệ quy để tìm tiếp x i+1, … và cứ như vậy cho tới khi tiến

trình duyệt xét tìm tới phần tử cuối cùng của cấu hình. Còn sau khi đã xét hết tất cả khả

năng chọn, tiến trình sẽ lùi lại thử áp đặt một giá trị khác cho xi-1.

c. Một số ví dụ

Bài tốn 1 : Liệt kê dãy nhị phân độ dài n.



Biểu diễn dãy nhị phân độ dài n dưới dạng dãy x 1...n. Ta sẽ liệt kê các dãy này bằng cách

thử dùng các giá trị {0,1} gán cho xi. Với mỗi giá trị thử gán cho xi lại thử các giá trị

có thể gán cho xi+1 …

procedure Attempt(i: Integer); //Thử các cách chọn x[i]

var j: AnsiChar;

begin

for j := '0' to '1' do //Xét các giá trị j có thể gán cho x[i]

begin //Với mỗi giá trị đó

x[i] := j; //Thử đặt x[i]

if i = n then WriteLn(x) //Nếu i = n thì in kết quả

else Attempt(i + 1); //Nếu x[i] chưa phải phần tử cuối thì tìm tiếp x[i + 1]

end;

end;

Ví dụ khi n=3 ta có thể vẽ cây đệ quy như sau :



Bài toán 2 : Liệt kê tập con có k phần tử.

Bài tốn liệt kê các tập con k phần tử của tập S = {1, 2, …, n} có thể quy về bài tốn liệt

kê các dãy k phần tử x1..k, trong đó 1≤x1
thứ tự từ điển, ta nhận thấy:

Tập con đầu tiên (cấu hình khởi tạo) là : {1, 2, …, k}



.



Tập con cuối cùng (cấu hình kết thúc) là : {n-k+1, n-k+2,…,n}

Như vậy giới hạn trên của xi là : n-k+i



Giới hạn dưới của xi là : xi-1 + 1



.



(Giả thiết rằng có thêm một số x0 = 0)

Thuật toán quay lui sẽ xét tất cả các cách chọn x1 từ 1 (=x0 + 1) đến n-k+1, với mỗi giá

trị đó, xét tiếp tất cả các cách chọn x2 từ x1 + 1đến n-k+2, … cứ như vậy khi chọn được

đến xk thì ta có một cấu hình cần liệt kê.

procedure Attempt(i: Integer); //Thử các cách chọn giá trị cho x[i]

var j: Integer;

begin

for j := x[i - 1] + 1 to n - k + i do

begin

x[i] := j;

if i = k then PrintResult

else Attempt(i + 1);

end;

end;

Bài tốn 3 : Liệt kê chỉnh hợp khơng lặp chập k.

Để liệt kê các chỉnh hợp không lặp chập k của tập S = {1, 2,…,n} ta có thể đưa về liệt kê

các cấu hình x1..k, các xi khác nhau đôi một.

Thủ tục Attempt (i) – xét tất cả các khả năng chọn x i



– sẽ thử hết các giá trị từ 1đến n



chưa bị các phần tử đứng trước x1… xi-1 chọn. Muốn xem các giá trị nào chưa được chọn

ta sử dụng kỹ thuật dùng mảng đánh dấu: Khởi tạo một mảng Free[1..n] mang kiểu logic

boolean. Ở đây Free[j] cho biết giá trị j có còn tự do hay đã bị chọn rồi. Ban đầu khởi tạo

tất cả các phần tử mảng Free[j] là True có nghĩa là các giá trị từ 1 đến n đều tự do.

 Tại bước chọn các giá trị có thể của xi ta chỉ xét những giá trị j còn tự do.

 Trước khi gọi đệ quy Attempt (i+1) để thử chọn tiếp xi+1 : ta đặt giá trị j vừa gán cho

xi là “đã bị chọn” (Free[j] = False) để các thủ tục Attempt (i+1), Attempt (i+2)….

gọi sau này không chọn phải giá trị j đó nữa.

 Sau khi gọi đệ quy Attempt (i+1) : có nghĩa là sắp tới ta sẽ thử gán một giá trị khác

cho xi thì ta sẽ đặt giá trị j vừa thử cho xi thành “tự do” (Free[j] = True), bởi khi xi đã

nhận một giá trị khác rồi thì các phần tử đứng sau (xi+1..k) hồn tồn có thể nhận lại



giá trị j đó.

 Tất nhiên ta chỉ cần làm thao tác đánh dấu/bỏ đánh dấu trong thủ tục Attempt (i) có

i≠k, bởi khi i=k thì tiếp theo chỉ có in kết quả chứ không cần phải chọn thêm phần

tử nào nữa.

procedure Attempt(i: Integer); //Thử các cách chọn x[i]

var j: Integer;

begin

for j := 1 to n do

if Free[j] then //Chỉ xét những giá trị j còn tự do

begin

x[i] := j;

if i = k then PrintResult //Nếu đã chọn được đến x[k] thì in kết quả

else

begin

Free[j] := False; //Đánh dấu j đã bị chọn

Attempt(i + 1);

Free[j] := True; //Bỏ đánh dấu

end;

end;

end;

Bài toán 4 : Liệt kê các cách phân tích số

Cho một số nguyên dương n, hãy tìm tất cả các cách phân tích số n thành tổng của các số

nguyên dương, các cách phân tích là hốn vị của nhau chỉ tính là 1 cách và chỉ được liệt

kê một lần.

Ta sẽ dùng thuật toán quay lui để liệt kê các nghiệm, mỗi nghiệm tương ứng với một dãy

x, để tránh sự trùng lặp khi liệt kê các cách phân tích, ta đưa thêm ràng buộc: dãy x phải

có thứ tự khơng giảm: x1 ≤ x2 ≤……



Thuật toán quay lui được cài đặt bằng thủ tục đệ quy Attempt (i) : thử các giá trị có thể

nhận của xi, mỗi khi thử xong một giá trị cho x i, thủ tục sẽ gọi đệ quy Attempt (i+1)

để thử các giá trị có thể cho xi+1. Trước mỗi bước thử các giá trị cho xi, ta lưu trữ m là

tổng của tất cả các phần tử đứng trước x i : x1..n và thử đánh giá miền giá trị mà x i có thể

nhận. Rõ ràng giá trị nhỏ nhất mà x i có thể nhận chính là xi-1 vì dãy có thứ tự khơng

giảm (Giả sử rằng có thêm một phần tử x 0=1, phần tử này không tham gia vào việc liệt

kê cấu hình mà chỉ dùng để hợp thức hố giá trị cận dưới của x 1)

Nếu xi chưa phải là phần tử cuối cùng, tức là sẽ phải chọn tiếp ít nhất một phần tử nữa mà

việc chọn thêm không làm cho tổng vượt quá n. Ta có:

n = m + xi + xi+1 ≥ m + 2xi

Tức là nếu xi chưa phải phần tử cuối cùng (cần gọi đệ quy chọn tiếpx i+1) thì giá trị lớn

nhất xi có thể nhận là [(n-m)/2], còn dĩ nhiên nếu x i là phần tử cuối cùng thì bắt buộc

phải bằng n-m.

Với giá trị khởi tạo x0=1 và m=0, thuật toán quay lui sẽ được khởi động bằng lời gọi

Attempt (1) và hoạt động theo cách sau: Với mỗi giá trị j : x i-1≤j≤[(n-m)/2], thử gán

xi=j, cập nhật m=m+j, sau đó gọi đệ quy tìm tiếp, sau khi đã thử xong các giá trị có thể

cho xi+1, biến m được phục hồi lại như cũ m=m+l trước khi thử gán một giá trị khác cho x i

procedure Attempt(i: Integer); //Thuật toán quay lui

var j: Integer;

begin

for j := x[i - 1] to (n - m) div 2 do



//Trường hợp còn chọn tiếp x[i+1]



begin

x[i] := j; //Thử đặt x[i]

m := m + j; //Cập nhật tổng m

Attempt(i + 1); //Chọn tiếp

m := m - j; //Phục hồi tổng m

end;

x[i] := n - m; //Nếu x[i] là phần tử cuối thì nó bắt buộc phải là n-m

PrintResult(i); //In kết quả

end;



4. Thuật toán nhánh cận

a. Tổng quan : Trong thực tế, có nhiều bài tốn u cầu tìm ra một phương án thoả mãn

một số điều kiện nào đó, và phương án đó là tốt nhất theo một tiêu chí cụ thể. Các bài

tốn như vậy được gọi là bài tốn tối ưu. Có nhiều bài tốn tối ưu khơng có thuật tốn nào

thực sự hữu hiệu để giải quyết, mà cho đến nay vẫn phải dựa trên mơ hình xem xét tồn

bộ các phương án, rồi đánh giá để chọn ra phương án tốt nhất.

- Tư tưởng của phương pháp nhánh và cận như sau: Giả sử, đã xây dựng được k thành

phần (x1, x2, ..., xk) của nghiệm và khi mở rộng nghiệm (x1, x2,..., xk+1,...), nếu biết rằng tất

cả các nghiệm mở rộng của nó (x1, x2,..., xk+1,...) đều khơng tốt bằng nghiệm tốt nhất đã

biết ở thời điểm đó, thì ta khơng cần mở rộng từ (x 1, x2, ..., xk) nữa. Như vậy, với phương

pháp nhánh và cận, ta không phải duyệt tồn bộ các phương án để tìm ra nghiệm tốt nhất

mà bằng cách đánh giá các nghiệm mở rộng, ta có thể cắt bỏ đi những phương án (nhánh)

khơng cần thiết.

b. Mơ hình

procedure Attempt(i: Integer);

begin

for «Mọi giá trị v có thể gán cho x[i]» do

begin

«Thử đặt x[i] := v»;

if «Còn hi vọng tìm ra cấu hình tốt hơn best» then

if «x[i] là phần tử cuối cùng trong cấu hình» then

«Cập nhật best»

else

begin

«Ghi nhận việc thử x[i] := v nếu cần»;

Attempt(i + 1); //Gọi đệ quy, chọn tiếp x[i + 1]

«Bỏ ghi nhận việc đã thử cho x[i] := v (nếu cần)»;

end;

end;

end;



c. Một số ví dụ

Bài tốn 1. Máy rút tiền tự động ATM : Một máy ATM hiện có n (n < 20) tờ tiền có giá

t1; t2, ..., tn. Hãy tìm cách trả ít tờ nhất với số tiền đúng bằng S.

Dữ liệu vào từfile “ATM.INP” có dạng:

- Dòng đầu là 2 số n và S

- Dòng thứ 2 gồm n số tl, t2 ,...,tn

Kết quả ra file “ATM.OUT” có dạng: Nếu có thể trả tiền đúng bằng S thì đưa ra số tờ ít

nhất cần trả và đưa ra cách trả, nếu không ghi -1.

Hướng dẫn : Như ta đã biết, nghiệm của bài toán là một dãy nhị phân độ dài n, giả sử đã

xây dựng được k thành phần (x1, x2, ...,xk), đã trả được sum và sử dụng c tờ. Để đánh giá

được các nghiệm mở rộng của (x1, x2, ...,xk), ta nhận thấy:

- Còn phải trả S - sum. Gọi tmax[k] là giá cao nhất trong các tờ tiền còn lại :

(tmax[k] = max{tk+1,.., tn}) thì ít nhất cần sử dụng thêm (S - sum)/tmax[k] tờ nữa.

- Do đó, nếu mà c + (s-sum)/tmax[i] lớn hơn hoặc bằng số tờ của cách trả tốt nhất hiện có

thì khơng cần mở rộng các nghiệm của (x1; x2,..., xk) nữa.

Procedure Attempt (i:longint);

Var j :longint;

Begin

if (c + (s-sum)/tmax[i] >= cbest) then exit;

for j:=0 to 1 do

begin

x[i]:=j;

sum:=sum + x[i]*t[i];

c:=c + j;

if (i=n) then update

else if (sum<=s) then Attempt(i+1);

sum:=sum - x[i]*t[i];

c:=c - j;

end;

End;



Bài toán 2. Bài toán người du lịch : Cho n thành phố từ 1 đến n và các tuyến đường

giao thông hai chiều giữa chúng, mạng giao thông này được cho bởi mảng C[1..n,1..n], ở

đây Cij = Cji là chi phí đi đoạn đường trực tiếp từ thành phố i đến thành phố j.

Một người du lịch xuất phát từ thành phố 1, muốn đi thăm tất cả các thành phố còn lại

mỗi thành phố đúng 1 lần và cuối cùng quay lại thành phố 1. Hãy chỉ ra cho người đó

hành trình với chi phí ít nhất.

Dữ liệu vào trong file “TSP.INP” có dạng:

- Dòng đầu chứa số n(1
- n dòng tiếp theo, mỗi dòng n số mơ tả mảng C

Kết quả ra file “TSP.OUT” có dạng:

- Dòng đầu là chi phí ít nhất

- Dòng thứ hai mơ tả hành trình

Ví dụ



Hướng dẫn :

- Hành trình cần tìm có dạng (x 1 = 1, x2, xn, xn+1 = 1), ở đây giữa xi và xi+1: hai thành phố

liên tiếp trong hành trình phải có đường đi trực tiếp; trừ thành phố 1, không thành phố nào

được lặp lại hai lần, có nghĩa là dãy (x1 x2, ..., xn) lập thành một hoán vị của (1, 2, ..., n).

- Duyệt quay lui: x2 có thể chọn một trong các thành phố mà x 1 có đường đi trực tiếp tới,

với mỗi cách thử chọn x 2 như vậy thì x3 có thể chọn một trong các thành phố mà x 2 có

đường đi tới (ngồi x1). Tổng qt: xi có thể chọn 1 trong các thành phố chưa đi qua mà từ

xi-1 có đường đi trực tiếp tới (2 < i < n).

- Nhánh cận: Khởi tạo cấu hình BestSolution có chi phí = +oo. Với mỗi bước thử chọn xi

xem chi phí đường đi cho tới lúc đó có nhỏ hơn chi phí của cấu hình BestSolution khơng?

Nếu khơng nhỏ hơn thì thử giá trị khác ngay bởi có đi tiếp cũng chỉ tốn thêm.



Khi thử được một giá trị x n ta kiểm tra xem xn có đường đi trực tiếp về 1 khơng? Nếu có

đánh giá chi phí đi từ thành phố 1 đến thành phố x n cộng với chi phí từ xn đi trực tiếp về 1,

nếu nhỏ hơn chi phí của đường đi BestSolution thì cập nhật lại BestSolution bằng cách đi

mới.

Procedure update;

Begin

if (sum+c[x[n],x[1]] < min) then begin

min:= sum+c[x[n],x[1]]; best:=x; end;

End;

Procedure Attempt (i:longint);

Var j :longint;

Begin

if sum>=best then exit;

for j:=1 to n do

if d[j]=0 then

begin

x[i]:=j; d[j]:=1;

sum:=sum + C[x[i-1],j];

if i=n then update

else if (sum < min) then Attempt (i+1);

sum:=sum - C[x[i-1],j]; d[j]:=0;

end;

End;

Có một phương pháp đánh giá cận tốt hơn đó là : tại thành phố x i sau khi tính được chi

phí sum ta nhẩm tính thử xem sum cộng với chi phí nhỏ nhất đi từ x i qua các thành phố

chưa tới và về lại 1 có nhỏ hơn cấu hình tốt nhất tìm được khơng :

sum + minC*(n-i+1) < min

Nếu nhỏ hơn thì tìm tiếp còn khơng thì quay lui sớm tìm cấu hình khác.



III. Quy hoạch động

1. Tổng quan

Trong chiến lược chia để trị, người ta phân bài toán cần giải thành các bài toán con. Các

bài toán con lại được tiếp tục phân thành các bài toán con nhỏ hơn, cứ thế tiếp tục cho tới

khi ta nhận được các bài tốn con có thể giải được dễ dàng. Tuy nhiên, trong q trình

phân chia như vậy, có thể ta sẽ gặp rất nhiều lần cùng một bài tốn con. Ví dụ như bài

toán Fibonaci :

1, n �2



Fn  �

�Fn 1  Fn 2 , n>2



Ta đã biết cách giải bằng phương pháp chia để trị và đệ quy :

Function Fibo(n: longint): int64;

Begin

If ( n <=2) then Fibo := n

else Fibo := Fibo(i-1) + Fibo(i-2);

End;

Hàm đệ quy Fibo(n) để tính số Fibonacci thứ n. Ví dụ n = 6, chương trình chính gọi

Fibo(6), nó sẽ gọi tiếp Fibo(5) và Fibo(4) để tính ... Q trình tính tốn có thể vẽ như cây

dưới đây. Ta nhận thấy để tính Fibo(6) nó phải tính 1 lần Fibo(5), hai lần Fibo(4), ba lần

Fibo(3), năm lần Fibo(2), ba lần Fibo(1).



Bây giờ ta xét một cách khác để tiếp cận bài toán như sau : Ta sử dụng mảng F[1..N], với

F[i] để tính số Fibonacci thứ i.

F[1] := 1; F[2]:= 1;

for i := 3 to n do

F[i] := F[i-1] + F[i-2];

Ta nhận thấy, mỗi bài toán con chỉ được giải đúng một lần. Phương pháp này được gọi là

quy hoạch động: Khi không biết cần phải giải quyết những bài toán con nào, ta sẽ đi giải

quyết tất cả các bài toán con và lưu trữ những lời giải hay đáp số của chúng với mục đích

sử dụng lại theo một sự phối hợp nào đó để giải quyết những bài tốn tổng qt hơn mà

khơng cần phải giải lại các bài toán con.

3. Cách nhận diện bài tốn quy hoạch động

Một bài tốn có thể giải bằng quy hoạch động thường có 3 tính chất sau :

 Bài tốn lớn có thể phân rã thành những bài tốn con đồng dạng, những bài tốn con

đó có thể phân rã thành những bài tốn nhỏ hơn nữa …(recursive form).

 Lời giải tối ưu của các bài tốn con có thể sử dụng để tìm ra lời giải tối ưu của bài

toán lớn (optimal substructure)

 Hai bài tốn con trong q trình phân rã có thể có chung một số bài tốn con

khác (overlapping subproblems).

Tính chất thứ nhất và thứ hai là điều kiện cần của một bài tốn quy hoạch động. Tính

chất thứ ba nêu lên đặc điểm của một bài toán mà cách giải bằng phương pháp quy hoạch

động hiệu quả hơn hẳn so với phương pháp giải đệ quy thơng thường.

Với những bài tốn có hai tính chất đầu tiên, chúng ta thường nghĩ đến các thuật toán chia

để trị và đệ quy: Để giải quyết một bài tốn lớn, ta chia nó ra thành nhiều bài toán con

đồng dạng và giải quyết độc lập các bài tốn con đó.

Khác với thuật tốn đệ quy, phương pháp quy hoạch động thêm vào cơ chế lưu trữ

nghiệm hay một phần nghiệm của mỗi bài toán khi giải xong nhằm mục đích sử dụng l i,

hạn chếnhững thao tác thừa trong q trình tính tốn.

4. Các khái niệm

- Công thức phối hợp nghiệm của các bài tốn con để có nghiệm của bài tốn lớn gọi là

cơng thức truy hồi (hay phương trình truy tốn) của quy hoạch động.



Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Thuật toán quay lui

Tải bản đầy đủ ngay(0 tr)

×