Bài tập môn học kỹ thuật lập trình
Trang 1TRƯỜNG ĐẠI HỌC BÁCH KHOA HÀ NỘI
VIỆN CÔNG NGHỆ THÔNG TIN VÀ TRUYỀN THÔNG
Vũ Văn Hiệp-CNTT2 20101545Nguyễn Xuân Hòa-CNTT2 20101559Giáo viên :
Lương Mạnh Bá
HÀ NỘINgày 25 tháng 3 năm 2012
Trang 2Mục lục
1.1 Cách giải đệ quy 3
1.1.1 Phân tích bài toán 3
1.1.2 Thuật toán 4
1.1.3 Listing chương trình 4
1.1.4 Kết quả 9
1.2 Cách giải không đệ quy 13
1.2.1 Phân tích 13
1.2.2 Listing chương trình 14
1.2.3 Kết quả 17
2 Bài toán tháp Hà Nội 19 2.1 Cách giải đệ quy 19
2.1.1 Phân tích 19
2.1.2 Thuật toán 19
2.1.3 Listing chương trình 19
2.1.4 Kết quả 21
2.2 Cách giải không đệ quy 22
2.2.1 Phân tích thuật toán 22
2.2.2 Thuật toán 23
2.2.3 List chương trình 23
2.2.4 Kết quả 24
Trang 3Hình 1: Quân Hậu ở ô (3,2) chiếm cột thứ 2, đường chéo loại I thứ 5 và đường chéo loại IIthứ 1
Đề bài: Liệt kê tất cả các cách sắp xếp N quân hậu trên bàn cờ N x N sao cho chúngkhông ăn được nhau
1.1 Cách giải đệ quy
1.1.1 Phân tích bài toán
Đánh số cột và số dòng của bàn cờ từ 0 đến N − 1 Rõ ràng N quân Hậu được xếp trên
N hàng khác nhau Do đó ta chỉ cần tìm xem mỗi quân Hậu được xếp ở cột nào Gọi colsi
là chỉ số cột của quân Hậu ở hàng thứ i (i = 0− > N − 1), colsi có thể lấy giá trị từ 0 đến
N − 1 Ta sẽ tìm colsi cho i từ 1 đến N − 1 Giá trị colsi = j được coi là thoả mãn khi ô cờ(i, j) chưa bị quân Hậu nào trong số những quân Hậu đã tìm trước đó ăn Một quân Hậu cóthể ăn theo chiều ngang, dọc và theo hai đường chéo
Một quân Hậu trước đó ăn ô (i, j) theo chiều ngang có nghĩa là quân Hậu đó ở hàng
i, điều này là không thể xảy ra do cách làm của ta là mỗi hàng chỉ xếp đúng một quânHậu Quân Hậu ăn theo chiều dọc có nghĩa là quân Hậu đó có chỉ số cột là j Đối với haiđường chéo, ta thấy một đường có i + j = const, (0 ≤ i + j ≤ 2N − 2), đường kia có phươngtrình i − j = const, (1 − N ≤ i − j ≤ N − 1) Như vậy có tất cả 2N − 1 đường chéo loại I(i + j = const) và 2N − 1 đường chéo loại II (i − j = const) Khi xếp một quân Hậu vào ô(i, j) thì có nghĩa là cột j; đường chéo loại I thứ (i + j) và đường chéo loại II thứ (i − j) đã
bị chiếm, các quân Hậu sau này không được phép đặt vào vị trí có cùng cột, đường chéo vớinó
Để biểu diễn một cột, đường chéo đã bị chiếm ta có thể sử dụng 3 mảng bool ai, bi, ci thểhiện việc cột thứ i, đường chéo loại I thứ i, đường chéo loại II i đã bị chiếm hay chưa Dongôn ngữ C không hỗ trợ mảng có chỉ số âm nên ta có thể gán:
bool raw_c[2*N];
bool* c = raw_c + N;
3
Trang 4Khi đó, c[i] == raw_c[i + N ] với −N ≤ i ≤ N − 1.
1.1.2 Thuật toán
Ta dùng thuật toán đệ quy quay lui với bài toán này Cụ thể gồm các bước như sau:
1 Khởi tạo a, b, c đều bằng true, có nghĩa là tất cả các cột vào đường chéo lúc đầu đềuchưa bị chiếm i = 0 là thứ tự quân Hậu đang xét, cũng là chỉ số hàng của nó
2 Thử các giá trị j, 0 ≤ j ≤ N − 1 là chỉ số cột của quân Hậu hiện tại (hàng i)
3 Nếu ô (i, j) chưa bị chiếm, tức aj == bi+j == ci−j == true thì chấp nhận giá trị
j (gán colsi = j) Đánh dấu các cột, đưòng chéo tương ứng với ô (i, j) đã bị chiếm(aj = bi+j = ci−j = f alse) Nếu i == N − 1, tức là đã tìm ra 1 kết quả thì in nó ra.Còn lại ta tìm colsi+1 Sau đó trả lại ô (i, j) (tức là gán aj = bi+j = ci−j = true)
4 Nếu ô (i, j) đã bị chiếm thì lại tiếp tục thử giá trị tiếp theo của j
5 Nếu không có khả năng nào thoả mãn thì quay lại bước trước đó để tìm giá trị tiếptheo của colsi−1
Có thể biểu diễn bằng mã giả như sau:
1 def find(i):
Trang 524 *b, // Đường chéo I, i + j = const
25 *raw_c, *c; // Đường chéo II, i - j = const
26
27 void init_state(int);
28 void free_state(int);
29
30 void find(int, int);
31
32 void print_result(int const*, int);
33 void print_result_board(int const*, int);
34 void (*print_func)(int const*, int) = print_result;
Trang 678 void init_state(int n) {
79 a = (bool*) malloc(sizeof(bool)*n);
80 b = (bool*) malloc(sizeof(bool)*2*n);
81 raw_c = (bool*) malloc(sizeof(bool)*2*n), c = raw_c + n;
87 for(int i = 0; i < n; ++i)
88 a[i] = b[i] = raw_c[i] = true;
89 for(int i = n; i < 2*n; ++i)
90 b[i] = raw_c[i] = true;
Trang 7117 void print_result(int const* cols, int n) {
118 static int count = 0
119
120 printf("Kết quả thứ %d: \n ", ++count);
121
122 /* giá trị thứ i trong kết quả là chỉ số cột
123 * của quân hậu có chỉ số hàng là i */
140 void print_result_board(int const* cols, int n) {
141 static int count = 0
149 for(int i = n-1; i >= 0; i) {
150 for(int j = 0; j < cols[i]; ++j)
Trang 8167 * Description: Tìm vị trị đặt quân hậu tại hàng thứ curr_row
168 * thoả mãn yêu cầu
169 * =============================================================
170 */
171 void find(int curr_row, int n) {
172 for(int curr_col = 0; curr_col < n; ++curr_col) {
173 if(a[curr_col] /* cột curr_col chưa bị chiếm */
174 /* đường chéo loại I curr_row + curr_col chưa bị chiếm */
175 && b[curr_row + curr_col]
176 /* đường chéo loại II curr_row - curr_col chưa bị chiếm */
177 && c[curr_row - curr_col])
179 cols[curr_row] = curr_col;
180
181 // Cập nhật trạng thái
182 a[curr_col] = b[curr_row + curr_col]
193 a[curr_col] = b[curr_row + curr_col]
Trang 91.1.4 Kết quả
Với N = 8, có tất cả 92 kết quả khác nhau (46 nếu tính 2 kết quả đối xứng nhau là 1)
9
Trang 12Có thể in kết quả dạng bàn cờ trực quan hơn, thay vì các con số chỉ vị trí:
./queen_recursive 5 board
Nếu kết quả quá dài, có thể lưu vào file Ví dụ: với N = 12 (có 14200 kết quả)
./queen_recursive 12 board > qr12.txt
Trang 131.2 Cách giải không đệ quy
1.2.1 Phân tích
Theo thuật toán đệ quy:
1 def find(i):
10 a[j] = b[i+j] = c[i-j] = True
Giả sử cấu hình hiện tại đang là cols = (j0, j1, , ji−1) Ta thấy:
Việc lựa chọn giá trị ji trong khoảng từ 0 đến N-1 ở dòng (2) được bắt đầu ngay saukhi đã tìm được ji−1 trước đó Và tiếp tục sau khi đã kết thúc việc tìm kết quả ứng vớicấu hình cols = (j0, j1, , ji−1, ji− 1),
Dòng (5) được thực hiện ngay sau khi chấp nhận một ji,
Dòng (10) được thực hiện khi hoặc là đã in ra một kết quả, hoặc là việc chấp nhận j
ở dòng (4) không dẫn ra được một kết quả đúng
Do đó ta có thể khử đệ quy bằng phép lặp, dùng một biến i để lưu giá trị của hàng hiệntại Cho ji thử lần lượt các giá trị từ 0 đến N-1, nếu thoả mãn thì cập nhật trạng thái, tăng
i lên 1 và tiếp tục với i + 1 Cho đến khi i == N − 1 thì in kết quả và trả lại trạng thái.Nếu không còn ji nào thoả mãn thì giảm i đi 1 và trả lại trạng thái cũ Quá trình kết thúckhi i == 0 và không còn giá trị j0 nào thoả mãn nữa (đã hết cấu hình để thử) Ta có mã giảnhư sau:
8 # Neu o (i, j) chua bi chiem
9 if a[j] && b[i+j] && c[i-j]:
10 a[j] = b[i+j] = c[i-j] = False
Trang 1418 # Tra lai trang thai cu
20 elif i > 0:
21 # Tro lai thu gia tri tiep theo cua cols[i-1]
23
24 # Tra lai trang thai cu
25 a[j] = b[i+j] = c[i-j] = True
23 void print_result(int const*, int);
24 void print_result_board(int const*, int);
25 void (*print_func)(int const*, int) = print_result;
Trang 1567 void print_result(int const *cols, int n) {
68 static int count = 0
69
70 printf("Kết quả thứ %d: \n ", ++count);
71
72 /* giá trị thứ i trong kết quả là chỉ số cột
73 * của quân hậu có chỉ số hàng là i */
Trang 1680 /* - end of function print_result - */
90 void print_result_board(int const *cols, int n) {
91 static int count = 0
99 for(int i = n-1; i >= 0; i) {
100 for(int j = 0; j < cols[i]; ++j)
122 bool b[2*n]; // Đường chéo 1, i + j = const
123 bool raw_c[2*n], *c= raw_c + n; // Đường chéo 2, i - j = const
Trang 17128 a[i] = b[i] = raw_c[i] = true;
129 for(int i = n; i < 2*n; ++i)
130 b[i] = raw_c[i] = true;
137 if(a[curr_col] /* cột curr_col chưa bị chiếm */
138 /* đường chéo loại I curr_row + curr_col chưa bị chiếm */
139 && b[curr_row + curr_col]
140 /* đường chéo loại II curr_row - curr_col chưa bị chiếm */
141 && c[curr_row - curr_col])
143 // Cập nhật trạng thái
144 a[curr_col] = b[curr_row + curr_col]
153 a[curr_col] = b[curr_row + curr_col]
161 a[curr_col] = b[curr_row + curr_col]
Trang 192 Bài toán tháp Hà Nội
Đề bài: Có 3 cọc a, b, c Trên cọc a có một chồng gồm n cái đĩa đường kính giảm dần từdưới lên trên Cần phải chuyển chồng đĩa từ cọc a dang cọc c tuân thủ qui tắc: mỗi lần chỉchuyển một đĩa và chỉ được xếp đĩa có đường kính nhỏ hơn lên đĩa có đường kính lớn hơn.Trong quá trình chuyển được phép dùng cọc b làm cọc trung gian
2.1 Cách giải đệ quy
2.1.1 Phân tích
Với n = 1, ta chỉ việc chuyển đĩa từ cọc a sang cọc c
Với n = 2, ta có thể di chuyển 1 đĩa (cái nhỏ hơn) từ cọc a sang cọc b, chuyển đĩa còn lại ởcọc a sang cọc c và cuối cùng chuyển đĩa đã ở cọc b sang cọc c Cả ba bước di chuyển này đềhợp lệ
Lập luận tương tự như trên, ta có thể di chuyển n đĩa bất kỳ (n > 1) như sau:
1 Chuyển n − 1 đĩa từ cọc a sang cọc trung gian b,
2 Chuyển đĩa lớn nhất từ cọc a sang cọc đích c,
3 Chuyển n − 1 đĩa từ cọc b sang cọc c
Có thể chứng minh được việc di chuyển như trên mất 2n− 1 bước
2.1.2 Thuật toán
Cách làm như trên đưa bài toàn di chuyển n đĩa về bài toán di chuyển n − 1 đĩa, rồi n − 2đĩa, Rất thích hợp cho một thuật toán đệ quy:
1 # Ham thuc hien viec di chuyen
2 def move(n, source, dest, mid):
3 if n > 1
4 move(n-1, source, mid, dest)
5 move( 1, source, dest, mid)
6 move(n-1, mid, dest, source)
Trang 2034 if(ndisk > MAX_DISK || ndisk == 0) {
35 fprintf(stderr, "Số đĩa không hợp lệ \n ");
49 * Description: Di chuyển ndisk đĩa từ cột src đến cột dst,
50 * có thể sử dụng cột trung gian mid
51 * =============================================================
52 */
53 void move (size_t ndisk, char src, char dst, char mid) {
54 static size_t count = 0;
Trang 222.2 Cách giải không đệ quy
2.2.1 Phân tích thuật toán
Với bài toán tháp Hà Nội có rất nhiều các phương án để thực hiện việc phá bỏ đệ quynhư sử dụng vòng lặp, sử dụng stack và một trong số đó là sử dụng cây nhi phân(binarytree)
Như vậy chúng ta có thể mã hóa bài toán thông qua biểu diễn nhị phân của số thứ tự dichuyển, trong đó các dãy 1 và các dãy 0 tượng trưng cho các dãy đĩa liền kề nhau trên cùngmột cọc, và mỗi khi chữ số nào thay đổi thì đĩa kế tiếp dời sang trái hay phải một cọc(haychuyển sang cọc đối diện).Quy tắc:
1 Dãy bit đọc từ trái qua phải, và mỗi bit có thể được sử dụng để xác định vị trí tươngứng
2 Bit có cùng giá trị nghĩa là đĩa ương ứng được xếp chồng lên đỉnh đĩa trước trên cungmột cọc
3 Bit có cùng giá trị khác trước nghĩa là đĩa tương ứng có vị trí bên trái hoặc phải.Xácđịnh bên trái hay phải theo quy tắc sau:
Giả thiết cọc đích nằm bên trái và cọc nguồn nằm bên phải
Cũng giả thiết:cọc phải được tính như là cọc trái của cọc trái và ngược lại
Giả sử n là số đĩa lớn nhất trên cùng một cọc.Nếu n chẵn , đĩa sẽ được đặt bên trái,nếu n lẻ đĩa được đặt ở cọc phải
Để dễ hiểu ta xét một ví dụ bài toán tháp Hà Nội với ndisk = 3
(3 cọc lần lượt là a, b, c) Với ndisk = 3:
1 Ba đĩa cọc A kí hiệu là (000)2
2 Chuyển đĩa 1 từ a sang c, kí hiệu (001)2 = 110
Chữ số 1 biểu thị đĩa trên cùng được chuyển
3 Chuyển đĩa 2 từ a sang b, kí hiệu (010)2
Chữ số 1 biểu thị đĩa thứ 3 đã được chuyển, ba chữ số 0, 1, 0 biểu thị ba đĩa ở ba cọckhác nhau
4 Chuyển đĩa 1 từ c sang b, kí hiệu (011)2 = (3)10
Chữ số 1 ở vị trí thứ nhất biểu thị đĩa số 1 đã được chuyển Ba chữ số 0, 1, 1 biểu thịhai đĩa nhỏ ở trên cùng cọc c (đĩa thứ hai chồng lên đĩa thứ nhất), đĩa lớn ở cọc khác(cọc a) Cọc c trống
5 Chuyển đĩa số 3 từ cọc a sang cọc c, kí hiệu là (100)2
Chữ số 1 thứ ba biểu thị đĩa số 3 đã được chuyển Ba chữ số 1, 0, 0 biểu thị hai đĩanhỏ ở trên cùng một cọc b (đĩa thứ hai chồng lên đĩa thứ nhất) và không chuyển, đĩalớn ở cọc khác (cọc c) Cọc a trống
Trang 236 Chuyển đĩa số 1 từ cọc b sang cọc a, kí hiệu là (101)2.
Chữ số 1 thứ ba (khác số 0 trong bước 5) biểu thị đĩa số 3 đã được chuyển.Ba chữ số
1, 0, 1 khác nhau biểu thị ba đĩa ở ba cọc khác nhau
7 Chuyển đĩa số 2 từ cọc b sang cọc c, kí hiệu là (110)2
Chữ số 1 thứ n (khác số 0 trong bước 5) biểu thị đĩa số 3 đã được chuyển.Ba chữ số 1,
0, 1 biểu thị ba đĩa ở ba cột khác nhau
8 Chuyển đĩa 2 từ cột b sang c
9 Chuyển đĩa 1 từ b sang c
Với cách thực hiện trên thì nguồn và cái cọc đích cho lần di chuyển thứ k có thể được tìmthấy từ sự biểu diễn nhị phân của k sử dụng toán tử phân theo bit (bitwise operations) Để sửdụng cú pháp của ngôn ngữ lập trình C, di chuyển k từ cọc (k& k-1)% 3 tới cọc ((k|k-1)+1)%3,nơi mà cái đĩa bắt đầu rừ cọc 0 và kết thúc ở cọc 1 và 2 tùy theo số đĩa là chãn hay lẻ.Thêm một phát biểu khác là từ (k-(k-& k )% 3) tới cọc (k+(k-& k )% 3)
2.2.2 Thuật toán
Ta có mã giả:
1 # Ham thuc hien viec di chuyen
2 def move(size_t ndisk, int source, int dest, int mid):
Trang 2433 if(ndisk > MAX_DISK || ndisk == 0) {
34 fprintf(stderr, "Số đĩa không hợp lệ \n ");
47 * Description: Di chuyển ndisk đĩa từ cột src đến cột dst,
48 * có thể sử dụng cột trung gian mid
Trang 25Xét với ndisk = 5 ta có:
25