Là tài liệu được trình bày từ cơ bản đến nâng cao về thuật toán quay lui, quy hoạch động, tham lam trong việc giải các bài toán thi học sinh giỏi Tin học. Có các bài tập minh họa và hướng dẫn cách giải từ đề thi HSG tỉnh, Quốc gia các năm
CHUYÊN ĐỀ THIẾT KẾ THUẬT TOÁN I Chia để trị giải thuật đệ quy Chia để trị a Định nghĩa đệ quy Ta nói đối tượng đệ quy định nghĩa qua đối tượng khác dạng với Một toán P mang chất đệ quy lời giải toán P thực lời giải tốn P' có dạng giống P P' có dạng giống P, P' phải “nhỏ” P, dễ giải P việc giải khơng cần dùng đến P b Chia để trị Chia để trị (divide and conquer) phương pháp thiết kế giải thuật cho toán mang chất đệ quy: Để giải toán lớn, ta phân rã thành tốn đồng dạng, tiến hành phân rã tốn đủ nhỏ để giải trực tiếp Sau nghiệm tốn phối hợp lại để nghiệm tốn lớn có nghiệm tốn ban đầu Khi tốn tìm thuật giải phương pháp chia để trị? Có thể tìm thấy câu trả lời qua việc giải đáp câu hỏi sau: Có thể định nghĩa toán dạng phối hợp tốn loại nhỏ hay khơng? Khái niệm “nhỏ hơn” nào? ( Xác định quy tắc phân rã toán) Trường hợp đặc biệt tốn coi đủ nhỏ để giải trực tiếp được? ( Xác định toán sở) Giải thuật đệ quy Trong ngơn ngữ lập trình, giải thuật đệ quy thường cài đặt chương trình đệ quy Trong Pascal, ta thấy nhiều ví dụ hàm thủ tục có chứa lời gọi tới nó, gọi đệ quy Một chương trình đệ quy gồm hai phần: Phần neo (anchor): Phần thực mà công việc q đơn giản, giải trực tiếp khơng cần phải nhờ đến toán Phần đệ quy: Trong trường hợp toán chưa thể giải phần neo, ta xác định toán gọi đệ quy giải tốn Một số ví dụ a Tính giai thừa N 1, N=0 � �N *( N 1)!, N �1 Bài tốn : Tính N ! � Vì N! = N*(N-1)! Nên để tính N! (bài tốn lớn) ta tính (N-1)! (bài tốn con) lấy kết nhân với N function Factorial(n: Integer): Int64; begin if n = then Result := //Phần neo else Result := n * Factorial(n - 1); //Phần đệ quy end; Ví dụ : dùng hàm để tính 3! 3! = * 2! 2! = * 1! 1! = * 0! 0! = b Tính dãy fibonaci Bài tốn : Dãy số Fibonacci bắt nguồn từ toán cổ việc sinh sản cặp thỏ Bài toán đặt sau: Các thỏ không chết Hai tháng sau đời, cặp thỏ sinh cặp thỏ (một đực, cái) Khi sinh tháng chúng lại sinh cặp Giả sử từ đầu tháng có cặp đời đến tháng thứ có cặp Ví dụ n = 5, ta thấy: Giữa tháng thứ 1: cặp (ab) (cặp ban đầu) Giữa tháng thứ 2: cặp (ab) (cặp ban đầu chưa đẻ) Giữa tháng thứ 3: cặp (AB)(cd) (cặp ban đầu đẻ thêm cặp con) Giữa tháng thứ 4: cặp (AB)(cd)(ef) (cặp ban đầu tiếp tục đẻ) Giữa tháng thứ 5: cặp (AB)(CD)(ef)(gh)(ik) (cả cặp (AB) (CD) đẻ) Bây giờ, ta xét tới việc tính số cặp thỏ tháng thứ n : f(n) Nếu ta tháng n-1 tính số thỏ tháng n thì: Số tháng tới = Số có + Số sinh tháng tới Mặt khác, với tất cặp thỏ ≥ tháng tuổi sang tháng sau, chúng ≥ tháng tuổi sinh Tức là: Số sinh tháng tới = Số tháng trước Vậy: Số tháng tới = Số có + Số tháng trước f(n) = f(n-1) + f(n-2) Vậy tính f(n) theo cơng thức sau: 1, n �2 � f (n) � �f (n 1) f (n 2), n>2 function f(n: Integer): Int64; begin if n ≤ then Result := //Phần neo else Result := f(n - 1) + f(n - 2); //Phần đệ quy end; c Tính lũy thừa Bài tốn: Cho hai số dương x, n Hãy tính giá trị xn Thuật tốn : Tính trực tiếp vịng lặp function Power(x, n: Integer): Int64; var i: Integer; begin Result := 1; for i := to n Result := Result * x; end; Thuật toán : Xét thuật tốn chia để trị dựa vào tính chất sau xn : 1, n=0 � x n � n 1 �x * x , n>0 function Power(x, n: Integer): Int64; begin if n = then Result := else Result := x * Power(x, n – 1); end; Thuật toán : Xét thuật toán chia để trị dựa vào tính chất sau xn : 1, n=0 � � n /2 x � ( x ) , n>0; n mod 2=0 � n /2 ( x ) *x, n>0; n mod �0 � n function Power(x, n: Integer): Int64; begin if n = then Result := else begin Result := Power(x, n div 2); Result := Result * Result; if n mod = then Result := Result * x; end; end; b Tháp hà nội Bài toán : Đây tốn mang tính chất trị chơi, tương truyền ngơi đền Benares có ba cọc kim cương Khi khai sinh giới, thượng đế đặt 64 đĩa vàng chồng lên theo thứ tự giảm dần đường kính tính từ lên, đĩa to đặt cọc Các nhà sư chuyển đĩa sang cọc khác theo luật: Khi di chuyển đĩa, phải đặt vào vị trí ba cọc cho Mỗi lần chuyển đĩa phải đĩa chồng đĩa Tại vị trí, đĩa chuyển đến phải đặt lên Đĩa lớn không phép đặt lên đĩa nhỏ (hay nói cách khác: đĩa đặt cọc đặt đĩa lớn hơn) Ngày tận đến toàn chồng đĩa chuyển sang cọc khác Thuật tốn : Giả sử có n đĩa Nếu n=1 ta chuyển đĩa từ cọc sang cọc xong Nếu ta có phương pháp chuyển n-1 đĩa từ cọc sang cọc 2, tổng quát, cách chuyển n-1 đĩa từ cọc x sang cọc y (1≤x,y≤3) tương tự Giả sử ta có phương pháp chuyển n-1 đĩa hai cọc Để chuyển n đĩa từ cọc x sang cọc y, ta gọi cọc lại z (=6-x-y) Coi đĩa to … cọc, chuyển đĩa cịn lại từ cọc x sang cọc z, sau chuyển đĩa to sang cọc y cuối lại coi đĩa to cọc, chuyển n-1 đĩa lại cọc z sang cọc y chồng lên đĩa to Như để chuyển n đĩa (bài toán lớn), ta quy hai phép chuyển n-1 đĩa (bài toán nhỏ) phép chuyển đĩa (bài tốn sở) Cách làm thể thủ tục đệ quy đây: procedure Move(n, x, y: Integer); begin if n = then Output ← Chuyển đĩa từ x sang y else begin Move(n-1, x, 6-x-y); //Chuyển n-1 đĩa từ cọc x sang cọc trung gian Move(1, x, y); //Chuyển đĩa to từ x sang y Move(n-1, 6-x-y, y); //Chuyển n-1 đĩa từ cọc trung gian sang cọc y end; end; II Bài tốn liệt kê Có số tốn thực tế yêu cầu rõ: tập đối tượng cho trước có đối tượng thoả mãn điều kiện định đối tượng Bài toán gọi toán liệt kê hay toán duyệt Nếu ta biểu diễn đối tượng cần tìm dạng cấu hình biến số để giải tốn liệt kê, cần phải xác định thuật toán để theo xây dựng tất cấu hình quan tâm Có nhiều phương pháp liệt kê, chúng cần phải đáp ứng hai yêu cầu đây: Không lặp lại cấu hình Khơng bỏ sót cấu hình Trước nói thuật tốn liệt kê, giới thiệu số khái niệm bản: Lý thuyết tập hợp - Tập hợp khái niệm toán học : tập hợp học sinh lớp, tập hợp số nguyên… - Số phần tử tập hợp liệt kê nêu tính chất đặc trưng a Ngun lí cộng - Giả sử làm cơng việc A có phương pháp Phương pháp có m cách làm Phương pháp có n cách làm - Khi số cách làm cơng việc A m + n cách Ví dụ : An có áo tay dài, áo tay ngắn để chọn áo có cách? Ví dụ : Có xâu nhị phân độ dài N bit khơng có hai bit liền kề Ta có hệ thức truy hồi : an = an-1 + an-2 b Ngun lí nhân - Giả sử làm cơng việc A có bước Bước có m cách làm Bước có n cách làm - Khi số cách làm công việc A m * n cách Ví dụ : Từ A tới B có đường, từ B tới C có đường để từ A tới C có cách? c Chỉnh hợp lặp - Xét tập hữu hạn gồm n phần tử A = {a1, a2, , an} - Một chỉnh hợp lặp chập k n phần tử (k lớn n) có thứ tự gồm k phần tử A, phần tử lặp lại - Theo ngun lí nhân, số tất chỉnh hợp lặp chập k n n k Ank n k Ví dụ : Có xâu nhị phân độ dài N bit Ví dụ : Có thể lập số có chữ số? d Chỉnh hợp không lặp - Một chỉnh hợp không lặp chập k n phần tử (k < n) có thứ tự gồm k thành phần lấy từ n phần tử tập cho Các thành phần không lặp lại - Để xây dựng chỉnh hợp không lặp, ta xây dựng dần từ thành phần Thành phần có n khả lựa chọn Mỗi thành phần tiếp theo, khả lựa chọn giảm 1, theo nguyên lí nhân, số chỉnh hợp không lặp chập k n n(n -1) (n-k + 1) Ank n(n 1) (n k 1) n! ( n k )! Ví dụ 1: Có số tự nhiên ba chữ số tạo số 1, 2, 3, 4, 5, e Hoán vị - Một hoán vị n phần tử cách xếp thứ tự phần tử Một hốn vị n phần tử xem trường hợp riêng chỉnh hợp khơng lặp k = n Do số hốn vị n phần tử n! Ví dụ 1: Có số tự nhiên năm chữ số tạo số 1, 2, 3, 4, f Tổ hợp - Một tổ hợp chập k n phần tử (k < n) không kể thứ tự gồm k thành phần khác lấy từ n phần tử tập cho Cnk n(n 1) (n k 1) n! k! k !(n k )! Ví dụ Có đội thi đấu vịng trịn lượt Hỏi phải tổ chức trận đấu? Ví dụ : Có tập k phần tử tập S = {1, 2, 3, 4} Kết tổ hợp chập k n phần tử Thuật toán quay lui a Tổng quan : Thuật toán vét cạn dùng để giải toán liệt kê cấu hình theo cách: - Mỗi cấu hình xây dựng cách xây dựng phần tử, - Mỗi phần tử chọn cách thử tất khả Giả sử cấu hình cần liệt kê có dạng x1 n, thuật tốn quay lui xét tất giá trị x1 nhận, thử cho x1 nhận giá trị Với giá trị thử gán cho x 1, thuật tốn xét tất giá trị x2 nhận, lại thử cho x2 nhận giá trị Với giá trị thử gán cho x2 lại xét tiếp khả chọn x 3, tiếp tục vậy… Mỗi ta tìm đầy đủ cấu hình liệt kê cấu hình b Mơ hình : procedure Attempt(i); begin for «mọi giá trị v gán cho x[i]» begin «Thử cho x[i] := v»; if «x[i] phần tử cuối 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 bắt đầu lời gọi Attempt(1) Tên gọi thuật toán quay lui dựa chế duyệt cấu hình: Mỗi thử chọn giá trị cho xi, thuật tốn gọi đệ quy để tìm tiếp x i+1, … tiến trình duyệt xét tìm tới phần tử cuối cấu hình Cịn sau xét hết tất khả chọn, tiến trình lùi lại thử áp đặt giá trị khác cho xi-1 c Một số ví dụ Bài toán : Liệt kê dãy nhị phân độ dài n Biểu diễn dãy nhị phân độ dài n dạng dãy x n Ta liệt kê dãy cách thử dùng giá trị {0,1} gán cho xi Với giá trị thử gán cho xi lại thử giá trị gán cho xi+1 … procedure Attempt(i: Integer); //Thử cách chọn x[i] var j: AnsiChar; begin for j := '0' to '1' //Xét giá trị j gán cho x[i] begin //Với giá trị x[i] := j; //Thử đặt x[i] if i = n then WriteLn(x) //Nếu i = n in kết else Attempt(i + 1); //Nếu x[i] chưa phải phần tử cuối tìm tiếp x[i + 1] end; end; Ví dụ n=3 ta vẽ đệ quy sau : Bài toán : Liệt kê tập có k phần tử Bài tốn liệt kê tập k phần tử tập S = {1, 2, …, n} quy tốn liệt kê dãy k phần tử x1 k, 1≤x1