Giáo trình Cấu trúc dữ liệu và giải thuật - Chương 6: Đệ quy

Chúng ta đặc biệt chú ý đến đệ quy, do đó thông thường chúng ta chỉ vẽ một

phần của cây biểu diễn sự gọi đệ quy, và chúng ta gọi là cây đệ quy (recursion

tree). Trong sơ đồ cây chúng ta cũng lưu ý một điều là không có sự khác nhau giữa

cách gọi đệ quy với cách gọi hàm khác. Sự đệ quy đơn giản chỉ là sự xuất hiện của

các nút khác nhau trong cây có quan hệ nút trước – nút sau với nhau mà có cùng

tên. Điểm thứ hai cần lưu ý rằng, chính vì cây biểu diễn các lần gọi hàm, nên

trong chương trình, nếu một lệnh gọi hàm chỉ xuất hiện một lần nhưng lại nằm

trong vòng lặp, thì nút biểu diễn hàm sẽ xuất hiện nhiều lần trong cây, mỗi

lần tương ứng một lần gọi hàm. Tương tự, nếu lệnh gọi hàm nằm trong phần rẽ

nhánh của một điều kiện mà điều kiện này không xảy ra thì nút biểu diễn hàm sẽ

không xuất hiện trong cây.

Cơ cấu ngăn xếp ở hình 6.1 cho thấy nhu cầu về vùng nhớ của đệ quy. Nếu một

hàm gọi đệ quy chính nó vài lần thì bản sao của các biến khai báo trong hàm

được tạo ra cho mỗi lần gọi đệ quy. Trong cách hiện thực thông thường của đệ

quy, chúng được giữ trong ngăn xếp. Chú ý rằng tổng dung lượng vùng nhớ

cần cho ngăn xếp này tỉ lệ với chiều cao của cây đệ quy chứ không phụ thuộc

vào tổng số nút trong cây. Điều này có nghĩa rằng, tổng dung lượng vùng nhớ

cần thiết để hiện thực một hàm đệ quy phụ thuộc vào độ sâu của đệ quy, không

phụ thuộc vào số lần mà hàm được gọi.

 

pdf46 trang | Chuyên mục: Cấu Trúc Dữ Liệu & Giải Thuật | Chia sẻ: tuando | Lượt xem: 763 | Lượt tải: 0download
Tóm tắt nội dung Giáo trình Cấu trúc dữ liệu và giải thuật - Chương 6: Đệ quy, để xem tài liệu hoàn chỉnh bạn click vào nút "TẢI VỀ" ở trên
ước đi của trò chơi được nhìn trước với độ sâu là depth, nước đi tốt nhất được chỉ ra 
trong tham biến recommended. 
uses: các lớp Stack, Board, và Move, cùng với hàm look_ahead một cách đệ quy. 
*/ 
Algorithm look_ahead (thông số là một đối tượng Board); 
1. if đệ quy dừng (độ sâu depth == 0 hoặc game.done()) 
 1. return trị lượng giá của tình huống 
2. else 
 1. for mỗi nước đi Move hợp lệ 
 1. tạo một đối tượng Board mới bằng cách thực hiện nước đi Move. 
 2. gọi đệ quy look_ahead tương ứng với sự lựa chọn tốt nhất của 
người chơi kế tiếp; 
 2. Chọn cách đi tốt nhất cho người chơi trong số các cách đi tìm được 
trong vòng lặp trên; 
3. return đối tượng Move tương ứng và trị; 
Chương 6 – Đệ quy 
Giáo trình Cấu trúc dữ liệu và Giải thuật 132
{ 
 if (game.done() || depth == 0) 
 return game.evaluate(); 
 else { 
 Stack moves; 
 game.legal_moves(moves); 
 int value, best_value = game.worst_case(); 
 while (!moves.empty()) { 
 Move try_it, reply; 
 moves.top(try_it); 
 Board new_game = game; 
 new_game.play(try_it); 
 value = look_ahead(new_game, depth - 1, reply); 
 if (game.better(value, best_value)) { // nước đi thử try_it vừa rồi hiện 
là tốt nhất. 
 best_value = value; 
 recommended = try_it; 
 } 
 moves.pop(); 
 } 
 return best_value; 
 } 
} 
Tham biến recommended sẽ nhận về một nước đi được tiến cử (trừ khi đệ quy 
rơi vào điểm dừng, đó là khi trò chơi kết thúc hoặc độ sâu của việc nhìn trước 
depth bằng 0). Do chúng ta không muốn đối tượng game bị thay đổi trong hàm, đồng thời để 
tránh việc chép lại mất thời gian, nó được gởi cho hàm bằng tham chiếu hằng. Chúng ta cũng lưu 
ý rằng việc khai báo tham chiếu hằng này chỉ hợp lệ khi đối tượng game trong hàm chỉ thực hiện 
các phương thức đã được khai báo const trong định nghĩa của lớp Board. 
6.4.5. Tic-Tac-Toe 
Như trên đã nói, hàm đệ quy look_ahead cùng hai lớp Board và Move trên 
đây tuy rất đơn giản, nhưng nó lại là cốt lõi trong các chương trình biểu diễn trò 
chơi có hai đối thủ. Chúng ta sẽ triển khai phác thảo này trong trò chơi tic-tac-toe 
rất quen thuộc. Để làm được điều này, cả hai lớp sẽ chứa thêm một ít dữ liệu khác 
so với hiện thực hình thức ban đầu. 
Việc viết chương trình chính hoàn tất cùng hàm look_ahead cho trò chơi tic-
tac-toe được dành lại như bài tập. Chương trình có thể chứa thêm nhiều chức 
năng như cho phép người chơi với máy, đưa ra các phân tích đầy đủ cho mỗi tình 
huống, cung cấp chức năng cho hai người chơi với nhau, đánh giá các bước đi của 
hai đối thủ,... 
Chúng ta biểu diễn lưới trò chơi tic-tac-toe bằng một mảng 3x3 các số nguyên, 
ô có trị 0 là ô trống, trị 1 và 2 biểu diễn nước đi của người thứ nhất và thứ hai 
tương ứng. 
Chương 6 – Đệ quy 
Giáo trình Cấu trúc dữ liệu và Giải thuật 133
Trong đối tượng Move, chúng ta chứa tọa độ các ô trên lưới. Một nước đi hợp lệ 
chứa tọa độ có các trị từ 0 đến 2. Chúng ta không cần đến tính đóng kín của đối 
tượng Move vì nó chỉ như một vật chứa để chứa các giá trị. 
// lớp move cho trò chơi tic-tac-toe 
class Move { 
public: 
 Move(); 
 Move(int r, int c); 
 int row; 
 int col; 
}; 
Move::Move() 
/* 
post: đối tượng Move được khởi tạo bởi trị mặc định không hợp lệ. 
*/ 
{ 
 row = 3; 
 col = 3; 
} 
Move::Move(int r, int c) 
/* 
post: đối tượng Move được khởi tạo bởi toạ độ cho trước. 
*/ 
{ 
 row = r; 
 col = c; 
} 
Lớp Board cần một constructor để khởi tạo trò chơi, phương thức print và 
instruction (in các thông tin cho người chơi), phương thức done, play và 
legal_moves (hiện thực các quy tắc chơi), và các phương thức evaluate, 
better, và worst_case (phán đoán điểm cho các nước đi khác nhau). Chúng ta 
còn có thể bổ sung hàm phụ trợ the_winner cho biết rằng trò chơi đã có người 
thắng chưa và người thắng là ai. 
Lớp Board cần chứa thông tin về trạng thái hiện tại của trò chơi trong mảng 
3x3 và tổng số bước đi đã thực hiện. Chúng ta có lớp Board như sau: 
class Board { 
public: 
 Board(); 
 bool done() const; 
 void print() const; 
 void instructions() const; 
 bool better(int value, int old_value) const; 
 void play(Move try_it); 
 int worst_case() const; 
Chương 6 – Đệ quy 
Giáo trình Cấu trúc dữ liệu và Giải thuật 134
 int evaluate() const; 
 int legal_moves(Stack &moves) const; 
private: 
 int squares[3][3]; 
 int moves_done; 
 int the_winner() const; 
}; 
Constructor đơn giản chỉ làm một việc là khởi tạo tất cả các ô của mảng là 0 
để chỉ rằng cả hai người chơi đều chưa đi nước nào. 
Board::Board() 
/* 
post: đối tượng Board được khởi tạo rỗng tương ứng trạng thái ban đầu của trò chơi. 
*/ 
{ 
 for (int i = 0; i < 3; i++) 
 for (int j = 0; j < 3; j++) 
 squares[i][j] = 0; 
 moves_done = 0; 
} 
Chúng ta dành các phương thức in các thông tin cho người chơi như là bài tập. 
Thay vào đó, chúng ta tập trung vào các phương thức liên quan đến các quy tắc 
của trò chơi. Để thực hiện một nước đi, chúng ta chỉ cần gán lại trị cho một ô và 
tăng biến đếm moves_done lên 1. Dựa vào biến đếm này chúng ta còn biết được 
đến lượt người nào đi. 
void Board::play(Move try_it) 
/* 
post: nước đi try_it được thực hiện 
*/ 
{ 
 squares[try_it.row][try_it.col] = moves_done % 2 + 1; 
 moves_done++; 
} 
Hàm phụ trợ the_winner trả về một số khác không nếu đã có người thắng. 
int Board::the_winner() const 
/* 
post: trả về 0 nếu chưa ai thắng; 1 nếu người thứ nhất thắng; 2 nếu người thứ hai thắng. 
*/ 
{ 
 int i; 
 for (i = 0; i < 3; i++) 
 if (squares[i][0] && squares[i][0] == squares[i][1] 
 && squares[i][0] == squares[i][2]) 
 return squares[i][0]; 
 for (i = 0; i < 3; i++) 
 if (squares[0][i] && squares[0][i] == squares[1][i] 
 && squares[0][i] == squares[2][i]) 
 return squares[0][i]; 
Chương 6 – Đệ quy 
Giáo trình Cấu trúc dữ liệu và Giải thuật 135
 if (squares[0][0] && squares[0][0] == squares[1][1] 
 && squares[0][0] == squares[2][2]) 
 return squares[0][0]; 
 if (squares[0][2] && squares[0][2] == squares[1][1] 
 && squares[2][0] == squares[0][2]) 
 return squares[0][2]; 
 return 0; 
} 
Trò chơi kết thúc sau 9 nước đi hoặc khi có người thắng. (Chương trình của chúng ta không 
phát hiện sớm khả năng hòa trước khi kết thúc 9 nước đi). 
bool Board::done() const 
/* 
post: trả về true nếu trò chơi đã phân thắng bại hoặc cả 9 ô trên bàn cờ đã được đánh dấu, 
ngược lại trả về false. 
*/ 
{ 
 return moves_done == 9 || the_winner() > 0; 
} 
Trong trò chơi đơn giản này, nước đi hợp lệ là những ô mang trị 0. 
int Board::legal_moves(Stack &moves) const 
/* 
post: Thông số moves chứa các nước đi hợp lệ. 
*/ 
{ 
 int count = 0; 
 while (!moves.empty()) moves.pop(); 
 for (int i = 0; i < 3; i++) 
 for (int j = 0; j < 3; j++) 
 if (squares[i][j] == 0) { 
 Move can_play(i,j); 
 moves.push(can_play); 
 count++; 
 } 
 return count; 
} 
Chúng ta chuyển sang các phương thức có thể cho những sự đánh giá về một 
tình huống của trò chơi hoặc của một nước đi có thể xảy ra. Chúng ta sẽ bắt đầu 
đánh giá một tình huống của trò chơi là 0 trong trường hợp chưa có người nào 
thắng. Nếu một trong hai người thắng, chúng ta sẽ đánh giá tình huống dựa vào 
quy tắc: càng thắng nhanh thì càng hay. Điều này cũng đồng nghĩa với việc càng 
thua nhanh thì càng dở. Nếu moves_done là số nước đi cả hai người chơi đã thực 
hiện thì (10-moves_done) càng lớn khi số nước đã đi càng nhỏ, tương ứng sự 
đánh giá cao khi người thứ nhất sớm thắng. Ngược lại, nếu người thứ hai thắng 
thì chúng ta dùng trị (moves_done-10), số nước đi càng nhỏ thì trị này là một 
số càng âm, tương ứng việc thua nhanh chóng của người thứ nhất là rất dở. 
Chương 6 – Đệ quy 
Giáo trình Cấu trúc dữ liệu và Giải thuật 136
Dĩ nhiên rằng, cách đánh giá này chỉ được áp lên điểm cuối của việc nhìn 
trước (ở độ sâu mong muốn hoặc khi trò chơi đã phân thắng bại). Trong phần lớn 
các tình huống, nếu chúng ta nhìn trước càng xa thì bản chất chưa tinh của cách 
đánh giá sẽ đỡ gây hậu quả xấu hơn. 
int Board::evaluate() const 
/* 
post: trả về 0 khi chưa có người thắng cuộc; hoăc 1 số dương từ 1 đến 9 trong trường hợp người 
thứ nhất thắng, ngược lại là một số âm từ –1 đến –9 trong trường hợp người thứ hai 
thắng. 
*/ 
{ 
 int winner = the_winner(); 
 if (winner == 1) return 10 - moves_done; 
 else if (winner == 2) return moves_done - 10; 
 else return 0; 
} 
Phương thức worst_case có thể chỉ đơn giản trả về một trong hai trị 10 hoặc 
-10, do evaluate luôn trả một trị nằm giữa -9 và 9. Từ đó, phương thức so sánh 
better chỉ cần so sánh một cặp số nguyên có trị nằm giữa -10 và 10. Các phương 
thức này xem như bài tập. 
Giờ thì chúng ta đã phác thảo xong phần lớn chương trình trò chơi tic-tac-toe. 
Một chương trình lấy độ sâu cho việc nhìn trước là 9 sẽ chơi tốt do chúng ta luôn 
có thể nhìn trước đến một tình huống mà sự đánh giá về nó là chính xác. Một 
chương trình nhìn trước với một độ sâu không lớn có thể mắc phải sai lầm, do nó 
có thể kết thúc việc nhìn trước bởi tập các tình huống có trị đánh giá bằng 0 một 
cách nhầm lẫn. 

File đính kèm:

  • pdfgiao_trinh_cau_truc_du_lieu_va_giai_thuat_chuong_6_de_quy.pdf