Ở bước phân tích, bài toán lớn được phân tích thành bài toán đồng dạng nhưng đơn giản hơn.. Bước phân tích sẽ dừng lại khi chúng ta phân tích đến bài toán đồng dạng đơn giản nhất mà ở
Trang 1THUẬT TOÁN ĐỆ QUY MINH HỌA BẰNG FREE PASCAL
Trong thế giới lập trình, có rất nhiều thuật toán như: các thuật toán tìm kiếm, sắp xếp, quy hoạch động, đồ thị, Trong đó có một thuật toán rất nổi tiếng, đó là Đệ quy
Nếu bạn là một người đã và đang học lập trình, chắc hẳn bạn đã tìm hiểu hoặc ít nhất cũng nghe nói
về thuật toán kinh điển này Thuật toán này được ứng dụng cũng khá rộng rãi trong tin học và cả toán học Có rất nhiều bài toán được giải quyết bằng thuật toán Đệ quy rất hiệu quả Thậm chí có những bài toán chỉ có thể suy nghĩ theo cách Đệ quy mới giải quyết được
Trang 21 Vấn đề Đệ quy
1.1 Các ví dụ:
Ví dụ 1: Hãy tính S(n) = 1+2+3+…+n (tính tổng của n số nguyên dương đầu tiên)
Nếu n = 10, chúng ta có thể giải bằng cách cộng 10 số nguyên dương đầu tiên, kết quả là 55:
S(10) = 1+2+3+…+10 = 55 Nếu tính tiếp n = 11 chúng ta cũng có thể giải bài toán với như trên, nghĩa là cộng 11 số nguyên dương đầu tiên và kết quả là 66 Tuy nhiên trong thực tế để tính S(11) khi đã biết S(10) người ta lấy kết quả của S(10) cộng thêm cho 11:
S(11) = S(10) + 11 = 66 Bài toán tính S(n) được giải tổng quát qua hai bước phân tích và thế ngược như sau:
(1) Bước phân tích:
Để tích S(n) trước tiên chúng ta phải tính S(n-1), sau đó tính S(n) = S(n-1) + n
Để tích S(n-1) trước tiên chúng ta phải tính S(n-2), sau đó tính S(n-1) = S(n-2) + n-1
…
Để tích S(2) trước tiên chúng ta phải tính S(1), sau đó tính S(2) = S(1) + 1
Và cuối cùng S(1) có ngay kết quả là 1
Bước phân tích của bài toán tính S(n)
S(n) = S(n-1) + n
S(n-1) = S(n-2) + n-1
…
…
S(2) = S(1) + 1
S(1) = 1
(2) Bước thế ngược:
Có kết quả của S(1) chúng ta thay nó vào biểu thức tính S(2) và tính được S(2), có S(2) chúng ta sẽ tính được S(3),…, có S(n-1) chúng ta sẽ tính được S(n), và bài toán đã được giải quyết
Bước thế ngược của bài toán tính S(n) S(1)= 1
S(2) = S(1) + 2 = 3
S(3) = S(2) +3 = 6
…
S(n) = S(n-1) + n
Ví dụ 2: Tính P(n) = xn với x là số thực và n là số nguyên ≥ 0
Tương tự như bài toán trên, bài toán tính P(n) được giải tổng quát qua hai bước là bước phân tích và bước thế ngược như sau:
(1) Bước phân tích:
Để tính P(n) trước tiên chúng ta phải tính P(n-1), sau đó tính P(n) = P(n-1) * x
Để tính P(n-1) trước tiên chúng ta phải tính P(n-2), sau đó tính P(n-1) = P(n-2) * x
…
Để tính P(1) trước tiên chúng ta phải tính P(0), sau đó tính P(1) = P(0) * x
Và cuối cùng P(0) có ngay kết quả là 1
Bước phân tích của bài toán tính P(n)
Trang 3P(n) = P(n-1) * x
P(n-1) = P(n-2) + x
…
…
P(1) = P(0) * 1
P(0) = 1
(2) Bước thế ngược:
Có kết quả của P(0) chúng ta thay nó vào biếu thức tính P(1) và tính được P(1), có P(1) chúng ta sẽ tính được P(2),…, có P(n-1) chúng ta sẽ tính được P(n), và bài toán đã được giải quyết
Bước thế ngược của bài toán tính S(n) P(0)= 1
P(1) = P(0) * x = x
P(2) = P(1) * x = x*x
…
P(n) = P(n-1) * x Lưu ý:
Trong quá trình giải hai bài toán trên giống nhau ở chỗ đều tiến hành giải qua hai bước là bước phân tích và bước thế ngược
Ở bước phân tích, bài toán lớn được phân tích thành bài toán đồng dạng (nhưng đơn giản hơn) Bước phân tích sẽ dừng lại khi chúng ta phân tích đến bài toán đồng dạng đơn giản nhất mà ở đó chúng ta có thể xách định kết quả một cách trực tiếp
Ở bước thế ngược giúp chúng ta tuần tự xác định kết quả các bài toán đồng dạng (từ đơn giản đến phức tạp hơn), và cuối cùng chúng ta sẽ xác định được kết quả của bài toán
1.2 Vấn đề Đệ quy là gì?
Vấn đề Đệ quy là vấn đề được định nghĩa bằng chính nó, ví dụ:
Tính tổng S(n) (tính tổng n số nguyên dương đầu tiên) được định nghĩa thông qua S(n-1) (tính tổng n-1 số nguyên dương đầu tiên)
Tính P(n) ( tính xn) được định nghĩa thông qua P(n-1) (tính (xn-1)
Vấn đề Đệ quy thường được giải qua hai bước là bước phân tích và bước thế ngược Bài toán (hay vấn đề) giải quyết theo phương pháp Đệ quy cần hai điều kiện sau để hiện hữu (tồn tại) tính Đệ quy
và không bị gọi Đệ quy bất tận (bị loop):
(1) Để hiện hữu (tồn tại) tính Đệ quy – nghĩa là để giải một bài toán chúng ta phải giải các bài toán đồng dạng, để giải bài toán đồng dạng này chúng ta phải giải bài toán đồng dạng khác – phải tồn tại bước Đệ quy Bài toán 1 có bước Đệ quy là S(n) = S(n-1) + n, bài toán 2 có bước Đệ quy là
xn=xn-1*x
(2) Để không bị gọi Đệ quy bất tận (bị loop) thì phải có điều kiện dừng Bài toán 1 có điều kiện dừng là S(1) = 1, bài toán 2 có điều kiện dừng là x0 =1
=> Bài toán Đệ quy là những bài toán này có thể được phân rã thành các bài toán nhỏ hơn, đơn giản hơn nhưng có cùng dạng với bài toán ban đầu Những bài toán nhỏ lại được phân rã thành các bài toán nhỏ hơn Cứ như vậy, việc phân rã chỉ dừng lại khi bài toán con đơn giản đến mức có thể suy
ra ngay kết quả mà không cần phải phân rã nữa Ta phải giải tất cả các bài toán con rồi kết hợp các kết quả đó lại để có được lời giải cho bài toán lớn ban đầu Cách phân rã bài toán như vậy gọi là
"chia để trị" (devide and conquer), là một dạng của Đệ quy
2 Chương trình con Đệ quy
Trang 4Một chương trình con (hàm hoặc thủ tục) được gọi là Đệ quy nếu trong quá trình thực hiện nó có phần phải gọi đến chính nó
Ví dụ 1: Hàm tính lũy thừa nguyên của một số thực x (tính xn)
Function LT(x: real, n: integer): real;
Begin
If n=0 then LT:=1 else
LT:=LT(x,n-1)*x;
End;
Khi có lệnh gọi hàm, chẳng hạn: a:=LT(3,4);
Thì máy sẽ nhớ là: LT(3,4):=3*LT(3,3) và đi tính LT(3,3);
Kế tiếp máy lại phải ghi nhớ: LT(3,3):=3*LT(3,2) và đi tính LT(3,2);
Kế tiếp máy lại phải ghi nhớ: LT(3,2):=3*LT(3,1) và đi tính LT(3,1);
Kế tiếp máy lại phải ghi nhớ: LT(3,1):=3*LT(3,0) và đi tính LT(3,0);
Theo định nghĩa của hàm LT: LT(3,0):=1;
Máy sẽ quay ngược lại: LT(3,1):=3*1=3;
Rồi tiếp tục quy ngược lại: LT(3,2):=3*3=9; LT(3,3):=9*3=27; LT(3,4):=27*3:=81;
Ví dụ 2: Hàm tính giai thừa của n (tính n!)
Function GT(n: word): longint;
Begin
If n=1 then GT:=1 else
GT:=GT(n-1)*n;
End;
3 Cấu trúc chính của một chương trình con Đệ quy
Một chương trình con Đệ quy vè căn bản gồm 2 phần:
(1) Phần cố định (điều kiện dừng):
Trong đó chứa các tác động của hàm hoặc thủ tục với với một số giá trị của thể ban đầu của tham
số
Trong ví dụ 1: If n=0 then LT:=1;
Trong ví dụ 2: If n=1 then GT:=1;
(2) Phần hạ bậc
Trong đó tác động cần được thực hiện cho giá trị hiện thời của các tham số được định nghĩa bằng các tác động đã được định nghĩa trước đây với kích thước nhỏ hơn của tham số:
Trong ví dụ 1: If n > 0 then LT:=LT(x,n-1)*x;
Trong ví dụ 2: If n > 1 then GT:=GT(n-1)*n;
4 Các loại Đệ quy
4.1 Đệ quy đuôi
Đệ quy đuôi là dạng Đệ quy mà trong một cấp Đệ quy, chỉ có lời gọi Đệ quy duy nhất xuống cấp thấp
Ví dụ 1: Hàm tính giai thừa của n (tính n!)
Funtion GT(n: word): longint;
Trang 5Begin
If n:=1 then GT:=1 else
GT:=GT(n-1)*n;
End;
Ví dụ 2: Tìm vị trí xuất hiện của số x trong dãy a gồm n số được sắp tăng dần Ta giải bài toán này
bằng tìm kiếm nhị phân
Tư tưởng thuật toán: Xét số ở chính giữa dãy số gội là phần tử giữa, nếu x lớn hơn số này, nghĩa
là x phải nằm ở dãy số phía bên phải phần tử giữa, nếu x nhỏ hơn thi x phải nằm ở dãy số bên trái phần tử giữa, nếu x bằng giữa thì đã tìm được x
Type DaySo=Array[1 100] of integer;
Function Tim(a:Dayso;left, right:integer): integer;
Begin
If (left>right) then Tim:= -1 Else
Begin
Mid:=(right+left) div 2;
If (a[mid]=x) then Tim:=mid Else
If x>a[mid] then Tim(mid+1,right) Else Tim:=Tim(left, mid -1);
End;
End;
Nhận xét: Chúng ta thấy lời gọi Đệ quy xuất hiện hai lần trong hàm (Tim(mid+1,right) và Tim(left,
mid -1), nhưng trong một lần thi hành hàm chỉ một trong hai được thi hành
4.2 Đệ quy nhánh
Đệ quy nhánh là dạng Đệ quy mà trong suốt quá trình thực hiện hàm Đệ quy, lời gọi hàm Đệ quy được thực hiện nhiều hơn một lần
Ví dụ 1: Bài toán tháp Hà Nội
Cho n dĩa tròn có kích thước khác nhau và được đặt xuyên qua một cọc sao cho dĩa nhỏ ở trên dĩa
to Hãy chuyển toàn bộ số dĩa này sang một cọc khác với điều kiện: Khi chuyển, khi chuyển, có thể chuyển dĩa sang một cọc trung gian khác và trên mỗi cọc, dĩa nhỏ luôn nằm trên dĩa lớn
Để đơn giản, gọi cọc nguồn là cọc A, cọc đích là B và cọc trung gian là cọc C Các dĩa được đánh
số theo thứ tự từ 1 đến n Dĩa có số lớn hơn thì đường kính lớn hơn
Thuật giải Đệ quy
Để chuyển n dĩa từ cọc A sang cọc B thì cần:
1 chuyển n-1 dĩa từ A sang C Chỉ còn lại dĩa n trên cọc A
2 chuyển dĩa n từ A sang B
3 chuyển n-1 dĩa từ C sang B cho chúng nằm trên dĩa n
Trang 6Phương pháp trên được gọi là thuật giải Đệ quy: Để tiến hành bước 1 và 3, áp dụng lại thuật giải
cho n-1 Toàn bộ quá trình là một số hữu hạn các bước, vì đến một lúc nào đó thuật giải sẽ áp dụng cho n = 1 Bước này chỉ đơn giản là chuyển một dĩa duy nhất từ cọc A sang cọc C
Giải thích thuật giải với n=3:
1 chuyển đĩa 1 sang cọc C
2 chuyển đĩa 2 sang cọc B
3 chuyển đĩa 1 từ C sang B sao cho nó nằm lên 2
Vậy ta hiện có 2 đĩa đã nằm trên cọc B, cọc C hiện thời trống
1 chuyển đĩa 3 sang cọc C
2 lặp lại 3 bước trên để chuyển 1 & 2 cho nằm lên 3
Mỗi lần dựng xong tháp từ đĩa i đến 1, chuyển đĩa i+1 từ cọc A là cọc xuất phát, rồi lại di chuyển tháp đã dựng lên đĩa i+1
Program ThapHaNoi;
Var n:integer;
Procedure ChuyenDia(n:integer; nguon,dich:char);
Begin
writeln('chuyen dia: ',n,' tu coc ',nguon,' sang coc ',dich);
End;
Procedure ChuyenChongDia(n:integer; nguon,dich:char);
Var trunggian:char;
Begin
if(nguon<>'A') and (Dich<>'A') then trunggian:='A';
if(nguon<>'B') and (Dich<>'B') then trunggian:='B';
if(nguon<>'C') and (Dich<>'C') then trunggian:='C';
if n=1 then ChuyenDia(n,nguon,dich)
else Begin
ChuyenChongDia(n-1,nguon,trunggian);
ChuyenDia(n,nguon,dich);
ChuyenChongDia(n-1,trunggian,dich);
End;
End;
Begin
Write('Nhap vao so dia: '); readln(n);
ChuyenChongDia(n,'A','B');
readln
End
Nhận xét: Trong một lần thi hành, có hai lời gọi Đệ quy – Đệ quy rẽ nhánh
Ví dụ 2: Liệt kê tất cả hoán vị của một dãy n phần tử khác nhau (tổng số hoán vị của dãy n phần tử
là n!) Chẳng hạn với dãy có 3 phần tử là ABC thì tất cả hoán vị là ABC, ACB, BAC, BCA, CAB, CBA
Tư tưởng của thuật toán này như sau:
Xét các phần tử ai từ 1 đến n (n là chiều dài dãy số - DaySo):
+ Bỏ phần tử ai ra khỏi dãy số
+ Ghi nhận đã lấy ra phần tử ai
+ Hoán vị (dãy số)
Trang 7+ Đưa phần tử ai vào lại dãy số
Nếu dãy số rỗng thì thứ thự các phần tử được lấy ra chính là một hoán vị
Ví dụ: Dãy số ABC
Bỏ A (còn BC)
Bỏ B (còn C)
Bỏ C -> 1 hoán vị ABC
Bỏ C (còn B)
Bỏ B -> 1 hoán vị ACB
Bỏ B (còn AC)
Bỏ A (còn C)
Bỏ C -> 1 hoán vị BAC
Bỏ C (còn A)
Bỏ A -> 1 hoán vị BCA
Bỏ C (còn AB)
Bỏ A (còn B)
Bỏ B -> 1 hoán vị CAB
Bỏ B (còn A)
Bỏ A -> 1 hoán vị CBA
Program HoanViDaySo;
Var Day: Array[1 10] of char; // mảng lưu các phần tử
SoKyTu,i: Integer;// Số phần tử
DuocChon: Array[1 10] of boolean;//ghi nhận phần tử nào đã lấy ra khỏi dãy
Procedure HoanVi(kytu:string);
Var i:integer;
Chon: Boolean;// =false, không còn ký tự nào để lấy ra nữa, dãy rỗng
Begin
chon:=false;
for i:=1 to SoKyTu do Begin
if (DuocChon[i]=false) then Begin
DuocChon[i]:=True;
HoanVi(KyTu+Day[i]); // Lời gọi Đệ quy được gọi nhiều lần trong vòng lặp DuocChon[i]:=false;
Chon:=true;
End;
End;
if chon=false then writeln(KyTu);// dãy rỗng các phần tử đã chọn theo thứ tự là 1 HV End;
Begin
Write('Nhap vao so phan tu cua day: '); readln(SoKyTu);
For i:=1 to SoKyTu do Begin
Write('Nhap vao phan tu thu ',i,': ');
readln(Day[i]);
End;
For i:=1 to SoKyTu Do DuocChon[i]:=false;
Trang 8hoanvi('');
readln
End
4.3 Đệ quy tương hỗ tương
Đệ quy hỗ tương là dạng Đệ quy mà trong đó có sự gọi quay vòng như A gọi B, B gọi C rồi C lại gọi A Đây là trường hợp rất phức tạp nhưng cũng có nhiều ví dụ rất hấp dẫn
Ví dụ: Viết chương trình tính X(n) và Y(n) X(n) và Y(n) được xác định bởi công thức sau:
( ) = ( − 1) + ( − 1)
( ) = ( − 1) ∗ ( − 1) A(0) và B(0) =1
Cây nhị phân biểu diễn tính A(3) và B(3) như sau:
Chương trình:
Program TinhXnVaYn;
Var n:byte;
Function TinhYn(n:byte): longint; forward;// Từ khóa dùng để khai báo trước tên hàm.
Function TinhXn(n:byte): longint; forward;// Nếu không có thì không thể sự dụng Đệ quy
Function TinhXn(n:byte): longint; // tương hỗ
Trang 9Begin
if n=0 then TinhXn:=1
else TinhXn:=TinhXn(n-1)+TinhYn(n-1);
End;
Function TinhYn(n:byte): longint;
Begin
if n=0 then TinhYn:=1
else TinhYn:=TinhXn(n-1)*TinhYn(n-1);
End;
Begin
Write('Nhap vao n: '); readln(n);
Writeln('Gia tri cuar X(',n,') la: ',TinhXn(n));
Write('Gia tri cuar Y(',n,') la: ',TinhYn(n));
readln
End
5 Ưu và nhược điểm của Đệ quy
5.1 Ưu điểm
Đệ quy mạnh ở chỗ có thể định nghĩa một tập hợp rất lớn các tác động chỉ bởi một số ít mệnh đề Một chương trình viết theo giải thuật Đệ quy sẽ tự nhiên hơn đó là:
+ Sáng sủa
+ Dễ hiểu
+ Nêu bật được bản chất của vấn đề
Có nhiều bài toán cho đến nay vẫn chưa có cách giải không Đệ quy
5.2 Nhược điểm
Trong giải thuật Đệ quy các bài toán con có thể được gọi lại rất nhiều lần, dù trước đó kết quả của bài toán con đã được tính rồi Nên thực hiện giải thuật Đệ quy sẽ:
+ Tốn nhiều dung lượng
+ Cực kỳ chậm
6 Khử Đệ quy
Có một số giải thuật Đệ quy thuộc loại tính toán đơn giản có thể được thay thế bởi một giải thuật khác không tự gọi nó, sự thay thế đó được gọi là khử Đệ quy
Tuy nhiên, như vậy không có nghĩa là phải khử Đệ quy bằng mọi giá và cũng không e ngại cũng như ác cảm với việc dùng Đệ quy
6.1 Khử Đệ quy bằng vòng lặp
Quy tắc: Chuyển tham số Đệ quy thành biến đếm của vòng lặp
Phương pháp vòng lặp thường được áp dụng với kiểu Đệ quy đuôi Nếu quan sát thấy hình ảnh “cái đuôi” trong Đệ quy đuôi, ta nhận thấy đó là là một dãy thao tác được lặp đi lặp lại với quy mô
“nhỏ” dần cho đến lúc đạt được trạng thái dừng Đặc tính này cũng tương tự ý niệm của vòng lặp: Lặp lại dãy thao tác cho đến lúc gặp điều kiện dừng Nếu ta có thể chuyển đổi từ quy mô bài toán sang điều kiện thi hành (hoặc điều kiện dừng) của vòng lặp thì ta có thể khử được lời gọi Đệ quy bằng vòng lặp
Ví dụ 1: Tính giai n!
Điều kiện dừng của thật toán Đệ quy là n=2, và đó cũng là điều kiện dừng của vòng lặp
Trang 10Cách 1: Đệ quy
var n:integer;
Function GT(n:integer): integer;
Begin
if n <=2 then GT:=n
else Gt:=Gt(n-1)*n
End;
Begin
read(n);
write(GT(n));
readln
End
Cách 2: Sử dụng vòng lặp
var n,i:integer;
Gt:longint;
Begin
read(n);
Gt:=1;
for i:=n downto 2 Do Gt:=Gt*i;
write(Gt);
readln
End
Ví dụ 2: Tìm vị trí xuất hiện của số x trong dãy a gồm n số được sắp tăng dần Ta giải bài toán này
bằng tìm kiếm nhị phân
Quy mô bài toán trong trường hợp này chính là chiều dài của dãy số tìm kiếm Nếu chiều dài này bằng 0 thì thuật toán kết thúc Trong thuật toán Đệ quy, điều kiện này được thể hiện bằng biểu thức (left > right) Ta sẽ dùng biểu thức này làm điều kiện dừng của vòng lặp
Program Timkiemnhiphan;
Type Dayso=Array[1 1000] of integer;
Var a:DaySo; n,x,i:integer;
Function Tim(a:dayso): integer;
Var TimThay: Boolean;
left, right, mid: integer;
Begin
Tim:=-1; TimThay:=False; //Ban đầu xem như không tim thấy
left:=1;
right:=n; // tìm trên toàn bộ mảng
While(left<=right) and (timthay=false) Do
Begin
Mid:=(right+left) div 2;
if(a[mid]=x) then Begin
Tim:=mid;
TimThay:=True; // Tìm thấy thì trả về kết quả và thoát ra khỏi vòng lặp End
Else
if x>a[mid] then left:=mid+1 //Thu hẹp dãy số chỉ còn nửa bên phải
else right:=mid-1; //Thu hẹp dãy số chỉ còn nửa bên trái
End;
write('Vi tri gia tri can tim trong day la: ',tim);