MỤC LỤC
1
Lời cảm ơn
2
LỜI NÓI ĐẦU
Trong quá trình nghiên cứu giải quyết các vấn đề - bài toán, người ta đã
đưa ra những nhận xét như sau:
− Có nhiều bài toán cho đến nay vẫn chưa tìm ra một cách giải theo kiểu
thuật toán và cũng không biết là có tồn tại thuật toán hay không;
− Có nhiều bài toán đã có thuật toán để giải nhưng không chấp nhận
được vì thời gian giải theo thuật toán đó quá lớn hoặc các điều kiện
cho thuật toán khó áp dụng;
− Có những bài toán được giải theo những cách giải vi phạm thuật toán
nhưng vẫn chấp nhận được.
Thuật toán đệ quy là một trong những sự mở rộng cơ bản nhất của khái
niệm thuật toán. Như đã biết, một thuật toán cần phải thỏa mãn các tính chất:
− Tính hữu hạn;
− Tính xác định;
− Tính đúng đắn.
Tuy nhiên, có những bài toán mà việc xây dựng một thuật toán với đầy đủ
ba tính chất trên là rất khó khăn. Trong khi đó, nếu ta xây dựng một thuật toán vi
phạm một vài tính chất trên thì cách giải lại trở nên đơn giản hơn nhiều và có thể
chấp nhận được. Một trong những trường hợp đó là thuật toán đệ quy.
Khi thực hiện đề tài này, mục đích chính của em là tìm hiểu các vấn đề
căn bản nhất, những lý thuyết nền tảng của thuật toán đệ quy cũng như thấy
rõ ưu điểm của sự đệ quy trong việc đơn giản hóa quá trình tính toán, giải một
số bài toán khó, bài toán điển hình có tính ứng dụng trong thực tiễn. Đồng
thời chỉ ra cài đặt đệ quy trong máy tính đôi lúc lại không đơn giản như phát
biểu đệ quy, từ đó giới thiệu phương pháp khử đệ quy và mục đích, ý nghĩa
của phương pháp khử đệ quy.
Bố cục của luận văn được chia làm 3 chương:
Chương I: TỔNG QUAN VỀ ĐỆ QUY VÀ GIẢI THUẬT ĐỆ QUY
Chương II: MỘT SỐ BÀI TOÁN GIẢI BẰNG GIẢI THUẬT ĐỆ
3
QUY ĐIỂN HÌNH
Chương III: KHỬ ĐỆ QUY
KẾT LUẬN VÀ HƯỚNG PHÁT TRIỂN CỦA ĐỀ TÀI
Với thời gian làm đề tài không dài, vốn kiến thức tiếp thu còn hạn chế
và chưa có kinh nghiệm trong lập trình nên bài khóa luận của em không tránh
khỏi những thiếu sót, em rất mong nhận được sự đóng góp cũng như phê bình
của thầy cô và các bạn để bài khóa luận được hoàn thiện hơn!
4
CHƯƠNG I
TỔNG QUAN VỀ ĐỆ QUY VÀ GIẢI THUẬT ĐỆ QUY
1.1. Khái niệm về đệ quy
Đệ quy là một khái niệm tồn tại trong thực tế sống, trong toán học và
nhất là trong các ngôn ngữ lập trình. Sau đây là một số hiện tượng có tính đệ
quy trong thực tế cuộc sống.
Bộ Matruska là một mặt hàng mỹ nghệ nổi tiếng thế giới của các nghệ
nhân người Nga mà đã từng xuất hiện ở Việt Nam. Một con to nhất có cái
bụng rỗng, thân nó được chia thành hai phần như cái hộp, khi mở nắp ra có
một con bé hơn nằm trong bụng của nó. Con lật đật thứ hai này cũng như con
lớn hơn, khi mở nắp ra cũng thấy con bé hơn nằm trong bụng nó, lại mở ra ta
lại được con bé hơn. Xếp tất cả các con lật đật này cạnh nhau ta thấy chúng có
hình dáng giống nhau nhưng kích thước và khi lồng chúng vào nhau thì ta
thấy dường như mỗi con chứa trong nó hình ảnh của chính bản thân nó.
Hiện tượng thứ hai quen thuộc hơn, khi chúng ta theo dõi các chương
trình “cầu truyền hình” của đài truyền hình Việt Nam, một hiện tượng khá lý
thú mà ta có thể nhìn thấy khi ngồi xem truyền hình. Trên màn ảnh tivi ta
nhìn thấy hướng dẫn viên truyền hình đang nói, bên cạnh anh ta là một chiếc
tivi dùng để phát ngay lại cảnh của trường quay, trong màn ảnh của tivi ta lại
thấy hình ảnh của chính nó, và một chuỗi các hình ảnh tivi lồng vào nhau.
Cả hai hiện tượng mà ta thấy ở trên và nhiều hiện tượng khác tương tự
như vậy đều có tính chất chung, là một đối tượng bao hàm trong nó hình ảnh
của chính nó và ta gọi đó là hiện tượng đệ quy.
Một vấn đề đặt ra là: các hiện tượng đệ quy mà ta đã xét ở phần trên
đều mang tính chất tự nhiên trong cuộc sống đời thường. Vậy còn trong toán
học, thường các đối tượng đều được thông qua các đối tượng khác, liệu có
hiện tượng một đối tượng được tính toán dựa trên dữ kiện của chính nó
không? Có thể trả lời câu hỏi này thông qua một số ví dụ sau:
Ví dụ 1: Tính giai thừa của số tự nhiên n (ký hiệu n!)
5
n! được tính như sau:
− Nếu n = 0 thì n! = 1;
− Nếu n > 0 thì n! = n(n-1)!
Ví dụ 2: Tính giá trị a n với a là số thực khác không và n là số nguyên
không âm.
( n = 0)
1
an =
a. a
n −1
(n > 0)
Ví dụ 3: Tìm phần tử thứ n của 1 cấp số cộng có số hạng đầu là a,
công sai là r.
a
u n = r + u
n −1
( n = 1)
(n > 1)
Ví dụ 4: Tìm phần tử thứ n của 1 cấp số nhân có số hạng đầu là a,
công bội là q.
a
un =
q * u n−1
( n = 1)
( n > 1)
Trong các ví dụ trên cho thấy, khái niệm toán học sẽ cho ta một phương
pháp ngắn gọn mô tả một số đối tượng.
Khái niệm:
Ta nói một đối tượng là đệ quy (recursive algorithm) nếu nó được
định nghĩa qua chính nó hoặc một đối tượng khác cùng dạng với chính nó
bằng quy nạp.
1.2. Giải thuật đệ quy
1.2.1. Khái niệm giải thuật đệ quy
Nếu lời giải của bài toán P được thực hiện bằng lời giải của bài toán P’
có dạng giống như P thì đó là một lời giải đệ quy. Giải thuật tương ứng với lời
giải như vậy gọi là giải thuật đệ quy. Mới nghe có vẻ hơi lạ nhưng điểm mấu
chốt cần lưu ý là: P’ tuy có dạng giống như P, nhưng theo một định nghĩa nào
đó P’ phải “nhỏ” hơn P, dễ giải hơn P và việc giải nó không cần dùng đến P.
Hãy xét bài toán tìm một từ trong một quyển từ điển. Có thể nêu giải
thuật như sau:
6
If (Từ điển là một trang) then (Tìm từ trong trang này)
Else
Begin
Mở từ điển vào trang “giữa”;
Xác định xem nửa nào của từ điển chứa từ cần tìm;
If (Từ đó nằm ở nửa trước của từ điển) then (Tìm từ đó
trong nửa trước)
Else (Tìm từ đó trong nửa sau);
End;
Tất nhiên giải thuật trên mới chỉ được nêu dưới dạng “thô” và còn
nhiều chỗ chưa cụ thể, chẳng hạn:
− Tìm từ trong một trang thì làm thế nào?
− Thế nào là mở từ điển vào trang giữa?
− Làm thế nào để biết từ đó nằm ở nửa nào của từ điển?...
Để trả lời rõ những câu hỏi trên không phải là khó, nhưng ta sẽ không
sa vào các chi tiết này mà muốn tập trung vào việc xét “chiến thuật” của lời
giải. Có thể hình dung chiến thuật tìm kiếm này một cách khái quát như sau:
Tìm từ trong từ điển
Tìm từ trong từ điển
nửa sau
Tìm từ trong từ điển
nửa trước
Hình 1.1. Mô tả đệ quy tìm một từ trong từ điển
Ta thấy có hai điểm chính cần lưu ý:
Sau mỗi lần từ điển được tách đôi thì một nửa thích hợp sẽ lại được tìm
kiếm bằng một “chiến thuật” như đã dùng trước đó.
Có một trường hợp đặc biệt, khác với mọi trường hợp trước, sẽ đạt được
sau nhiều lần tách đôi, đó là trường hợp từ điển chỉ còn duy nhất một trang.
Lúc đó việc tách đôi ngừng lại và bài toán trở thành đủ nhỏ để ta có thể giải
7
quyết trực tiếp bằng cách tìm từ mong muốn trên trang đó chẳng hạn bằng cách
tìm tuần tự. Trường hợp đặc biệt này được gọi là trường hợp suy biến
(degenerate case).
Có thể coi đây là một “chiến thuật” kiểu “chia để trị” (devide and
conquer). Bài toán được tách thành bài toán nhỏ hơn và bài toán nhỏ hơn lại
được giải quyết với thuật chia để trị như trước, cho tới khi xuất hiện trường
hợp suy biến.
Ta thể hiện giải thuật tìm kiếm này dưới dạng một thủ tục:
Procedure TIMKIEM (TD,Tu)
{TD được coi là đầu mối để truy nhập vào từ điển đang xét, Tu
chỉ từ cần tìm}
1. If (Từ điển là một trang) then (Tìm từ trong trang này)
2. Else
Begin
Mở từ điển vào trang “giữa”;
Xác định xem nửa nào của từ điển chứa từ Tu;
If (Từ đó nằm ở nửa trước của từ điển) then
call TIMKIEM (TD1,Tu)
Else call TIMKIEM (TD2,Tu)
End;
{TD1 và TD2 là đầu mối để truy nhập vào nửa trước và nửa sau
của từ điển}
3. Return
Thủ tục như trên gọi là thủ tục đệ quy. Từ đó, có thể nêu ra các đặc
điểm sau:
− Trong thủ tục đệ quy có lời gọi đến chính thủ tục đó. Ở đây, trong
thủ tục TIMKIEM có call TIMKIEM;
− Mỗi lần có lời gọi lại thủ tục thì kích thước của bài toán đã thu nhỏ
hơn trước. Ở đây, khi có call TIMKIEM thì kích thước chỉ còn bằng
một “nửa” trước đó;
8
− Có một trường hợp đặc biệt: Trường hợp suy biến. Đó chính là
trường hợp mà từ điển chỉ còn là một trang. Khi trường hợp này xảy
ra thì bài toán còn lại sẽ được giải quyết theo một cách khác hẳn và
gọi đệ quy cũng kết thúc. Chính tình trạng kích thước của bài toán
cứ giảm dần sẽ đảm bảo dẫn tới trường hợp suy biến.
1.2.2. Bố cục một chương trình con đệ quy
Phương pháp đệ quy mạnh ở chỗ nó cho phép mô tả một tập lớn các đối
tượng chỉ bởi một số ít các mệnh đề hoặc mô tả một giải thuật phức tạp bằng
một số ít các thao tác (một chương trình con đệ quy).
Một chương trình con đệ quy căn bản gồm hai phần:
− Phần neo (anchor): Tương ứng với các trường hợp suy biến, thông
thường trong trường hợp suy biến ta đã biết lời giải của bài toán.
Một chương trình đệ quy nhất thiết phải có phần suy biến này, nếu
không có chương trình của chúng ta có thể bị rơi vào tình trạng gọi
đệ quy vô hạn lần và chương trình không thể tiếp tục được.
− Phần đệ quy: Tương ứng với trường hợp tổng quát, qui bài toán về
bài toán tương tự bằng cách gọi tới chương trình con đó với các bộ
dữ liệu “đơn giản hơn”, thông thường bài toán con này có kích
thước nhỏ hơn bài toán ban đầu. Phần đệ quy thể hiện tính “quy
nạp” của lời giải.
Ví dụ: Tính giai thừa của số tự nhiên n:
1
n * GT (n − 1)
GT(n) =
(n = 0)
(n > 0)
(1)
(2)
Khi đó: (1) là phần neo; (2) là phần đệ quy.
1.2.3. Phương pháp tìm giải thuật đệ quy cho một bài toán
Để xây dựng giải thuật một bài toán có tính đệ quy bằng phương pháp
đệ quy ta cần thực hiện tuần tự 3 nội dung sau:
− Thông số hóa bài toán;
− Tìm các trường hợp neo cùng giải thuật giải tương ứng;
9
− Tìm giải thuật giải trong trường hợp tổng quát bằng phân rã bài toán
theo kiểu đệ quy.
1.2.3.1. Thông số hóa bài toán
Tổng quát hóa bài toán cụ thể cần giải thành bài toán tổng quát (một họ
các bài toán chứa bài toán cần giải), tìm ra các thông số cho bài toán tổng quát
đặc biệt là nhóm các thông số biểu thị kích thước của bài toán - các thông số
điều khiển (các thông số mà độ lớn của chúng đặc trưng cho độ phức tạp của
bài toán và giảm đi qua mỗi lần gọi đệ quy).
Ví dụ: n trong hàm GiaiThua(n);
m, n trong hàm USCLN(n,m).
1.2.3.2. Phát hiện các trường hợp neo (suy biến) và tìm giải thuật cho các
trường hợp này
Đây là các trường hợp suy biến của bài toán tổng quát, là các trường
hợp tương ứng với các giá trị biên của các biến điều khiển (trường hợp kích
thước bài toán nhỏ nhất) mà giải thuật không đệ quy (thường rất đơn giản).
Ví dụ: GiaiThua(0) = 1;
USCLN(m,n) = m khi m = n.
1.2.3.3. Tìm thuật giải trong trường hợp tổng quát
Tìm phương án (giải thuật) giải bài toán tổng quát bằng cách phân chia
nó thành các thành phần mà hoặc có giải thuật không đệ quy hoặc là bài toán
trên nhưng có kích thước nhỏ hơn.
Ví dụ: GiaiThua = n*GiaiThua(n-1)
1.3. Phân loại đệ quy
1.3.1. Phân loại theo mô tả đệ quy
Trong nhiều tình huống việc mô tả các bài toán, các giải thuật, các sự
kiện, các sự vật, các quá trình, các cấu trúc... sẽ đơn giản và hiệu quả hơn nếu
ta nhìn được nó dưới góc độ mang tính đệ quy.
Mô tả mang tính đệ quy về một đối tượng là mô tả theo cách phân tích
đối tượng thành nhiều thành phần mà trong số các thành phần có thành phần
10
mang tính chất của chính đối tượng được mô tả. Tức là mô tả đối tượng qua
chính nó.
Người ta phân đệ quy thành 2 loại: Đệ quy trực tiếp và đệ quy gián tiếp.
− Đệ quy trực tiếp: Là loại đệ quy mà đối tượng được mô tả trực tiếp
qua nó: A mô tả qua A, B, C,...trong đó B, C,...không chứa A.
− Đệ quy gián tiếp: Là đệ quy mà đối tượng được mô tả gián tiếp qua
nó: A mô tả qua A 1 , A 2 , A 3 ...,A n . Trong đó có một A i được mô tả
qua A.
1.3.2. Phân loại theo cách gọi hàm
1.3.2.1. Đệ quy tuyến tính
Chương trình con đệ quy tuyến tính là chương trình con đệ quy trực
tiếp đơn giản nhất, trong thân hàm có duy nhất một lời gọi hàm gọi lại chính
nó một cách tường minh, có dạng:
P ≡ If (thỏa mãn điều kiện dừng) then thực hiện S
Else
Begin
Thực hiện S*;
Gọi P;
End;
Với S, S* là các thao tác không đệ quy.
Ví dụ 1: Hàm GiaiThua(n) tính số hạng n của n!
− Dạng ngôn ngữ toán học:
1
n * ( n − 1)!
n! =
(n = 0)
( n > 0)
− Dạng hàm trong ngôn ngữ tựa Pascal:
Function GiaiThua(n): Item;
Begin
If (n=0) then GiaiThua := 1
Else Giaithua := n*GiaiThua(n-1);
End;
11
{Item quy ước là một kiểu dữ liệu bất kỳ nào đó}
Ví dụ 2: Chương trình con tính USCLN của 2 số nguyên dương a và b
− Dạng ngôn ngữ toán học:
a
USCLN(a,b) = USCLN (a − b, b)
USCLN (b − a, a )
( a = b)
( a > b)
( a < b)
− Dạng hàm trong ngôn ngữ tựa Pascal:
Function USCLN(a,b): Item;
Begin
If (a=b) then USCLN := a
Else
If (a>b) then USCLN := USCLN(a-b,b)
Else USCLN := USCLN(a,b-a);
End;
1.3.2.2. Đệ quy nhị phân
Chương trình con đệ quy nhị phân là chương trình con đệ quy trực
tiếp, trong thân hàm có hai lời gọi hàm gọi lại chính nó một cách tường
minh, có dạng:
P ≡ If (thỏa mãn điều kiện dừng) then thực hiện S
Else
Begin
Thực hiện S*;Gọi P(x); Gọi P(y);
End;
Với S, S* là các thao tác không đệ quy.
Ví dụ: Hàm FIBO(n) tính số hạng n của dãy FIBONACCI
− Dạng ngôn ngữ toán học:
1
F (n − 2) + F (n − 1)
F(n) =
(n = 0; n = 1)
(n > 1)
Ở bài toán này, trường hợp suy biến ứng với hai giá trị FIBO(0) =
FIBO(1) = 1.
12
− Dạng hàm trong ngôn ngữ tựa Pascal:
Function FIBO(n): Item;
Begin
If (n<=1) then FIBO := 1
Else FIBO := FIBO(n-2) + FIBO(n-1);
End;
1.3.2.3. Đệ quy phi tuyến
Chương trình con đệ quy phi tuyến là chương trình con đệ quy trực tiếp
mà lời gọi đệ quy được thực hiện bên trong vòng lặp.
Dạng tổng quát của chương trình con đệ quy phi tuyến:
P ≡ If (thỏa điều kiện dừng) then thực hiện S
Else
for giá trị đầu to giá trị cuối do
Begin
Thực hiện S*;Gọi P;
End;
Với S, S* là các thao tác không đệ quy.
Ví dụ: Cho dãy {X n } xác định theo công thức truy hồi:
X 0 = 1; X n = n 2 X 0 + (n-1) 2 X 1 +...+ 2 2 X n−2 + 1 2 X n−1
− Dạng hàm trong ngôn ngữ tựa Pascal:
Function X(n): Item;
Begin
If (n=0) then X:=1
Else
Begin
tg=0;
for i:= 0 to n-1 do
tg := tg + sqr(n-i)*X(i);
X : = tg;
{tg là giá trị của hàm X(n)}
13
End;
End;
1.3.2.4. Đệ quy hỗ tương
Trong đệ qui tương hỗ thì thường có 2 hàm, và trong thân của hàm này
có lời gọi của hàm kia, điều kiện dừng và giá trị trả về của cả hai hàm có thể
giống nhau hoặc khác nhau.
Dạng tổng quát của chương trình con đệ quy hỗ tương:
Function TenHamY(thamso): Item; forward;
Function TenHamX(thamso): Item;
Begin
If (Điều kiện dừng) then
Begin
...;
Return gia_tri_tra_ve;
End
Else
Begin
...;
Return TenHamX(thamso) <lien ket hai ham>
TenHamY(thamso);
End;
End;
Function TenHamY(thamso): Item;
{hoặc Function TenHamY;}
Begin
If (Điều kiện dừng) then
Begin
...;
return gia_tri_tra_ve;
End;
14
Else
Begin
...;
Return TenHamY(thamso) <lien ket hai ham>
TenHamX(thamso);
End;
End;
Ví dụ: Hàm TimX(n) và TimY(n) tìm số hạng thứ n của dãy:
X(0)=1; Y(0)=1;
X(n) = X(n-1) + Y(n-1);
Y(n) = X(n-1) * Y(n-1).
− Dạng hàm trong ngôn ngữ mã giả:
TimX ≡ Nếu n = 0 thì TimX = 1
Còn không TimX = TimX(n-1) + TimY(n-1);
TimY ≡ Nếu n = 0 thì TimY = 1
Còn không TimY = TimX(n-1) * TimY(n-1);
− Dạng hàm trong ngôn ngữ tựa Pascal:
Function TimY(n): Item; forward;
Function TimX(n): Item;
Begin
If (n=0) then TimX:=1
Else TimX:=TimX(n-1)+TimY(n-1);
End;
Function TimY(n): Item; {hoặc Function TimY;}
Begin
If (n=0) then TimY:=1
Else TimY:=TimX(n-1)*TimY(n-1);
End;
15
1.4. Độ phức tạp của các chương trình đệ quy
Với các chương trình con đệ quy, ta không thể áp dụng các quy tắc
thông thường như quy tắc nhân, quy tắc cộng... để tính độ phức tạp của
chúng, bởi vì một chương trình đệ quy sẽ gọi chính bản thân nó.
Với chương trình đệ quy, trước hết ta cần thành lập các phương trình đệ
quy, sau đó giải các chương trình đệ quy. Nghiệm của phương trình đệ quy là
thời gian thực hiện chương trình đệ quy đó.
1.4.1. Thành lập phương trình đệ quy
Phương trình đệ quy là một phương trình biểu diễn mối liên hệ giữa
T(n) và T(k), trong đó: T(n) là thời gian thực hiện với kích thước dữ liệu nhập
là n, T(k) là thời gian thực hiện với kích thước dữ liệu nhập là k (k < n).
Để thành lập phương trình đệ quy ta căn cứ vào chương trình đệ quy.
Ví dụ 1: Hàm tính giai thừa của số tự nhiên n viết bằng giải thuật đệ
quy có dạng:
Function GiaiThua(n:integer): integer;
Begin
If (n=0) then GiaiThua := 1
Else Giaithua := n*GiaiThua(n-1);
End;
Gọi: T(n) là thời gian thực hiện tính n!
T(n-1) là thời gian thực hiện tính (n-1)!
Trường hợp n = 0, chương trình chỉ thực hiện một lệnh gán GiaiThua := 1,
nên tốn O(1), do đó ta có T(0) = C 1 .
Trường hợp n > 0, chương trình gọi đệ quy GiaiThua(n-1), việc gọi đệ
quy này tốn thời gian là T(n-1), sau khi có kết quả của việc gọi đệ quy,
chương trình phải nhân kết quả đó với n và gán cho GiaiThua. Thời gian để
thực hiện phép nhân và phép gán là một hằng C 2 .
Vậy ta có phương trình đệ quy để tính thời gian thực hiện của chương
trình đệ quy GiaiThua là:
16
C1
T (n − 1) + C2
T(n) =
(n = 0)
(n > 0)
Ví dụ 2: Xét thủ tục Mergesort một cách phác thảo như sau:
Function Mergesort (L:List; n:integer):List;
Var L1, L2:List;
Begin
If n=1 then return(L);
Else
Begin
Chia L thành 2 nửa L1 và L2, mỗi nửa có độ dài n/2;
Return (Merge(Mergesort(L1,n/2),Mergesort(L2,n/2)));
End;
End;
Hàm Mergesort nhận một danh sách có độ dài n và trả về một danh sách
đã được sắp xếp. Thủ tục Merge nhận 2 danh sách đã được sắp L1và L2 mỗi
danh sách có độ dài n/2, trộn chúng lại với nhau để được một danh sách gồm
n phần tử có thứ tự. Giải thuật chi tiết của Merge ta không bàn đến, chúng ta
chỉ để ý rằng thời gian để Merge các danh sách có độ dài n/2 là O(n).
Gọi: T(n) là thời gian thực hiện Mergesort một danh sách có n phần tử;
T(n/2) là thời gian thực hiện Mergesort một danh sách có n/2 phần tử.
Ta có phương trình đệ quy như sau:
C1
2T (n / 2) + C2 n
T(n) =
(n = 1)
( n > 1)
Trong đó:
− C 1 là thời gian phải tốn khi L có độ dài bằng 1;
− Trường hợp n > 1, thời gian Mergesort được chia làm 2 phần:
• Phần gọi đệ quy Mergesort 1 danh sách có độ dài n/2 là
T(n/2);
17
• Phần thứ 2 bao gồm phép thử n > 1, chia danh sách thành 2
nửa và Merge. Ba thao tác này có thời gian không đổi, thời
gian thực hiện là C 2 n.
1.4.2. Giải phương trình đệ quy
Để giải phương trình đệ quy ta có thể giải bằng những phương pháp
như: phương pháp truy hồi, phương pháp đoán nghiệm, phương pháp giải
phương trình đặc trưng, sử dụng định lý Master. Ta đi tập trung nghiên cứu
phương pháp truy hồi.
Từ phương trình đệ quy ta thay thế T(m) (m < n) vào phía phải của
phương trình cho đến khi tất cả T(m) (m > 1) được thay thế bởi biểu thức của
T(1). Vì T(1) luôn là hằng nên ta có công thức của T(n) chứa các số hạng chỉ
liên quan tới n và các hằng số.
Ví dụ: Giải phương trình:
(n = 1)
C1
2T (n / 2) + C2 n
T(n) =
( n > 1)
n
2
Ta có: T(n) = 2 T( ) + C 2 n
n
4
n
n
] + C 2 n = 4T( ) + 2C 2 n
2
4
n
8
n
n
] + 2C 2 n = 8T( ) + 3C 2 n
4
8
T(n) = 2[2T( ) + C 2
T(n) = 4[2T( ) + C 2
T(n) = 2 i T(
n
) + iC 2 n
2i
Giả sử n = 2 k quá trình suy rộng này sẽ kết thúc khi i = k ⇒ T(n) = 2 k
T(1) + kC 2 n
Vì 2 k = n ⇒ k = logn và với T(1) = C 1 ⇒ T(n) = C 1 n + C 2 nlogn
Vậy thời gian thực hiện thuật toán là O(nlogn).
1.5. Đệ quy và quy nạp toán học
Có một mối quan hệ giữa đệ quy và quy nạp toán học. Cách giải đệ quy
một bài toán dựa trên việc định rõ lời giải cho trường hợp suy biến rồi thiết kế
18
làm sao để lời giải của bài toán được suy từ lời giải của bài toán nhỏ hơn,
cùng loại như thế.
Tương tự như vậy, quy nạp toán học chứng minh một tính chất nào đó
ứng với số tự nhiên cũng bằng cách chứng minh tính chất ấy đúng đối với một
số trường hợp cơ sở (chẳng hạn với n = 1) và rồi chứng minh tính chất ấy
đúng với n bất kỳ, nếu nó đã đúng với các số tự nhiên nhỏ hơn n.
Do đó, ta không lấy làm lạ khi thấy quy nạp toán học được dùng để
chứng minh các tính chất có liên quan đến giải thuật đệ quy. Chẳng hạn, sau
đây ta chứng minh tính đúng đắn của giải thuật đệ quy tính n! và giải thuật
tìm ước chung lớn nhất của 2 số nguyên dương.
1.5.1. Tính đúng đắn của giải thuật đệ quy tính n!
GiaiThua(n) ≡ If (n=0) then GiaiThua := 1
Else Giaithua := n*GiaiThua(n-1);
Chứng minh tính đúng đắn của thuật toán trên là chứng minh với mọi
tham số đầu vào n (nguyên không âm) thì thuật toán đều dừng và cho kết quả
là n!. Ta chứng minh bằng quy nạp như sau:
Trường hợp cơ sở với n = 0 thuật toán thực hiện 1 lần và cho kết quả là 0! = 1
Giả sử thuật toán đúng với n = k (k>0), tức là với n = k thì GiaiThua(k)
dừng và cho kết quả là k!
Xét n = k + 1, ta có thuật toán tính n! lúc này là tính (k+1)! sẽ được
thực hiện như sau:
GiaiThua(k+1) := (k+1) * GiaiThua(k)
Theo giả thiết quy nạp thì GiaiThua(k) dừng và cho kết quả là k! và do
vậy GiaiThua(k+1) cũng dừng và cho kết quả là (k+1)!.
1.5.2. Tính đúng đắn của giải thuật đệ quy tính USCLN của hai số nguyên dương
Thuật toán tìm ước số chung lớn nhất của 2 số nguyên dương như sau:
− Nếu a = b thì USCLN(a,b) = a
− Nếu a ≠ b thì:
USCLN(a,b) = USCLN(a - b,b) nếu a > b
USCLN(a,b) = USCLN(a,b - a) nếu a < b
19
Ta phải chứng minh với mọi cặp số nguyên dương (a,b) thì thuật toán
đều kết thúc và cho kết quả d = (a,b) là ước chung lớn nhất của a và b.
Thật vậy, với a = b thì thuật toán dừng và cho kết quả đúng.
Ta phải chứng minh với a ≠ b thì sau một số hữu hạn lần gọi đệ quy nó
sẽ trở về trường hợp suy biến. Thật vậy:
Nếu a > b, gọi d là ước chung lớn nhất của a và b, xét
giả thiết a chia hết cho d, b chia hết cho d và a > b nên
a −b a b
= − do
d
d d
a b
− ∈Z + hay (a - b)
d d
cũng chia hết cho d hay nói cách khác d cũng là ước chung của a - b và b.
Nếu a < b, gọi d là ước chung lớn nhất của a và b, xét
giả thiết a chia hết cho d, b chia hết cho d và a < b nên
b−a b a
= − do
d
d d
b a
− ∈Z + hay (b - a)
d d
cũng chia hết cho d hay nói cách khác d cũng là ước chung của a và b - a.
Theo thuật toán đệ quy trên do quá trình đệ quy mới được tiếp tục bằng
cách lấy số lớn trừ số bé, nên dãy thu được đơn điệu giảm và bị chặn dưới bởi
0 nên quá trình đó ắt phải dừng và đến lúc nào đó chúng phải bằng nhau
(trường hợp suy biến).
20
Chương II
MỘT SỐ BÀI TOÁN GIẢI BẰNG GIẢI THUẬT ĐỆ QUY ĐIỂN HÌNH
2.1. Bài toán “Tháp Hà Nội” (Tower of Hanoi)
Phát biểu bài toán: Đây là một bài toán mang tính chất một trò chơi,
nội dung như sau:
Có n đĩa, kích thước nhỏ dần, đĩa có lỗ ở giữa (như đĩa hát). Có thể xếp
chồng chúng lên nhau xuyên qua một cọc, to dưới nhỏ trên để cuối cùng có
một chồng đĩa dạng như hình tháp (hình 2.1).
A
B
C
Hình 2.1. Mô hình bài toán “Tháp Hà Nội”
Yêu cầu đặt ra là:
Chuyển chồng đĩa từ cọc A sang cọc khác, chẳng hạn sang cọc C, theo
những điều kiện:
1. Mỗi lần chỉ được chuyển một đĩa;
2. Không khi nào có tình huống đĩa to ở trên đĩa nhỏ (dù là tạm thời);
3. Được phép sử dụng một cọc trung gian, chẳng hạn cọc B để đặt tạm
đĩa (gọi là cọc trung gian) khi chuyển từ cọc A sang cọc C.
Phân tích bài toán:
21
Để đi tới cách giải tổng quát, trước hết xét vài trường hợp đơn giản.
− Trường hợp một đĩa:
• Chuyển đĩa từ cọc A sang cọc C.
− Trường hợp hai đĩa:
• Chuyển đĩa thứ nhất từ cọc A sang cọc B;
• Chuyển đĩa thứ hai từ cọc A sang cọc C;
• Chuyển đĩa thứ nhất từ cọc B sang cọc C.
Ta thấy trường hợp n đĩa (n>2) nếu coi (n-1) đĩa ở trên, đóng vai trò
như đĩa thứ nhất thì có thể xử lý giống như trường hợp 2 đĩa được, nghĩa là:
• Chuyển (n-1) đĩa trên từ cọc A sang cọc B;
• Chuyển đĩa thứ n từ cọc A sang cọc C;
• Chuyển (n-1) đĩa từ cọc B sang cọc C.
Có thể hình dung việc thực hiện 3 bước này theo mô hình (hình
2.2) như sau:
A
B
22
C
Bước 1
A
B
C
B
C
Bước 2
A
23
Bước 3
A
B
C
Hình 2.2. Mô hình chuyển đĩa bài toán “Tháp Hà Nội”
Như vậy, bài toán “Tháp Hà Nội” tổng quát với n đĩa được dẫn đến
bài toán tương tự với kích thước nhỏ hơn, chẳng hạn từ chỗ chuyển n đĩa
từ cọc A sang cọc C nay là chuyển (n-1) đĩa từ cọc A sang cọc B. Và ở
mức này thì giải thuật lại là:
• Chuyển (n-2) đĩa trên từ cọc A sang cọc C;
• Chuyển 1 đĩa từ cọc A sang cọc B;
• Chuyển (n-2) đĩa từ cọc C sang cọc B.
Và cứ như thế cho đến khi trường hợp suy biến xảy ra, đó là trường hợp
ứng với bài toán chuyển 1 đĩa thôi.
Vậy thì các đặc điểm của đệ quy trong giải thuật đã được xác định và ta
có thể viết giải thuật đệ quy của bài toán “Tháp Hà Nội” như sau:
Procedure HANOI (n,A,B,C)
{Chuyển n đĩa từ cọc A sang cọc C thông qua cọc B}
1. If n=1 then chuyển đĩa từ A sang C
2. Else
24
Begin
Call HANOI (n-1,A,C,B);
{chuyển n-1 đĩa từ A-B}
Call HANOI (1,A,B,C);
{chuyển 1 đĩa từ A-C}
Call HANOI(n-1,B,A,C);
{chuyển n-1 đĩa từ B-C}
End;
Return;
Đánh giá độ phức tạp của thuật toán:
Gọi Sn là số lần chuyển đĩa để chơi xong trò chơi với n đĩa.
Nếu n = 1 thì rõ ràng là S1 = 1.
Nếu n > 1 thì trước hết ta chuyển n-1 đĩa trên sang cọc B (giữ yên đĩa
thứ n ở dưới cùng của cọc A). Số lần chuyển n-1 đĩa là S n−1 . Sau đó ta chuyển
đĩa thứ n từ cọc A sang cọc C. Cuối cùng, ta chuyển n-1 đĩa từ cọc B sang cọc
C (số lần chuyển là S n−1 ).
Phương trình truy hồi tương ứng là:
1
Sn =
2 S n−1 + 1
(n = 1)
( n > 1)
Ta thực hiện phép truy toán cho phương trình trên ta có:
S n = 2S n−1 + 1 = 2(2S n−2 +1) + 1 = 2 2 S n−2 + 2 1 + 2 0
= ...
n −1
=
∑2
i =0
i
= 2n - 1
Giả sử n = 64, đòi hỏi 2 64 - 1 lần chuyển đĩa (xấp xỉ 18,4 tỉ tỉ lần). Nếu
mỗi lần chuyển đĩa mất 1 giây thì thời gian thực hiện thuật toán xấp xỉ 585 tỉ
năm!. Ta nói độ phức tạp của bài toán là 2 n - 1.
Bài toán trên cho thấy rằng: một thuật toán phải kết thúc sau một số
hữu hạn bước, nhưng nếu số hữu hạn này quá lớn thì thuật toán không thể
thực hiện được trong thực tế.
2.2. Mở rộng bài toán “Tháp Hà Nội” - bài toán “Tháp Hà Nội vòng”
25