Tải bản đầy đủ (.docx) (33 trang)

Tiểu luận giải thuật đệ quy

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (283.3 KB, 33 trang )

A. MỞ ĐẦU
I. Lý do chọn đề tài
Cấu trúc dữ liệu và giải thuật là học phần cung cấp kiến thức cơ sở cho chuyên
ngành công nghệ thông tin; bao hàm các mơ hình dữ liệu, cấu trúc dữ liệu như danh
sách, cây, ngăn xếp và hàng đợi,… và các thuật tốn như sắp xếp, tìm kiếm, đệ quy,….
Mỗi thuật tốn đều nhằm mục đích giải quyết một lớp bài tốn nào đó, nó có một độ
khó riêng, địi hỏi khả năng hiểu rõ thuật tốn thật chính xác và có sự liên tưởng thật
phong phú để làm sao giúp nguời học hiểu thật rõ về thuật tốn đó. Trong nội dung
nghiên cứu học phần này tôi nghiên cứu về giải thuật đệ quy, vì vậy để học và tìm hiểu
thật chắc về giải thuật đệ quy thì phải hiểu được cách thiết kế - cài đặt nó và cách nó
thực thi như thế nào. Đã có rất nhiều ý kiến cho rằng tìm hiểu đệ quy là việc khá khó
và áp dụng nó cũng hạn chế vì nó thường hay gây tràn bộ nhớ. Nhưng ngược lại nó
được áp dụng để giải quyết một số bài toán phức tạp, mà chỉ có dùng đệ quy mới làm
được.
Thuật tốn đệ quy là thuật tốn tự gọi lại chính nó với kích thước bài tốn nhỏ
hơn, rất có lợi trong việc người lập trình xây dựng các bài tốn mà các hàm có sẵn
trong các ngơn ngữ lập trình khơng thể giải quyết tối ưu, hiệu quả. Chính điều đó mà
thiết kế thuật toán này đang được chú trọng nhiều hơn vào các bài tốn mà các hàm lặp
khơng thực hiện được. Nhờ việc thiết kế thuật toán đệ quy mà việc xây dựng cấu trúc
dữ liệu bài toán đảm bảo được các yêu cầu, quan trọng hơn nữa giúp cho việc học một
ngơn ngữ lập trình nào đó một cách dễ dàng hơn. Chính vì vậy tơi quyết định nghiên
cứu giải thuật đệ quy, cụ thể là thiết kế thuật toán đệ quy để giải một số bài toán trong
đề tài nghiên cứu của mình.
II. Mục tiêu của đề tài
-

Nâng cao kiến thức về học phần cấu trúc dữ liệu và giải thuật, cụ thể nghiên cứu các
vấn đề về đệ quy và giải thuật đệ quy: khái niệm, các đặc điểm, các thiết kế, ứng dụng
để giải các bài tốn từ đơn giản đến phức tạp,… . Qua đó, hệ thống được kiến thức học
phần và biết cách vận dụng nó.


1


III. Đối tượng và phạm vi nghiên cứu
1. Đối tượng nghiên cứu
- Nghiên cứu giải thuật đệ quy và các vấn đề liên quan.
2. Phạm vi nghiên cứu
-

Đề tài chỉ nghiên cứu giải thuật đệ quy trong lĩnh vực lập trình, cụ thể sử dụng ngơn
ngữ tựa C để mơ tả thuật toán và cài đặt, thực thi (sử dụng ngơn ngữ lập trình C) các ý
tưởng giải thuật đệ quy trong từng bài toán cụ thể.
IV. Phương pháp nghiên cứu

-

Phương pháp nghiên cứu lý luận: đọc, thu thập thông tin từ giáo trình, sách, tài liệu
chuyên ngành, tài liệu internet….
- Phương pháp chứng minh, nêu ví dụ.
- Phương pháp phân tích, đánh giá, so sánh.

2


B. NỘI DUNG
Chương 1. Giới thiệu
1.1.

Khái niệm về giải thuật


Trong khn khổ của Tin học, việc giải bài tốn có nghĩa trao được cho máy
tính cách giải. Để giao bài tốn cho máy tính, ta cần hướng dẫn cho máy tính các thao
tác mà về ngun tắc máy có thể thực hiện được. Một cách giải bài toán như vậy được
gọi là một giải thuật (cịn gọi là thuật tốn hay thuật giải).
Nói một cách đầy đủ hơn: Giải thuật A để giải bài toán P là một dãy hữu hạn
các thao tác đơn giản được sắp xếp theo một cách trình tự xác định sao cho sau khi
thực hiện dãy thao tác đó, từ Input của bài tốn, ta nhận được Output cần tìm.
Trong định nghĩa nêu trên, thao tác đơn giản được hiểu là thao tác mà máy tính
có thể thực hiện được. Ví dụ các phép tốn số học, các phép so sánh hai giá trị số là
các thao tác đơn giản.
Theo đó, việc giải bài tốn theo quan niệm của Tin học là quá trình thực hiện
một dãy hữu hạn các thao tác đơn giản để có thể từ Input dẫn ra Output một cách
tường minh. Chính quan niệm về việc giải bài tốn như vậy là cốt lõi để ta có thể
chuyển giao cơng việc đó cho máy tính.
1.2.

Giải thuật đệ quy

Đệ quy là một khái niệm cơ bản trong toán học và khoa học máy tính. Một đối
tượng là đệ quy nếu nó hoặc một phần của nó được định nghĩa thơng qua khái niệm
chính nó.
Ví dụ:





Người là con của 2 người khác.
Số n được gọi là số tự nhiên khi số (n-1) là số tự nhiên.
Giai thừa (n) được tính bằng n*giai thừa (n-1).



Như vậy, nếu lời giải bài toán T được thực hiện bằng lời giải của một bài tốn
T’, T’có dạng giống như T, thì đó là lời giải đệ quy hay là giải thuật đệ quy.

3


Thoạt nghe thì có vẻ hơi vơ lí vì một chương trình khơng thể gọi mãi chính nó,
như vậy sẽ tạo ra một vịng lặp vơ hạn. Nhưng điểm mấu chốt cần lưu ý ở đây là: T’
tuy có dạng giống như T, nhưng theo một nghĩa nào đó, nó phải “nhỏ” hơn T.
{Hàm_đệ_quy} ({Danh_sách_tham_số})
{
{Gọi_lại_hàm_đệ_quy}({Danh_sách_tham_số});
}
Một cách dễ hiểu nhất, chúng ta có thể tưởng tượng giải thuật đệ quy giống như
một vòng lặp. Nếu như vòng lặp sẽ lặp đi lặp lại khối lệnh của nó với một số lần hữu
hạn hoặc vơ hạn, thì giải thuật đệ quy cũng sẽ lặp đi lặp lại đoạn mã được viết bên
trong nó một số lần hữu hạn hoặc vơ hạn, tùy vào cách viết của chúng ta.
Ví dụ xét bài tốn tìm một từ trong một quyển sách từ điển thì ta có thuật giải
như sau:
if (từ điển là một trang)
tìm từ trong trang này;
else
{
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)
tìm từ đó trong nửa trước;
else tìm từ đó trong nửa sau;

}
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 đi sâu
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" 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:

4


Ta thấy có ba điểm chính cần lưu ý:
1) Trong thủ tục đệ quy có lời gọi đến chính thủ tục đó.
2) 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 đó.
3) 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 tốn trở thành đủ nhỏ để ta có thể giải 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.
Có thể coi đây là một "chiến thuật " kiểu "chia để trị". 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.
Mỗi một lần hàm tự gọi đệ quy đến nó thì máy tính sẽ tự tạo ra một biến cục bộ
mới. Có bao nhiêu lần hàm gọi đệ quy thì sẽ có bấy nhiêu lần thốt ra khỏi hàm (kiểu
như lặp hàm).
Khi thốt ra ngồi hàm đệ quy thì một loạt các biến cục bộ tạo ra do dùng đệ
quy lúc này mới được giải phóng, và chúng sẽ giải phóng trước các biến cục bộ (sinh

ra do đệ quy) tạo ra sau.
Sử dụng đệ quy là một phương pháp làm cho chương trình ngắn gọn nhưng nó
sẽ làm tốn bộ nhớ và thời gian nếu như cấu trúc hàm đệ quy “phức tạp”.
♦Ưu nhược điểm khi sử dụng đệ quy:
• Ư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
bởi một số hữu hạn các mệnh đề.

5


+ Làm cho chương trình ngắn gọn, trong sáng, dễ hiểu nổi bật được bản
chất của vấn đề.
• Nhược điểm:
+ Làm tốn bộ nhớ nếu như hàm đệ quy quá phức tạp.
+ Nếu bài tốn khơng suy biến thì sử dụng đệ quy khơng hợp lý, làm cho
bài tốn phức tạp lên và dẫn đến hiện tượng tràn bộ nhớ.
♦Đệ quy nên dùng khi nào?
• Chỉ sử dụng hàm đệ quy khi bài tốn xảy ra trường hợp suy biến.
• Trong một trường hợp tổng qt bài tốn có thể đưa về cùng dạng. Nhưng
giá trị thay đổi, sau một loạt thay đổi nó phải đưa về trường hợp suy biến.
1.3.

Thiết kế giải thuật đệ qui

Khi bài toán đang xét hoặc dữ liệu đang xử lý được định nghĩa dưới dạng đệ
quy thì việc thiết kế các giải thuật đệ quy tỏ ra rất thuận lợi. Hầu như nó phản ánh rất
sát nội dung của định nghĩa đó. Các bạn có thể thấy điều này qua bài tốn tính giai
thừa như sau:
* Tính n!

Định nghĩa đệ quy của n! có thể nhắc lại:

Giải thuật đệ quy được viết dưới dạng thủ tục hàm như sau:
Factorial (int n)
{
if (n==0)
return 1;
else return n*Factorial(n-1);
}
Đối chiếu với 3 đặc điểm của thủ tục đệ quy nêu ở trên ta thấy:
-

Lời gọi tới chính nó nằm ở trong câu lệnh trả về đứng sau else.
Mỗi lần gọi đệ quy đến Factorial, thì giá trị của n giảm đi 1.

Ví dụ: gọi Factorial (3) = 3* Factorial (2)
=3*2* Factorial (1)
6


=3*2*1* Factorial (0)
=3*2*1*1
-

Factorial (0) chính là trường hợp suy biến, nó được tính theo cách đặc
biệt Factorial (0) =1.

Thơng qua ví dụ trên ta thấy, mỗi thuật tốn đệ quy luôn bao gồm 2 phần: phần
cơ sở (trường hợp suy biến) và phần tổng quát (gọi đệ qui). Phần cơ sở chính là 0!=1,
cịn phần tổng qt chính là n!=n*(n-1)!. Mỗi lần ta áp dụng trường hợp tổng quát, bài

toán sẽ được đơn giản hóa theo nghĩa kích cỡ sẽ giảm đi rồi tiến dần tới trường hợp cơ
sở, khi đó thuật tốn sẽ kết thúc.
Đối với bài tốn nêu trên, việc thiết kế giải thuật đệ quy tương ứng khá thuận
lợi vì cả 2 đều thuộc tính giá trị hàm mà định nghĩa đệ quy của hàm đó xác định được
dễ dàng.
Nhưng khơng phải lúc nào tính đệ quy trong cách giải bài toán cũng thể hiện rõ
nét và đơn giản như vậy. Thế thì vấn đề gì cần lưu tâm khi thiết kế một giải thuật đệ
quy ? Có thể thấy câu trả lời qua việc giải đáp các câu hỏi sau:
1) Có thể định nghĩa được bài toán dưới dạng một bài toán cùng loại, nhưng
“nhỏ” hơn như thế nào?
2) Như thế nào là kích thước của bài toán được giảm đi ở mỗi lần gọi đệ quy?
3) Trường hợp đặc biệt nào của bài toán sẽ được coi là trường hợp suy biến?
1.4.

Phân loại đệ quy

Đệ quy có nhiều loại, nhiều dạng khác nhau, nhưng hai dạng đệ quy thường găp
là đệ 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ó:
mơ tả qua A, B, C,… trong đó B, C,… khơng chứa A.
Ví dụ:
giaithua (int n)
{
if (n==0) return 1;
else
return giaithua (n-1)*n;
}


7

A


-

Đệ quy gián tiếp là loại đệ quy mà đối tượng được mơ tả gián tiếp qua nó:

A mơ tả

qua A1, A2,… An, trong đó Ai được mơ tả qua A.
Ví dụ:
F1(int x)
{
If (x<0)
reurn x;
else
return F2(x);
}
F2(y)
{
return F1(y-1);
}
1.5.

Đệ quy quay lui

Thuật toán đệ quy quay lui dùng để giải các bài tốn liệt kê cấu hình. Mỗi cấu
hình được xây dựng bằng cách xây dựng từng phần tử, mỗi phần tử được chọn bằng

cách thử tất cả các khả năng.
Giả thiết cấu hình cần liệt kê có dạng (x 1,x2,...,xn). Khi đó thuật tốn đệ quy
quay lui thực hiện theo các bước như sau:
 Xét tất cả các giá trị x1 có thể nhận, thử cho x1 nhận lần lượt các giá trị đó.
Với mỗi giá trị thử gán cho x1 ta có.
 Xét lần lượt các giá trị x2 có thể nhận, lại thử x2 nhận lần lượt các giá trị đó.
Với mỗi giá trị thử gán cho x 2 lại xét tiếp các khả năng chọn x 3... Cứ như vậy
làm tiếp..
 Xét tất cả các giá trị của x n có thể nhận, cho xn nhận lần lượt các giá trị đó,
thơng báo cấu hình tìm được(x1,x2,...,xn).
Trên phương diện quy nạp có thể nói rằng thuật tốn đệ quy quay lui liệt kê các
cấu hình n phần tử dạng (x1,x2,...,xn) bằng cách thử cho x1 nhận các giá trị có thể. Với
mỗi giá trị thử gán cho x1 lại liệt kê tiếp cấu hình n – 1 phần tử (x1,x2,...,xn).
Mơ hình thuật tốn đệ quy quay lui thường mô tả bằng thủ tục TRY:
Try (int i);
{
for (mọi giá trị V có thể cho xi)
{
<Thử cho xi nhận 1 giá trị thuộc V>;
if <xi là phần tử cuối cùng trong cấu hình>
8


<thơng báo cấu hình tìm được>;
else
{
<ghi nhận việc xi nhận giá trị thuộc V>;
Try(i + 1);
<Nếu cần, bỏ ghi nhận việc thử xi thuộc V, để thử giá trị khác>;
}

}
}
Đệ quy quay lui có một số bài tốn điển hình như liệt kê các dãy nhị phân độ
dài n, liệt kê cá tập con k phần tử hay bài toán phân tích số. Nhưng điển hình nhất đó
là bài tốn xếp các quân hậu trên một bàn cờ (ta thường xét vị trí của 8 quân hậu).

9


Chương 2. Ứng dụng giải thuật đệ qui để giải một sớ bài toán
2.1.

Bài toán tính giai thừa

a) Nhận xét bài toán:
Giai thừa là một bài toán kinh điển trong lập trình, nó là một bài tốn mà bất kì
một lâp trình viên nào đều cũng phải trải qua. Bài toán này giúp hiểu được thuật toán
đệ quy một cách tường minh nhất.
b) Phân tích bài tốn:
Giải quyết bài tốn này, chúng ta cần hiểu định nghĩa về n! (n là một số nguyên
dương): n giai thừa là tích của n số nguyên dương đầu tiên.
Công thức tổng quát: n! = n*(n-1)! Chính vì thế ta sử dụng lệnh truy hồi (gọi lại
hàm đệ quy) dựa trên công thức này
Trường hợp đặc biệt: 0! = 1, là trường hợp suy biến cho phần cơ sở.
c) Giải thuật:
int giaithua (int n)
{
if (n==0) return 1;
else
return giaithua (n-1)*n;

}
2.2.

Bài toán tháp Hà Nội

a) Nhận xét bài tốn:
Bài tốn này mang tính chất một trị chơi có nội dung:
Có n đĩa, kích thước nhỏ dần, đĩa có lỗ ở giữa (như đĩa hát). Có thể chồng
chúng lên nhau xuyên qua một cái cọc, to dưới nhỏ trên để cuối cùng có một chồng đĩa
dạng như hình một chiếc tháp.
b) Phân tích bài tốn:
Giả sử ta gọi các cọc là A (cọc nguồn), B (cọc trung gian), C (cọc đích).
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
quy tắc sau:
+ Mỗi lần chỉ được chuyển một đĩa.
+ Không được xảy ra trường hợp đĩa to ở trên đĩa nhỏ dù chỉ là tạm thời.
10


+ Được phép sử dụng cọc trung gian, chẳng hạn đấy là cọc B để tạm đặt đĩa
(gọi là đĩa trung gian) khi chuyển từ cọc A sang cọc C.
Để đ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 (n=1):
- Chuyển đĩa từ cọc A sang cọc C.
* Trường hợp hai đĩa (n=2):
- 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.
*…
* Trường hợp có n đĩa (n>2) và nếu coi (n-1) đĩa ở trên, đóng vai trị như đĩa

thứ nhất thì có thể xử lý tương tự như 2 trường hợp trên. Nghĩa là:
-

Chuyển

(n-1)

-

Chuyển

đĩa

đĩa

từ

thứ

n

cọc
cọc

A
A

sang

cọc


B.

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ể hiện 3 bước này theo mơ hình sau:

11


Như vậy, ta thấy toàn bộ n đĩa đã được chuyển từ cọc A sang cọc C và không vi
phạm bất cứ điều kiện nào của bài toán. Ở đây, ta thấy rằng bài toán chuyển n đĩa đã
được chuyển về bài toán đơn giản hơn là chuyển n-1 đĩa. Điểm dừng của thuật toán đệ
quy này là khi n=1 và ta chuyển thẳng đĩa này từ cọc nguồn A sang cọc đích C.
Tính chất chia để trị của thuật toán này thể hiện ở chỗ: Bài toán chuyển n đĩa
được chia làm 2 bài toán nhỏ hơn là chuyển n-1 đĩa. Lần thứ nhất chuyển n-1 đĩa từ
cọc nguồn A sang cọc trung gian B và lần thứ 2 chuyển n-1 đĩa từ cọc trung gian B
sang cọc đích C.
c) Giải thuật:
Ta sử dụng thủ tục đệ quy: Với những gì bài tốn u cầu và hướng giải quyết
như trên thì việc dùng giải thuật đệ quy là hợp lý nhất. Ta có giải thuật đệ quy sau:
void Function_ThapHaNoi(n, A, B, C):
{
12



if (n=1)
Move(n,A,C); //thực hiện di chuyển đĩa thứ n từ đĩa A sang đĩa C.
else
{
Function_ThapHaNoi(n-1, A, C, B); // chuyển n-1 đĩa từ cọc A sang cọc B,
sử dụng cọc C làm cọc trung gian.
Move(n,A,C);
Function_ThapHaNoi(n-1, B, A, C); // chuyển n-1 đĩa từ cọc B sang cọc C,
sử dụng cọc A làm cọc trung gian.
}
return;
}
2.3.

Bài toán dãy số Fibonacci

a) Nhận xét bài toán:
Dãy số Fibonacci bắt nguồn từ bài toán cổ về việc sinh sản của các cặp thỏ. Bài
toán được đặt ra như sau:
1) Các con thỏ không bao giờ chết.
2) Hai tháng sau khi ra đời một cặp thỏ mới sẽ sinh ra một cặp thỏ con (một
đực và một cái).
3) Khi đã sinh con rồi thì cứ mỗi tháng tiếp theo chúng lại sinh được một cặp
con mới.
Giả sử bắt đầu từ một cặp mới ra đời thì đến tháng thứ n sẽ có bao nhiêu cặp?
b) Phân tích bài tốn:
Bài tốn trên được giải quyết như sau:
Ví dụ: n=6, ta thấy:
Tháng thứ 1: 1 cặp (cặp ban đầu)

Tháng thứ 2: 1 cặp (cặp ban đầu vẫn chưa đẻ)
Tháng thứ 3: 2 cặp (đã có thêm 1 cặp con)
Tháng thứ 4: 3 cặp (cặp đầu vẫn đẻ thêm)
Tháng thứ 5: 5 cặp (cặp con bắt đầu đẻ)

13


Tháng thứ 6: 8 cặp (cặp con vẫn đẻ tiếp)
Bây giờ ta xét tới việc tính số cặp thỏ ở tháng thứ n: F(n)
- Nếu mỗi cặp thỏ ở tháng thứ (n-1) đều sinh con thì:
F(n) = 2(n-1).
Nhưng khơng phải như vậy. Trong các cặp thỏ ở tháng thứ (n-1) chỉ có những
cặp đã có ở tháng thứ (n-2) mới sinh con ở tháng thứ n được thơi.
Do đó: F(n) = F(n-2) + F(n-1)
Vì vậy có thể tính F(n) theo:
1 if n ≤ 2
F(n) = 
 F(n − 2) + F(n − 1) if n > 2

Dãy số thể hiện F(n) ứng với các giá trị của n= 1, 2, 3,... có dạng:
1
1
2
3
5
8
13
21
34

55
...
Nó được gọi là dãy số Fibonacci. Dãy số Fibonacci cịn là mơ hình của rất
nhiều hiện tượng tự nhiên và cũng được sử dụng nhiều trong tin học. Sau đây là thủ
tục đệ quy thể hiện giải thuật tính F(n):
F(n)
1. if n <= 2
F=1;
else F=F(n-2) + F(n-1);
2. Return
Ở đây chỉ có một chi tiết hơi khác là trường hợp suy biến ứng với hai giá trị
F(1) = 1 và F(2)=1.
c) Giải thuật:
int Fibonacci (int n)
{
if (n<=2)
Return 1;
else return Fibonacci(n-2)+ Fibonacci(n-1);
}
2.4.

Bài toán tìm tất cả hoán vị của n phần tử

a) Nhận xét bài toán:

14


Yêu cầu của bài toán là chúng ta phải nhập một số ngun dương n, sau đó
chương trình phải liệt kê tất cả các hoán vị của 1,2,3…n

Giả sử với n = 3:
• Khi đó hốn vị đầu tiên sẽ là 1 2 3.
• Hốn vị tiếp theo sẽ phải lớn hơn hốn vị ban đầu 1 3 2.
• Tương tự như vậy hoán cuối cùng sẽ là hoán vị lớn nhất 3 2 1.
b) Phân tích bài tốn:
Chúng ta sẽ dùng một mảng A[n+1] lưu các hoán vị, khi đó các hốn vị sẽ được
biểu diễn như sau:
A[1], A[2], A[3], …,A[n].
Trong đó A[i] ≠ A[j], với mọi i,j ∈ [1,n] và i ≠ j.
Các giá trị từ 1 đến n lần lượt được đề cử cho pi, trong đó giá trị j được chấp
nhận nếu nó chưa được dùng. Vì vậy, cần phải ghi nhớ với mỗi giá trị j xem nó đã
được dùng hay chưa. Điều này được thực hiện nhờ một dãy các biến logic bj, trong đó
bj= true nếu j chưa được dùng. Các biến này phải được khởi đầu giá trị true trong thủ
tục Khoitao. Sau khi gán j cho pi, cần ghi nhận false cho bj và phải gán true khi thực
hiện xong thủ tục Ketqua hay Try(i+1).
Ý tưởng của phương pháp quay lui là chúng ta sẽ chọn ra một phần tử chưa sử
dụng. Lưu phần tử đó vào một cấu hình tổ hợp, sau đó đánh dấu nó đã sử dụng. Ta sẽ
lặp lại công việc như trên đến khi đủ cấu hình cho một tổ hợp thì sẽ xuất ra màn hình.
Sau khi xuất ra ta lại quay trở lại bước trước đó để đánh dấu là nó chưa được chọn.
Ta có thể hình dung bài tốn như hình vẽ sau: Với n=3 thì bài tốn trở thành liệt
kê các hốn vị của các phần tử 1, 2, 3. Các hoán vị được liệt kê theo thứ tự từ điển tăng
dần như hình vẽ sau:

15


c) Giải thuật:
void Try(int i)
{
int j;

for (j = 1; j <= n; j++)
{
if (B[j])
{
P[i] = j;
B[j] = FALSE;
if (i == n) Ketqua();
else Try(i + 1);
B[j] = TRUE;
}
}
}

2.5.

Bài toán 8 quân hậu

a) Nhận xét bài toán:

16


Xét bàn cờ hình vng 8 hàng 8 cột. Qn hậu là một quân cờ có thể ăn được
bất kỳ quân nào nằm trên cùng một hàng, cùng một cột hay cùng một đường chéo.
Bài toán đặt ra: Hãy xếp 8 qn hậu trên bàn cờ sao cho khơng có quân hậu nào
ăn được quân hậu nào, có nghĩa trên mỗi hàng mỗi cột chỉ có thể có một quân hậu mà
thôi.
Nét đặc trưng để giải bài này là ở mỗi lời giải là một bước thử. Nếu có một
bước thử được chấp nhận thì ghi nhớ các thơng tin cần thiết và tiến hành bước thử tiếp
theo. Nếu trái lại khơng có một lựa chọn nào thích hợp thì làm lại bước trước, xoá bớt

các ghi nhớ và quay về các lựa chọn còn lại. Hoạt động trên là quay lui
b) Phân tích bài tốn:
Ta khơng nên tìm lời giải cho bài toán bằng cách xét từng trường hợp với mọi
vị trí của 8 quân hậu trên bàn cờ rồi lọc các trường hợp chấp nhận được. Phương pháp
thử từng bước này tuy khơng hay lắm nhưng lại có thể đưa ra được tất cả các cách sắp
xếp vị trí cho các quân hậu.
Phương pháp này được gọi là thuật tốn đệ quy quay lui.Nó được áp dụng trong
cách giải bài toán 8 quân hậu như sau:
Do mỗi cột chỉ có một quân hậu nên lựa chọn đối với quân hậu thứ j, ứng với
cột j, là đặt nó vào hàng nào để đảm bảo “an toàn” nghĩa là không cùng hàng, cùng
đường chéo với (j-1) quân hậu đã được xếp trước đó. Vậy để đi đến các lời giải ta phải
thử tất cả các trường hợp sắp xếp quân hậu đầu tiên tại cột 1. Với mỗi vị trí như vậy ta
lại phải giải quyết bài tốn 7 quân hậu với phần còn lại của bàn cờ, nghĩa là ta đã quay
lui bài toán cũ”.
Ta dùng một thủ tục là dathau để minh hoạ cho giải pháp giải bài này. Cụ thể:
dathau(j)
B1:Khởi phát việc chọn vị trí cho quân hậu thứ j.
B2: Thực hiện việc chọn tiếp theo
do
if an toàn
{
đặt quân hậu;
if (j<8)
{
17


dathau(j+1)
if không thành công.
cất quân hậu;

}
}
while thành công hay hết chỗ
B3:Return
Cụ thể bài tốn được phân tích như sau:
Đối với qn hậu thứ j, vị trí của nó chỉ chọn trong cột thứ j.Vậy tham biến j trở
thành chỉ số cột và việc chọn lựa để tiến hành trên 8 giá trị của chỉ số hàng i.
Để lựa chọn i được chấp nhận, thì hàng i và 2 đường chéo ô (i,j) phải không có
quân hậu nào ở trên đó.
Chú ý là trong hai đường chéo thì đường chéo theo chiều đi lên có các ơ (i,j) mà
tổng i + j khơng đổi, cịn đường chéo theo đường đi xuống có các ơ (i,j) mà i - j khơng
đổi.
Do đó ta sẽ chọn các mảng một chiều Boolean để biểu diễn các tình trạng này
là:
a[i] = true có nghĩa là khơng có qn hậu nào chứa hàng i.
b[i + j] = true có nghĩa khơng có qn hậu nào chiếm được đường chéo i + j.
a[i - j] = true có nghĩa là khơng có qn hậu nào chiếm được đường chéo i – j.
Điều kiện: 1≤ i, j ≤ 8 nên suy ra 1≤ j ≤ 8
Vậy: 2 ≤ i + j ≤ 16 và -7 ≤ i - j ≤ 7
Như vậy điều kiện để lựa chọn i được chấp nhận là a[i] and b[i + j] and c[i - j]
có giá trị true.
Quân hậu được đặt theo nguyên tắc :
x[j] = i; a[i] := flase;
b[i+j] := false;
c[i - j] :=flase;
Quân hậu được cất theo nguyên tắc:
a[i] = true;
b[i + j] := true;
c[i - j] := true;
18



Ở đây thuật toán quay lui được sử dụng để kiểm soát sự tiến lùi của quân hậu.
c) Giải thuật:
Với thủ tục dathau trên ta sẽ có một giải thuật cho lời giải bài toán 8 con hậu
trên bàn cờ 8*8 như sau:
void dathau(int i)
{
int j,t,z=0;
if(i==8)
{
inKQ(x);
dem++;
}
else
for(j=0;j<8;j++)
if(a[j])
{

z=0;
for(t=0;tif((i-j)==b[t]||(i+j)==c[t])
z =1;
if(z==0)
{
x[i]=j;
a[j]=0;
b[i]=i-j;
c[i]=i+j;
dathau(i+1);

a[j]=1;
}
}

}

19


Chương 3. Cài đặt thuật toán trên ngôn ngữ C/C++
3.1.

Bài toán tính giai thừa
a) Chương trình cài đặt:
#include<stdio.h>
#include<conio.h>
long tinhgiaithua(int n) // khai bao ham tinhgiaithua
{
if (n == 0)
return 1;
else
return(n * tinhgiaithua(n-1));
}
int main() // ham main de tinh giai thua
{
int n;
long giaithua;
printf("Nhap mot so bat ky: ");
scanf("%d", &n);
giaithua = tinhgiaithua(n);

printf("Giai thua cua %d la: %d!=%ld", n, n, giaithua);
getch();
}
b) Hiển thị kết quả:

20


3.2.

Bài toán tháp Hà Nội
a) Chương trình cài đặt:
#include<stdio.h>
#include<conio.h>
void moves (int n, char a, char c)
{
printf("Chuyen dia thu %d tu coc %c sang coc %c.\n", n, a, c);
return;
}
int Ha_Noi_Tower(int n , char a, char b, char c )
{
if(n==1)
moves(n,a,c);
else
{
Ha_Noi_Tower(n-1,a,c,b);
moves(n,a,c);
Ha_Noi_Tower(n-1,b,a,c);
}
}

int main()
{
21


char a='A', b='B', c='C';
int n;
printf("Nhap n: ");
scanf("%d",&n);
printf("Thu tu di chuyen cac dia:\n \n");
Ha_Noi_Tower(n,a,b,c);
getch();
}
b) Hiển thị kết quả:

3.3.

Bài toán dãy số Fibonacci
a) Chương trình cài đặt:
#include<stdio.h>
#include<conio.h>
//ham doc day so Fibonacci
int Fibonacci(int n)
{
if (n < 0)
return -1;
else
22



if (n == 0 || n == 1)
return n;
else
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main()
{
int i, n;
printf(“Tinh n so dau tien cua day so Fibonacci, vui long nhap n= ”);
scanf(“%d”,&n);
printf("%d so dau tien cua day so Fibonacci: \n", n);
for (i = 0; i < n; i++)
printf("%d ", fibonacci(i));
getch();
}
b) Hiển thị kết quả:

3.4.

Bài toán tìm tất cả hoán vị của n phần tử
a) Chương trình cài đặt:
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#define MAX 100
#define TRUE 1
23


#define FALSE 0

int P[MAX], B[MAX], n, count = 0;
void Khoitao(void)
{
int i;
printf("\n Nhap n=");
scanf("%d", &n);
for (i = 1; i <= n; i++)
B[i] = TRUE;
}
void InKQ(void)
{
int i; count++;
printf("\n Hoan vi thu %d:", count);
for (i = 1; i <= n; i++)
printf("%3d", P[i]);
}
void Try(int i)
{
int j;
for (j = 1; j <= n; j++)
{
if (B[j])
{
P[i] = j;
B[j] = FALSE;
if (i == n) InKQ();
else Try(i + 1);
B[j] = TRUE;
}
}

}
int main(void)
{
Khoitao();

24


Try(1);
getch();
}
b) Hiển thị kết quả:

3.5.

Bài toán 8 con hậu
a) Chương trình cài đặt:
#include<stdio.h>
#include<conio.h>
int x[90],a[90],b[90],c[90],dem=0,n;
//in ket qua cua phuong an
void inKQ(int x[90])
{
int i,t;
printf(“cach thu %d:\n”,dem);
for(i=0;i<8;i++)
{
for(t=0;t<8;t++)
{
if(t!=x[i])

printf(" -");
else
printf(" H");

25


×