Quy lui là một kĩ thuật thiết kế giải thuật dựa trên đệ quy. Ý tưởng của quay lui là tìm lời giải từng bước, mỗi bước chọn một trong số các lựa chọn khả dĩ và đệ quy. Ok. Có vẻ vẫn chưa được rõ ràng lắm. Giờ chúng ta xem xét một vài ví dụ và khái quát hóa phương pháp quay lui.
Bài toán quân hậu ( Queens)
Bài toán như sau:
Problem 1: Cho một bàn cờ hình vuông kích thước và quân hậu. Hãy tìm cách đặt quân hậu trên bàn cờ sao cho không có 2 quân hậu nào có thể ăn được nhau.
Mã hóa lời giải: ta thấy để các quân hâu không ăn được nhau, ta phải xếp quân hậu trên hàng của bàn cờ. Ta dùng mảng , trong đó nếu quân hậu ở hàng thứ được đặt ở cột .
Ý tưởng chính của thuật toán: giả sử chúng ta đã đặt được quân hậu, , trên hàng đầu tiên sao cho không có 2 quân hậu nào ăn được nhau. Cụ thể các phần tử sẽ khác và các phần tử đều bằng . Chúng ta tìm cách đặt một quân hậu trên hàng thứ . Ta sẽ thử lần lượt đặt vào cột thứ . Nếu đặt vào cột thứ mà bị một trong quân hậu đã đặt trước đó ăn, ta sẽ thử cột thứ . Nếu ta tìm được một ví trí đặt khả dĩ (a feasible position), ta sẽ đặt vào đó và gọi đệ quy để đặt hàng thứ . Giả mã như sau:
RecursiveQueen():
if
print
else
for to
True
for to check conflict
if or or
False
if True
RecursiveQueen()
if
else
for to
True
for to check conflict
if or or
False
if True
RecursiveQueen()
Code của giả mã bằng C. Code đầy đủ được cho ở cuối bài.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| void recursive_queen( int _array[], int n, int r){ int i=1, j = 1; int legal = 0; if (r == n +1){ print_queen(_array, n); exit (0); } else { for (j = 1; j <= n ; j++){ legal = 1; // legal = true for (i = 1; i <= r-1; i++){ if (_array[i] == j || _array[i] == j + r - i ||_array[i] == j - r + i){ legal = 0; // legal = false } } if (legal == 1) { // legal = true _array[r] = j; recursive_queen(_array, n, r+1); } } } } |
Cây đệ quy là một cách nhìn khác của thuật toán. Tưởng tượng ta có một cây với gốc tượng trưng cho cấu hình ban đầu của bàn cờ (mảng gồm toàn 0). Nút con của gốc, gọi là nút level 1, là các cách đặt một quân hậu vào hàng thứ 1. Nút con của một nút level 1, gọi là nút level 2, là các cách đặt khả dĩ một quân hậu vào hàng thứ 2. Cứ như thế đến nút level (tại sao chỉ có level) ta sẽ thu được một cây gọi là cây đệu quy. Có thể thấy rằng thuật toán quay lui ở trên thực ra chính là một cách duyệt cây theo chiều sâu cho đến khi tìm được một nút ở level . Hình minh họa sau với được lấy từ [1]
Có thể thấy thuật toán trên có thời gian cỡ . Tuy nhiên nếu bạn cần một lời giải bất kì, bài toán này có công thức giải và bạn chỉ mất thời gian để in lời giải ra ()[3].
Có thể thấy thuật toán trên có thời gian cỡ . Tuy nhiên nếu bạn cần một lời giải bất kì, bài toán này có công thức giải và bạn chỉ mất thời gian để in lời giải ra ()[3].
Sudoku
Sudoku là một trò chơi khá phổ biến và chắc ai (readers of this bog) cũng biết. Trò chơi như sau: có một hình vuông được chia thành 9x9 ô vuông con. Mỗi ô vuông con có giá trị trong khoảng từ 1 đến 9. Ban đầu hình vuông có một số ô vuông con cho trước (có điền sẵn số) và còn lại là trống. Hãy điền các số từ 1-9 vào các ô con lại sao cho: hàng ngang là các số khác nhau từ 1 đến 9, hàng dọc là các số khác nhau từ 1 đến 9, và mỗi khối 3x3 chính là các số khác nhau từ 1 đến 9. Ví dụ một câu đố và lời gải tương ứng như sau (hình được lấy từ wikipedia):
Trong bài này mình sẽ giới thiệu cách giải sudoku bằng thuật toán quay lui. Ý tưởng của thuật toán cũng giống bài toán quân hậu. Mỗi bước tìm tập các giá trị khả dĩ để điền vào ô trống, và sau đó đệ quy để điền ô tiếp theo. Giả mã của thuật toán (ở đây chú ý mảng chỉ có kích thước ). Thủ tục Feasible kiểm trả xem giá trị có khả dĩ với ô không.
Sudoku():
if
if
print
else
Sudoku()
else if
for to
if Feasible
Sudoku()
for next branching
else is given
Sudoku()
if
if
else
Sudoku()
else if
for to
if Feasible
Sudoku()
for next branching
else is given
Sudoku()
Code của giả mã trên bằng C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| void solve_sudoku( int S[][9], int x, int y){ if (y == 9){ if (x == 8){ printSolution(S); exit (0); } else { solve_sudoku(S, x+1,0); } } else if (S[x][y] == 0){ int k = 0; for (k = 1; k <=9; k++){ if (feasible(S,x,y,k)){ S[x][y] = k; solve_sudoku(S, x, y+1); S[x][y] = 0; } } } else { solve_sudoku(S,x,y+1); } } |
Giả mã của thủ tục Feasible như sau:
Feasible():
for to
if
return False
for to
if
return False
for to
for to
if
return False
return True
for to
if
return False
for to
if
return False
for to
for to
if
return False
return True
Code bằng C của giả mã (code đầy đủ được cho ở cuối bài):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| int feasible( int S[][9], int x, int y, int k){ int i = 0, j = 0; for (i = 0; i <9 ; i++){ if (S[x][i] == k) return 0; } for (i = 0; i <9 ; i++){ if (S[i][y] == k) return 0; } int a = x/3, b = y/3; for (i = 3*a; i < 3*a+3; i++){ for (j = 3*b; j < 3*b+3; j++){ if (S[i][j] == k) return 0; } } return 1; } |
Subset Sum
Subset Sum là một trong những bài toán NP-hard. Trong bài này chúng ta sẽ thiết kế thuật toán quay lui để giải bài toán Subset Sum.
Problem 2: Cho một mảng phần tử và một số . Có tồn tại một tập con các phần tử của mảng sao cho tổng của chúng bằng .
Ví dụ: và . Lời giải là true vì tập con của có tổng bằng 12.
Ý tưởng của thuật toán quay lui dựa trên quan sát sau: xét một phần tử , tồn tại một dãy con có tổng bằng nếu một trong hai điều kiện sau là đúng:
- Tồn tại một tập con của có tổng bằng
- Tồn tại một tập con của có tổng bằng
Giả mã như sau:
SubsetSum():
if
return True
else if > or
return False
return SubsetSum Or
SubsetSum
if
return True
else if > or
return False
return SubsetSum Or
SubsetSum
Code của giả mã bằng C:
1
2
3
4
5
6
7
| int subset_sum( int X[], int r, int T){ if (T == 0) return 1; if (T < 0 || r == -1) return 0; if (subset_sum(X, r-1, T-X[r]) == 1) return 1; if (subset_sum(X,r-1, T) == 1) return 1; return 0; } |
Phân tích thời gian: Ở mỗi bước của thuật toán quay lui, ta gọi đệ quy hai lần trên mảng con của với kích thước nhỏ hơn 1. Ta có:
Tài liệu tham khảo
[1] J. Erickson. Algorithm Lecture Notes, UIUC.
[2] queen on stackexchange: http://cstheory.stackexchange.com/questions/12682/is-the-n-queens-problem-np-hard
[3] B. Bernhardsson, Explicit Solutions to the N-queens Problem for All N, SIGART Bull. 2(2)(1991)7.
[2] queen on stackexchange: http://cstheory.stackexchange.com/questions/12682/is-the-n-queens-problem-np-hard
[3] B. Bernhardsson, Explicit Solutions to the N-queens Problem for All N, SIGART Bull. 2(2)(1991)7.
Bài Tập
Bài 1 (Longest Increasing Subsequence) Cho một mảng phần tử . Tìm một dãy dài nhất ( lớn nhất) các chỉ số < << sao cho bằng phương pháp quay lui. (Gợi ý: xét , nếu nằm trong dãy con tăng, đệ quy trên tìm dãy con tăng dài nhất mà mọi phần tử đều lớn hoưn . Nếu không nằm trong dãy con tăng, đệ quy trên ).
Bài 2 (Dãy gia tốc) Một dãy gọi là gia tốc nếu > . Cho một dãy , tìm dãy con gia tốc dài nhất của . Tìm công thức đệ quy cho bài toán, dựa vào đó thiết kế giải thuật quay lui.
Bài 3 Hãy sửa đổi thuật toán quay lui ở trên của bài toán Subset Sum để in ra ít nhất một dãy con có tổng bằng của .