Tải bản đầy đủ (.pdf) (78 trang)

chuyen de do thi, quay lui, quy hoach dong

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 (1.48 MB, 78 trang )

Chuyên Đề:

PHÂN TÍCH THIẾT KẾ THUẬT TOÁN
PHẦN 1: THUẬT TOÁN VÀ PHÂN TÍCH THUẬT TOÁN
I. KHÁI NIỆM BÀI TOÁN VÀ THUẬT TOÁN
1. Khái niệm bài toán
Trong phạm vi tin học, ta có thể quan niệm bài toán là một việc nào đó ta muốn máy tính
thực hiện.
Một bài toán được mô tả bởi hai thành phần: tập dữ liệu đầu vào cho trước (Input) và tập
kết quả ra cần nhận được (Output).
Thuật ngữ “một” bài toán được hiểu theo nghĩa phổ dụng, bài toán có nhiều bộ dữ liệu khác
nhau.
Ví dụ: giải bài toán tìm ước chung lớn nhất (UCLN) của hai số nguyên dương M, N. Thuật
toán để giải bài toán này phải đáp ứng với bất kì bộ giá trị cụ thể của bộ dữ liệu vào M, N.
2. Khái niệm thuật toán:
Việc cho một bài toán là mô tả rõ Input và Output. Vấn đề là làm thế nào để tìm ra Ouput?.
Đó chính là lời giải của bài toán
Có nhiều xu hướng khác nhau trong việc nghiên cứu lời giải của các bài toán. Trong toán
học có xu hướng nghiên cứu định tính, chứng minh sự tồn tại lời giải bài toán mà không cần
thiết chỉ ra một cách tường minh cách tìm lời giải đó.Trong việc nghiên cứu thuật toán ở đây,
chúng ta quan tâm tới cách tường minh để tìm Output từ Input của bài toán.
Có nhiều định nghĩa khác nhau về thuật toán, dưới đây là một định nghĩa thường dùng
Thuật toán để giải một bài toán là một dãy hữu hạn các thao tác được sắp xếp theo một
trình tự xác định sao cho sau khi thực hiện dãy thao tác ấy, từ Input của bài toán, ta nhận được
Output cần tìm
3. Các bƣớc giải bài toán trên máy tính:
a. Xác định bài toán:
Việc xác định bài toán chính là xác định rõ hai thành phần Input, Output và mối liên hệ giữa
chúng.
Ví dụ: giải bài toán tìm ước chung lớn nhất (UCLN) của hai số nguyên dương M, N.
Input: hai số nguyên dương M, N


Output: UCLN của M và N

1


b. Thiết kế hoặc lựa chọn thuật toán:
Một bài toán có thể có nhiều thuật toán để giải. Nhưng một thuật toán thì chỉ có thể dùng để
giải quyết cho một bài toán cụ thể mà thôi. Vấn đề đặt ra ở đây nếu có nhiều thuật toán để giải
một bài toán thì ta nên chọn thuật toán nào? Dĩ nhiên ta sẽ chọn thuật toán nào tốt nhất. Nhưng
một thuật toán thế nào được cho là tốt nhất, nó được đánh giá dựa trên những tiêu chí nào?
Tùy theo bài toán mà ta sẽ lựa chọn thuật toán theo các tiêu chí sau đây:
(1) Tính hiệu quả về thời gian: là thời gian để thực hiện xong một bài toán
(2) Tính hiệu quả về không gian: là dung lượng cần thiết để lưu trữ các dữ liệu cần thiết khi
xử lí bài toán
(3) Tính khả thi khi cài đặt thuật toán: là thuật toán có dễ cài đặt hay không, có tốn nhiều
công sức của người lập trình không.
Ví dụ: Đối với một bài toán lớn và số lần giải bài toán là nhiều lần thì tiêu chí (1) sẽ được
ưu tiên hàng đầu. Nhưng đối với bài toán nhỏ và số lần giải bài toán đó không nhiều thì thông
thường ta sẽ chọn tiêu chí (3) làm tiêu chí được chọn đầu tiên
c. Viết chương trình:
Viết chương trình là tổng hợp giữa việc lựa chọn cách tổ chức dữ liệu và sử dụng ngôn ngữ
lập trình để diễn đạt đúng thuật toán
Cấu trúc dữ liệu (Data Structures): là các đơn vị cấu trúc (construct) của ngôn ngữ lập
trình dùng để biểu diễn các mô hình dữ liệu, ví dụ như mảng (array), bản ghi (record), tệp
(file), xâu (string), danh sách liên kết (list linked),…
Các cấu trúc dữ liệu được lựa chọn không những cần có khả năng biểu diễn được dữ liệu
vào Input và dữ liệu ra Output của bài toán cần giải mà còn cần phải phù hợp với các thao tác
của thuật toán và cài đặt được bằng ngôn ngữ lập trình đã được lựa chọn.
Hai bước b và c thường được thực hiện không tách rời và gắn kết chặt chẽ với nhau rất chặt
chẽ. Vì có những thuật toán chỉ thích ứng và hiệu quả với một cách tổ chức dữ liệu nhất định.

Như vậy có thuật toán tốt chưa đủ mà phải chọn được cấu trúc dữ liệu thích hợp, đồng thời phải
có kĩ thuật cài đặt chương trình tốt.
d. Kiểm thử và hiệu chỉnh:
Sau khi được viết xong, chương trình vẫn còn có thể có nhiều lỗi khác chưa phát hiện được
nên có thể không có kết quả đúng. Vì vậy, cần phải kiểm thử tính đúng đắn của chương trình.
Có nhiều cách để thực hiện được điều đó, cách thường dùng là thực hiện chương trình với một
số bộ Input tiêu biểu phù thuộc vào đặc thù của bài toán mà bằng cách nào đó ta đã biết trước
được bộ kết quả Output tương ứng. Các bộ Input và Output tương ứng này gọi là các Test.
Trong quá trình kiểm thử nếu phát hiện có sai sót, ta phải hiệu chỉnh lại chương trình rồi thử
lại. Trên cơ sở đánh giá các lỗi của chương trình qua kiểm thử có thể xác định được hoặc ngôn
ngữ lập trình, hoặc cấu trúc dữ liệu thậm chí cả thuật toán lựa chọn là không phù hợp, cần thiết
quay lại các công việc của bước trước đó.

2


e. Viết tài liệu
Tài liệu trình bày các nội dung liên quan đến mô tả bài toán, thuật toán, cấu trúc dữ liệu,
thiết kế chương trình và hướng dẫn sử dụng. Tài liệu (bao gồm cả các chú thích trong chương
trình) là cần thiết không những cho người dùng để có thể khai thác tốt chương trình, đề xuất
phương án để hoàn thiện chương trình mà còn cho chính người lập trình hoặc người đọc
chương trình dễ dàng chỉnh sửa, nâng cấp chương trình khi cần thiết
II. PHÂN TÍCH THUẬT TOÁN
1. Độ phức tạp của thuật toán:
Một trong những tiêu chí thường được lấy để đánh giá thuật toán là thời gian thực hiện thuật
toán. Vậy, làm thế nào để “đo được” thời gian thực hiện của một chương trình (mô tả một thuật
toán)?
a. Dùng mẫu chuẩn
Đây là cách rất tự nhiên và thường được sử dụng. Dựa vào thời gian thực tế thực hiện các
chương trình viết bằng cùng một ngôn ngữ lập trình cho các thuật toán khác nhau với cùng các

bộ dữ liệu vào Input như nhau, trên cùng một hệ thống máy tính để kết luận thuật toán nào
nhanh, thuật toán nào chậm. Tuy nhiên việc này cũng dễ mắc lỗi vì khi thực hiện chương trình,
ngoài yếu tố thuật toán còn các yếu tố khác của hệ thống, đặc trưng dữ liệu chạy thử cũng có
ảnh hưởng đến thời gian chạy của chương trình. Vì thế để so sánh chính xác hơn cần xây dựng
các bộ dữ liệu vào Input theo các mẫu chuẩn (benchmark) sao cho có thể thừa nhận một
chương trình thực hiện tốt trên mẫu chuẩn thì được coi là tốt trên mọi dữ liệu vào.
b. Phân tích thuật toán
Một cách khác để đánh giá chương trình (thuật toán) là dựa vào mỗi câu lệnh của chương
trình nguồn sẽ thực hiện bao nhiêu lần trên một tập dữ liệu vào. Phần lớn các môi trường lập
trình đều có bộ đếm lệnh (statement couter) là công cụ để thực hiện phép đo đó.
Mục tiêu của phân tích thuật toán không chỉ là để so sánh, đánh giá giúp cho việc lựa chọn
thuật toán tốt mà còn dựa vào kết quả phân tích đánh giá đó để hiệu chỉnh, cải tiến thuật toán
đã có được tốt hơn. Nhiều chương trình cho thấy, tổng thời gian thực hiện một chương trình lớn
thường tiêu phí cho những phần “nhỏ” của chương trình. Về nguyên tắc, số các thao tác trừu
tượng trong thuật toán là rất lớn. Tuy nhiên thường thì tính năng của thuật toán chỉ phục thuộc
vào một vài đại lượng. Khi đánh giá thời gian thực hiện thuật toán ta chú ý đặc biệt đến các
phép toán mà số lần thực hiện không ít hơn các phép toán khác (chứa trong các phần “nhỏ” của
chương trình).
Việc chú ý đến các phép toán được thực hiện nhiều lần là điều cần thiết khi cài đặt thuật
toán bằng chương trình. Ví dụ, người lập trình cần quan tâm lớn để cải tiến các “vòng lặp
trong” vì các phép toán trong đó là các phép toán được thực hiện lặp lại nhiều lần nhất.
Cách đánh giá thời gian thực hiện thuật toán độc lập với hệ thống máy tính dẫn tới khái
niệm độ phức tạp của thuật toán. Thời gian để thực hiện thuật toán phụ thuộc rất nhiều yếu tố.
Một yếu tố rất quan trọng là kích thước của dữ liệu vào. Dữ liệu càng lớn thì thời gian thực
hiện thuật toán càng lớn. Ta kí hiệu T(n) là hàm “đo” số lượng các phép toán cơ bản xuất hiện
khi thực hiện thuật toán và đố số nguyễn phúc thảo ngọc là kích thước dữ liệu vào
3


2. Kí pháp độ phức tạp của thuật toán

Ta có thể gọi hàm T(n) là thời gian thực hiện một thuật toán nào đó. Giải sử, f(n) và g(n) là
các hàm xác định dương với mọi n. Khi đó ta nói độ phức tạp tính toán của thuật toán có thời
gian thực hiện là T(n) là:
Hàm O lớn (đọc là ô lớn): O(g(n)) nếu tồn tại các hằng số phan văn mạnh và n0 sao cho
T(n) ≤ c.g(n) với mọi n ≥ n0 và gọi là kí pháp chữ O lớn, hàm g(n) được gọi là giới hạn trên
của hàm T(n).
Ví dụ: nếu T(n) = n2 + 1 thì T(n) = O(n2).
Thật vậy, chọn c = 2 và n0 = 1, khi đó mọi n ≥ 1, ta có:
T(n) = n2 + 1 ≤ 2n2 = 2g(n)
3. Các cách xác định độ phức tạp của thuật toán
a. Quy tắc hằng số
Nếu một thuật toán T có thời gian thực hiện T(n) = O (c1f(n)) với c1 là một hằng số dương
thì có thể coi thuật toán T đó có độ phức tạp tính toán là O (f(n)).
Thật vậy, vì T(n) = O(c1f(n)) nên tồn tại c0 > 0 và n0 >0 để T(n) ≤ c0.c1f(n) với mọi n ≥ n0.
Chọn c = c0.c1, với mọi n > n0 ta có T(n) ≤ cf(n). Vậy quy tắc hằng số là đúng.
Như vậy, hằng số ở đây không quan trọng.
b. Quy tắc cộng
Giả thiết, thuật toán gồm hai phần liên tiếp T1 và T2. Khi đó nếu phần T1 của thuật toán có
thời gian thực hiện là T1(n) = O(f(n)) và phần T2 có thời gian thực hiện là T2(n) = O(g(n)), khi
đó thời gian thực hiện thuật toán sẽ là
T1(n) + T2(n) = O(f(n) + g(n))
Chứng minh: Vì T1(n) = O(f(n)) nên tồn tại các hằng số c1 và n1 sao cho
T1(n) ≤ c1.f(n) với mợi n ≥ n1
Vì T2(n) = O(g(n)) nên tồn tại các hằng số c2 và n2 sao cho
T2(n) ≤ c2.f(n) với mợi n ≥ n2
Chọn c = max(c1, c2) và n0 = max(n1, n2) ta có với mọi n ≥ n0:
T(n) = T1(n) + T2(n) ≤ c1f(n) + c2g(n) ≤ cf(n) + cg(n) = c(f(n) + g(n))
Đó là điều phải chứng minh
c. Quy tắc lấy max
Nếu thuật toán T có thời gian thực hiện T(n) = O(f(n) + g(n)) thì có thể coi thuật toán T có

độ phức tạp tính toán là O(max(f(n),g(n))).
Thật vậy, vì T(n) = O(f(n)+g(n)) nên tồn tại n0 > 0 và c > 0 để T(n) ≤ cf(n) + cg(n), với mọi
n ≥ n0
Vậy T(n) ≤ cf(n) + cg(n) ≤ 2c.max(f(n), g(n)) với mọi n ≥ n0
4


Từ đó suy ra điều cần chứng minh
d. Quy tắc nhân
Nếu đoạn thuật toán T có thời gian thực hiện T(n) = O(f(n)). Khi đó nếu thực hiện k(n) lần
đoạn thuật toán T với k(n) = O(g(n)) thì độ phức tập tính toán sẽ là: O(f(n).g(n))
Chứng minh: Thời gian thực hiện k(n) lần đoạn thuật toán T sẽ là k(n)T(n), theo định nghĩa:
-

Tồn tại ck≥0 và nk>0 để k(n)≤ckg(n) với mợi n≥nk

-

Tồn tại cT≥0 và nT>0 để T(n)≤cTf(n) với mọi n≥nT

Vậy với mọi n ≥ max(nT,nk) ta có k(n)T(n) ≤ ckcT(f(n)g(n)). Từ đó suy ra điều cần chứng
minh.
4. Các thuật ngữ thƣờng dùng cho độ phức tạp thuật toán
Độ phức tạp
O(1)
O(logn)
O(n)
O(nlogn)
O(nb)
O(bn) (b>1)

O(n!)
5. Đánh giá thời gian thực hiện chƣơng trình

Thuật ngữ
Độ phức tạp hằng số
Độ phức tạp lôgarit
Độ phức tạp tuyến tính
Độ phức tạp nlogn
Độ phức tạp đa thức
Độ phức tạp hàm mũ
Độ phức tạp giai thừa

Trước hết, ta phân loại câu lệnh trong ngôn ngữ lập trình bậc cao. Có các loại câu lệnh
thường dùng sau:
- Câu lệnh đơn thực hiện một thao tác, ví dụ câu lệnh gán đơn giản (không chứa lời gọi
hàm trong biểu thức), đọc/ghi đơn giản, câu lệnh chuyển điều khiển đơn giản (break, goto,
continue, return)
-

Câu lệnh hợp thành là dãy các câu lệnh tạo thành một khối độc lập

-

Câu lệnh rẽ nhánh dạng IF…THEN (còn gọi là câu lệnh điều kiện IF)

-

Các câu lệnh lặp

Để đánh giá thời gian thực hiện chương trình, cần thiết phải biết đánh giá thời gian thực hiện

các câu lệnh. Để làm điều đó ta áp dụng các quy tắc tính độ phức tạp thuật toán đã trình bày ở
mục trên, cụ thể:
-

Thời gian thực hiện một lệnh đơn không phụ thuộc vào kích thước dữ liệu nên sẽ là O(1)

max

Thời gian thực hiện một câu lệnh hợp thành sẽ được tính theo quy tắc cộng và quy tắc

- Thời gian thực hiện câu lệnh IF: giả sử thời gian thực hiện hai câu lệnh thành phần dạng
đủ là f(n) và g(n) thì thời gian thực hiện của câu lệnh IF sẽ được tính theo quy tắc max nên sẽ là
O(max(f(n),g(n))).
5


- Thời gian thực hiện câu lệnh lặp sẽ áp dụng theo quy tắc nhân, nghĩa là O(k(n)f(n)),
trong đó k(n) là số lần lặp và f(n) là thời gian thực hiện câu lện bên trong vòng lặp
6. Một số ví dụ
Phân tích thời gian thực hiện của các đoạn chương trình sau:
Ví dụ 1:
VAR i, j, n: longint;
s1, s2: longint;
BEGIN
{1} Readln(n);
{2} s1:=0;
{3} FOR i:=1 TO n DO
{4}
s1:=s1+i;
{5} s2:=0;

{6} FOR j:=1 TO n DO
{7}
s2:=s2+j*j;
{8} writeln(‘1+2+…+’,n,’=’,s1);
{9} writeln(‘1^2+2^2+..+’,n,’^2=’,s2);
END.

Thời gian thực hiện chương trình phụ thuộc vào n
Các câu lệnh {1}, {2}, {4}, {5}, {7}, {8}, {9} có thời gian thực hiện là O(1)
Câu lệnh {3} thời gian thực hiện là O(n). Câu lệnh {6} thời gian thực hiện là O(n)
Vậy thời gian thực hiện của chương trình là
Max(O(1), O(1), O(n), O(1), O(n),O(1), O(1)) = O(n)
Ví dụ 2:
{1}
{2}
{3}
{4}
{5}
{6}

c:=0;
FOR i:=1 TO 2*n DO
c:=c+1
FOR i:=1 TO n DO
FOR j:=1 TO n DO
c:=c+1

Câu lệnh {1}. {3}, {6} có thời gian thực hiện là O(1). Lệnh lặp FOR {2} có số lần lặp là 2n
nên lệnh {2} có thời gian thực hiện là O(n). Lệnh lặp 5 có số lần lặp là n nên lệnh {5} có thời
gian thực hiện là O(n). Lệnh lặp {4} có số lần lặp là n và nó chứ câu lệnh lặp {5} nên thời có

thời gian thực hiện là O(n2).
Vậy thời gian thực hiện của đoạn chương trình trên là
Max(O(1), O(n), O(n2)) = O(n2)
6


Ví dụ 3:
{1}
{2}
{3}

FOR i:=1 TO n DO
FOR j:=1 TO i DO
c:=c+1

Câu lệnh {3} có thời gian thực hiện là O(1)
Khi i=1, j chạy từ 1 đến 1 nên lệnh lặp For {2} lặp 1 lần
Khi i:=2, j chạy từ 1 đến 2 nên lệnh lặp For {2} lặp 2 lần

Khi i:=n, j chạy từ 1 đến n nên lệnh lặp For {2} lặp n lần
Như vậy lệnh {3} được lặp 1+2+3…+n=n(n+1)/2 lần, do đó lệnh {1} có thời gian thực hiện
là O(n2)
Vậy thời gian thực hiện đoạn chương trình trên là O(n2)
BÀI TẬP
Phân tích thời gian thực hiện của các đoạn chƣơng trình sau:
Câu 1.
Readln(n);
S:=0;
i:=1;
WHILE i

BEGIN
Readln(X);
S:= S + X;
Inc(i);
END;
M:= S/n;
Writeln(M);

Câu 2.
FOR i := 1 TO n DO
FOR j := 1 TO n DO
BEGIN
c[i,j] := 0;
FOR k := 1 TO n DO
c[i,j] := c[i,j] + a[i,k] * b[k,j];
END;
7


Câu 3.
j:=n;
d:=0;
REPEAT
j:=j div 2;
d:=d+1;
UNTIL j<1

Câu 4.
FOR i:=1 TO n DO
IF i mod 2 =0 THEN c:=c+1

ELSE c:=c+2;

Câu 5.
FOR i:=1 TO n DO
IF i mod 2 = 0 THEN
FOR j:=1 TO n DO DO c:=c+1;

Câu 6.
D:=0
FOR i:=1 TO n-2 DO
FOR j:=i+1 TO n-1 DO
FOR k:=j+1 TO n DO d:=d+1;

8


PHẦN 2: THIẾT KẾ THUẬT TOÁN
I. THUẬT TOÁN DUYỆT
Như chúng ta đã biết các thuật toán được xây dựng để giải quyết vấn đề nhờ vào một quy
tắc nào đó. Tuy nhiên có những vấn đề không theo quy tắc, và khi đó phải dùng phương pháp
vét cạn.
Vét cạn, duyệt, quay lui… là một số tên gọi tuy không đồng nghĩa nhưng cùng chỉ một
phương pháp rất đơn giản trong tin học: tìm nghiệm của một bài toán bằng cách xem xét tất cả
các phương án có thể. Đối với con người phương pháp này thường là không khả thi vì số
phương án cần kiểm tra quá lớn. Tuy nhiên đối với máy tính, nhờ tốc độ xử lí nhanh, máy tính
có thể giải rất nhiều bài toán bằng phương pháp vét cạn.
Ưu điểm lớn nhất của phương pháp vét cạn là luôn đảm bảo tìm ra nghiệm chính xác. Ngoài
ra phương pháp vét cạn còn có một số ưu điểm so với các phương pháp khác là đòi hỏi rất ít bộ
nhớ và cài đặt đơn giản. Hạn chế duy nhất của phương pháp này là thời gian thực thi rất lớn, độ
phức tạp thường ở bậc mũ. Do đó vét cạn thường chỉ áp dụng tốt với các bài toán có kích thước

nhỏ.
Mặc dù vậy, không nên coi thường phương pháp này. Rất nhiều bài toán chỉ có thuật toán
duy nhất là vét cạn. Trong một số tình huống khác, chẳng hạn như thời gian lập trình hạn chế
thì vét cạn có thể coi như một giải pháp tình thế. Rất nhiều trường hợp ta có thể sử dụng vét
cạn theo phương châm: thà mất 1tiếng để viết một chương trình vét cạn chạy trong trong 4
tiếng, còn hơn mất 4 ngày tìm thuật toán hiệu qủa để chương trình chạy trong 1 phút.
Chúng ta không đề cập kĩ về việc áp dụng phương pháp vét cạn đối với các bài toán đơn
giản như tìm giá trị nhỏ nhất, lớn nhất hay tìm tất cả các số nguyên tố của một tập hợp. Chúng
ta sẽ xem xét thuật toán vét cạn đối với các bài toán tìm cấu hình tổ hợp và bài toán tối ưu tổ
hợp, là lớp các bài toán rất tổng quát và phổ biến trong tin học
1. Phƣơng pháp:
Trong nhiều bài toán, việc tìm nghiệm có thể quy về việc tìm vectơ hữu hạn (x1,x2,…,x n,
…), độ dài vectơ có thể xác định trước hoặc không. Vectơ này cần phải thỏa mãn một số điều
kiện tùy thuộc vào yêu cầu của bài toán. Các thành phần xi được chọn ra từ tập hữu hạn Ai.
Tùy từng trường hợp mà bài toán có thể yêu cầu: tìm một nghiệm, tìm tất cả các nghiệm
hoặc đếm số nghiệm.
Tư tưởng của thuật toán quay lui vét cạn như sau: Ta xây dựng vectơ nghiệm từng bước, bắt
đầu từ vecto không (). Thành phần đầu tiên x1 được chọn ra từ tập S1=A1. Giả sử đã chọn được
các thành phần x1, x2, …, xi. Lặp lại quá trình trên để tiếp tục mở rộng nghiệm. Nếu không thể
chọn được thành phần xi+1 (Si+1 rỗng) thì ta quay lại chọn một phần tử khác của Si cho xi. Nếu
không còn một phần tử nào khác của Si ta quay lại chọn một phần tử khác của Si-1 làm xi-1 và
cứ thế tiếp tục. Trong quá trình mở rộng nghiệm, ta phải kiểm tra nghiệm đang xây dựng đã là
nghiệm của bài toán chưa. Nếu chỉ cần tìm một nghiệm thì khi gặp nghiệm ta dừng lại. Còn nếu
cần tìm tất cả các nghiệm thì quá trình đó chỉ dừng lại khi tất cả các khả năng lựa chọn của
thành phần của vectơ nghiệm đã được vét cạn.
Thuật toán duyệt thường được dùng mô hình đệ quy sau:
9


PROCEDURE Try(i)

BEGIN
<xác định Si>
FOR xi  Si DO BEGIN
<ghi nhận thành phần thứ i>
IF (tìm thấy nghiệm) THEN <đưa ra nghiệm>
ELSE Try(i+1)
<loại phần tử thứ i>
END;
END;

Hoặc ta có thể sử dụng mô hình đệ quy quay lui như sau:
PROCEDURE Try(i)
BEGIN
FOR (mỗi phương án chọn) DO
IF (chọn được) THEN
BEGIN
<Thực hiện bước đi thứ i>
IF (thành công) THEN (thông báo kết quả)
ELSE Try(i+1)
<Hủy bước đi thứ i>
END;
END;

Với mô hình tổng quát trên ta có ba vấn đề trọng tâm cần làm là:
- Tìm cách biểu diễn nghiệm của bài toán dưới dạng một dãy các đối tượng được chọn
dần từng bước;
- Xác định tập Si các ứng cử viên được chọn làm thành phần tứ i của nghiệm. Chọn cách
thích hợp để biễu diễn Si;
-


Tìm các điều kiện để một vectơ đã được chọn làm nghiệm bài toán.

2. Giải các bài toán cấu hình tổ hợp bằng thuật toán duyệt
a. Tổ hợp:
Một tổ hợp chập k của n là một tập con k phần tử của tập n phần tử.
Số lượng tổ hợp chập k của n được tính theo công thức sau:

C kn 

n!
k!(n  k)!

Chẳng hạn tập {1,2,3,4} có các tổ hợp chập 2 là: {1,2}, {1,3, {1,4, {2,3}, {2,4}, {3,4}. Vì
trong tập hợp các phần tử không phân biệt thứ tự nên tập {1,2} cũng là tập {2,1} và do đó, ta
coi chúng chỉ là một tổ hợp.
Bài toán đặt ra cho chúng ta là hãy xác định tất cả các tổ hợp châp k của tập n phần tử. Để
đơn giản ta chỉ xét bài toán tìm các tổ hợp của tập các số nguyên từ 1 đến n. Đối với một tập
10


hữu hạn bất kì, bằng cách đánh số thứ tự của các phần tử, ta cũng đưa được về bài toán đối với
tập các số nguyên từ 1 đến n.
Nghiệm cần tìm của bài toán tìm các tổ hợp chập k của n phần tử phải thoả mãn các điều
kiện sau:
-

Là một vector x =(x1,x2,…xk)

-


xi lấy giá trị trong tập {1,2,…n}

-

Ràng buộc: xi
Có ràng buộc 3 là vì tập hợp không phân biệt thứ tự phần tử nên ta sắp xếp các phần tử theo
thứ tự tăng dần.
Để giải quyết bài toán này ta cần xác định 3 vấn đề quan trọng
-

Biểu diễn nghiệm: xây dựng một mảng x để biểu diễn tổ hợp

- Xác định tập Si: là từ xi-1+1 đến (n-k+i). Để điều này đúng cho cả trường hợp i = 1 ta
thêm vào x0=0
-

Điều kiện để một vectơ được chọn là: x[i1]< x[i]  nki.

Dưới đây là toàn bộ chương trình giải bài toán trên. Để đơn giản, các giá trị n,k được nhập
từ bàn phím và các tổ hợp được in ra màn hình. Người đọc có thể cải tiến chương trình để
nhập/xuất ra file.
PROGRAM tohop;
USES crt;
CONST
max = 20;
VAR
n,k : integer;
x : array[0..max] of integer;
{===============================}

PROCEDURE Input;
BEGIN
clrscr;
write('n,k = '); readln(n,k);
writeln('Cac TO hop chap ',k,' cua ',n);
END;
{===============================}
PROCEDURE print;
VAR
i : integer;
BEGIN
FOR i := 1 TO k DO write(' ',x[i]);
writeln;
END;
{===============================}
PROCEDURE try(i:integer);
VAR j : integer;
BEGIN
11


FOR j := x[i-1]+1 TO n-k+i DO
BEGIN
x[i] := j;
IF i = k THEN Print ELSE try(i+1);
END;
END;
{===============================}
PROCEDURE solve;
BEGIN

x[0] := 0;
try(1);
END;
{===============================}
BEGIN
Input;
solve;
END.

b. Chỉnh hợp lặp:
Chỉnh hợp lặp chập k của n là một dãy k thành phần, mỗi thành phần là một phần tử của tập n
phần tử, có xét đến thứ tự và không yêu cầu các thành phần khác nhau.
Số lượng chỉnh hợp lặp chập k của n được tính theo công thức sau:
k

An  n k
Một ví dụ dễ thấy nhất của chỉnh hợp lặp là các dãy nhị phân. Một dãy nhị phân độ dài m là
một chỉnh hợp lặp chập m của tập 2 phần tử {0,1}. Chẳng hạn 101 là một dãy nhị phân độ dài
3. Ngoài ra ta còn có 7 dãy nhị phân độ dài 3 nữa là 000, 001, 010, 011, 100, 110, 111. Vì có
xét thứ tự nên dãy 101 và dãy 011 là 2 dãy khác nhau.
Như vậy, bài toán xác định tất cả các chỉnh hợp lặp chập k của tập n phần tử yêu cầu tìm
các nghiệm như sau:
-

Là một vector x =(x1,x2,…xk)

-

xi lấy giá trị trong tập {1,2,…n}


-

Không có ràng buộc nào giữa các thành phần.

Chú ý là cũng như bài toán tìm tổ hợp, ta chỉ xét đối với tập n số nguyên từ 1 đến n. Nếu tập
hợp cần tìm chỉnh hợp không phải là tập các số nguyên từ 1 đến n thì ta có thể đánh số các
phần tử của tập đó để đưa về tập các số nguyên từ 1 đến n
Do không có ràng buộc nào giữa các thành phần nên đối với bài này chỉ cần dùng một mảng
x để lưu nghiệm
Dưới đây là chương trình
PROGRAM Chinhhoplap;
USES crt;
CONST max = 20;
VAR
12


n : integer;
x : array[1..max] of integer;
{===============================}
PROCEDURE Input;
BEGIN
clrscr;
write('n = '); readln(n);
writeln('Cac day nhi phan DO dai ',n);
END;
{===============================}
PROCEDURE print;
VAR i : integer;
BEGIN

FOR i := 1 TO n DO write(' ',x[i]);
writeln;
END;
{===============================}
PROCEDURE try(i:integer);
VAR j : integer;
BEGIN
FOR j := 0 TO 1 DO
BEGIN
x[i] := j;
IF i = n THEN Print ELSE try(i+1);
END;
END;
{===============================}
PROCEDURE solve;
BEGIN
try(1);
END;
{===============================}
BEGIN
Input;
solve;
END.

c. Chỉnh hợp không lặp:
Khác với chỉnh hợp lặp là các thành phần được phép lặp lại, tức là có thể giống nhau, chỉnh
hợp không lặp chập k của tập n phần tử cũng là một dãy k thành phần lấy từ tập n phần tử có
xét thứ tự nhưng các thành phần không được phép giống nhau.
Số lượng chỉnh hợp không lặp chập k của n được tính theo công thức sau:


A kn 

n!
(n  k)!

Chẳng hạn có n người, một cách chọn ra k người để xếp thành một hàng là một chỉnh hợp
không lặp chập k của n.
13


Một trường hợp đặc biệt của chỉnh hợp không lặp là hoán vị. Hoán vị của một tập n phần tử
là một chỉnh hợp không lặp chập n. Nói một cách trực quan thì hoán vị của tập n phần tử là
phép thay đổi vị trí của các phần tử (do đó mới gọi là hoán vị).
Nghiệm của bài toán tìm các chỉnh hợp không lặp chập k của tập n số nguyên từ 1 đến n là
các vector x thoả mãn các điều kiện:
-

x có k thành phần: x = (x1,x2,…xk)

-

Các giá trị xi lấy trong tập {1,2,..n}

-

Ràng buộc: các giá trị xi đôi một khác nhau, tức là xixj với mọi ij.

Chỉnh hợp không lặp yêu cầu các phần tử phải khác nhau. Để đảm bảo điều đó, ngoài mảng
x, ta sẽ dùng thêm một cấu trúc dữ liệu nữa là mảng d để đánh dấu. Khi một giá trị được chọn,
ta đánh dấu giá trị đó, và khi chọn, ta chỉ chọn các giá trị chưa đánh dấu. Mảng d sẽ là "trạng

thái" của thuật toán.
Dưới đây là chương trình tìm toàn bộ hoán vị của n số nguyên từ 1 đến n
PROGRAM Hoanvi;
USES crt;
CONST max = 20;
VAR n : integer;
x,d : array[1..max] of integer;
{===============================}
PROCEDURE Input;
BEGIN
clrscr;
write('n = '); readln(n);
writeln('Cac hoan vi cua day ',n);
END;
{===============================}
PROCEDURE print;
VAR i : integer;
BEGIN
FOR i := 1 TO n DO write(' ',x[i]);
writeln;
END;
{===============================}
PROCEDURE try(i:integer);
VAR j : integer;
BEGIN
FOR j := 1 TO n DO
IF d[j] = 0 THEN
BEGIN
x[i] := j; d[j] := 1;
IF i = n THEN Print ELSE try(i+1);

d[j] := 0;
END;
END;
{===============================}
14


PROCEDURE solve;
BEGIN
try(1);
END;
{===============================}
BEGIN
Input;
solve;
END.

3. Bài toán 8 quân hậu
Yêu cầu bài toán: Cho bàn cờ vua nxn. Hãy xếp n con hậu lên bàn cờ sao cho không con
nào khống chế con nào. Hai 2 con hậu khống chế nhau nếu chúng ở trên cùng một hàng, một
cột hoặc một đường chéo.
Chẳng hạn khi n =8 ta có một cách đặt sau, các ô đen là các vị trí đặt hậu:

Để chuyển bài toán này về dạng chuẩn của bài toán tìm cấu hình tổ hợp, ta có có nhận xét:
mỗi con hậu phải ở trên một hàng và một cột. Do đó ta coi con hậu thứ i ở hàng i và nếu biết
x[i] là cột đặt con hậu thứ i thì ta suy ra được lời giải. Vậy nghiệm của bài toán có thể coi là
một vector x gồm n thành phần với ý nghĩa:
-

Con hậu thứ i được đặt ở hàng i và cột x[i].


-

x[i] lấy giá trị trong tập {1,2…n}

- Ràng buộc: các giá trị x[i] khác nhau từng đôi một và không có 2 con hậu ở trên cùng
một đường chéo.
Để cài đặt bài toán này, chúng ta sẽ phân tích chi tiết về các ràng buộc trên.
Ràng buộc thứ nhất là các giá trị x[i] phải khác nhau. Ta có thể dùng một mảng đánh dấu
như ở thuật toán hoán vị để đảm bảo điều này.
Ràng buộc thứ 2 là các con hậu không được nằm trên cùng một đường chéo chính và phụ.
Ta dễ dàng nhận ra rằng 2 vị trí (x1,y1) và (x2,y2) nằm trên cùng đường chéo chính nếu:
x1y1=x2y2=const.
Tương tự, 2 vị trí (x1,y1) và (x2,y2) nằm trên cùng đường chéo phụ nếu:
x1y1=x2y2=const

15


Do đó, con hậu i đặt tại vị trí (i,x[i]) và con hậu j đặt tại vị trí (j,x[j]) phải thoả mãn ràng
buộc:
ix[i]  jx[j] và i+x[i]  j+x[j] với mọi ij
Ta có thể viết riêng một hàm Ok để kiểm tra các ràng buộc đó. Nhưng giải pháp tốt hơn là
dùng thêm các mảng đánh dấu để mô tả rằng một đường chéo chính và phụ đã có một con hậu
khống chế. Tức là khi ta đặt con hậu i ở vị trí (i,j), ta sẽ đánh dấu đường chéo chính i-j và
đường chéo phụ i+j.
Như vậy về cấu trúc dữ liệu, ta dùng 4 mảng:
-

Mảng x với ý nghĩa: x[i] là cột ta sẽ đặt con hậu ở hàng thứ i.


- Mảng cot với ý nghĩa: cot[j]=1 nếu cột j đã có một con hậu được đặt, ngược lại thì
cot[j]=0.
- Mảng dcc với ý nghĩa: dcc[k]=1 nếu đường chéo chính thứ k đã có một con hậu được
đặt, tức là ta đã đặt một con hậu tại vị trí (i,j) mà ij=k; ngược lại thì dcc[k]=0.
- Tương tự ta dùng mảng dcp với ý nghĩa: dcp[k]=1 nếu đường chéo phụ thứ k đã có một
con hậu được đặt.
Dưới đây là chương trình
CONST n=8;
TYPE vector = array[1..n] of longint;
VAR cot:array[1..n] of longint;
Dcc:array[1-n..n-1] of longint;
Dcp:array[1+1..n+n] of longint;
X:vector;
PROCEDURE print;
VAR i:longint;
BEGIN
FOR i:=1 TO n DO write(x[i],’’);
Writeln;
END;
PROCEDURE xephau(i:longint);
VAR j:longint;
BEGIN
FOR j:=1 TO n DO
IF (cot[j]=0) and (dcc[i-j]=0) and (dcp[i+j]=0) THEN
BEGIN
X[i]:=j;
Cot[j]:=1;
Dcc[i-j]=1;
Dcp[i+j]:=1;

IF j=n THEN print ELSE xephau(j+1);
Cot[j]:=0;
Dcc[i-j]:=0;
Dcp[i+j]:=0;
END;
END;
16


BEGIN
Fillchar(cot,sizeof(cot),0);
Fillchar(dcc,sizeof(dcc),0);
Fillchar(dcp,sizeof(dcp),0);
Xephau(1);
Readln;
END;

Bài này có tất cả 92 nghiệm. Một trong những nghiệm đó là (1, 5, 8, 6, 3, 7, 2, 4)
BÀI TẬP
Câu 1. Bài toán từ đẹp
Tìm tất cả các từ đẹp độ dài n
Một từ đẹp là một xâu độ dài n chỉ gồm các kí tự A,B,C mà không có 2 xâu con liên tiếp
nào giống nhau.
Chẳng hạn ABAC là một từ đẹp độ dài 4, BABCA là một từ đẹp độ dài 5.
Câu 2. Bài toán rút tiền tự động
Một máy ATM hiện có n (n<=20) tờ tiền có mệnh giá t1, t2, .., tn. Hãy đưa ra một cách trả
với số tiền đúng bằng S, nếu không thể trả đúng bằng S thì thông báo không chọn được
Câu 3. Bài toán xếp ba lô
Có một balô có tải trọng m và n đồ vật, đồ vật i có trọng lượng wi và có giá trị vi. Hãy lựa
chọn các vật để cho vào balô sao cho tổng trọng lượng của chúng không quá M và tổng giá trị

của chúng là lớn nhất.
Câu 4. Bài toán ngƣời du lịch
Có n thành phố, d[i,j] là chi phí để di chuyển từ thành phố i đến thành phố j. (Nếu không có
đường đi thì d[i,j] = ). Một người muốn đi du lịch qua tất cả các thành phố, mỗi thành phố
một lần rồi trở về nơi xuất phát sao cho tổng chi phí là nhỏ nhất. Hãy xác định một đường đi
như vậy.
Câu 5. Bài toán mã đi tuần
Cho một bàn cờ có kích thước nxn (n>=3). Một con mã di chuyển theo luật cờ vua được đặt
trong 1 ô với tọa độ đầu là (x1, y1). Hãy tìm một đường đi với n2-1 bước đi sao cho trên mọi ô
trên bàn cờ đều được mã nhảy đến đúng 1 lần
Câu 6. Viết chương trình liệt kê tất cả các xâu nhị phân có đúng n chữ số (n>=3) sao cho
không có xâu con 101
Câu 7. Có n cặp vợ chồng. Hãy xếp họ vào 1 bàn tròn gồm 2n chiếc ghế sao cho
- Không có ghế thừa
- Nam nữ ngồi xen kẻ
- Vợ chồng không được ngồi cạnh nhau
17


II. THUẬT TOÁN SẮP XẾP
1. Tầm quan trọng của bài toán sắp xếp:
Sắp xếp một danh sách các đối tượng theo một thứ tự nào đó là một bài toán thường được
vận dụng trong các ứng dụng tin học. Ví dụ ta cần sắp xếp danh sách thí sinh theo tên với thứ
tự Alphabet, hoặc sắp xếp danh sách sinh viên theo điểm trung bình với thứ tự từ cao đến thấp.
Một ví dụ khác là khi cần tìm kiếm một đối tượng trong một danh sách các đối tượng bằng giải
thuật tìm kiếm nhị phân thì danh sách các đối tượng này phải được sắp xếp trước đó.
Tóm lại sắp xếp là một yêu cầu không thể thiếu trong khi thiết kế các phần mềm. Do đó
việc nghiên cứu các phương pháp sắp xếp là rất cần thiết để vận dụng trong khi lập trình.
2. Sắp xếp trong và sắp xếp ngoài
Sắp xếp trong là sự sắp xếp dữ liệu được tổ chức trong bộ nhớ trong của máy tính, ở đó ta

có thể sử dụng khả năng truy nhập ngẫu nhiên của bộ nhớ và do vậy nó thực hiện rất nhanh.
Sắp xếp ngoài là sự sắp xếp được sử dụng khi số lượng đối tượng cần sắp xếp lớn không thể
lưu trữ trong bộ nhớ trong mà phải lưu trữ trên bộ nhớ ngoài. Cụ thể là ta sẽ sắp xếp dữ liệu
được lưu trữ trong các tập tin.
3. Phát biểu bài toán:
Giả sử các đối tượng cần được sắp xếp là các mẩu tin gồm một hoặc nhiều trường. Một
trong các trường được gọi là khóa sắp xếp (key), kiểu của nó là một kiểu có quan hệ thứ tự
(như các kiểu số nguyên, số thực, chuỗi ký tự...).
Danh sách các đối tượng cần sắp xếp sẽ là một mảng của các mẩu tin vừa nói ở trên. Mục
đích của việc sắp xếp là tổ chức lại các mẩu tin sao cho các khóa của chúng được sắp thứ tự
tương ứng với quy luật sắp xếp.
Ví dụ: Cho mảng a các đối tượng, cần sắp xếp lại các thành phần (phần tử) của mảng a để
nhận được mảng (dãy) a mới với các phần tử có giá trị khóa tăng dần:
a[1].key ≤ a[2].key ≤ … ≤ a[n].key
Ðể trình bày các ví dụ minh họa chúng ta sẽ dùng PASCAL làm ngôn ngữ thể hiện và sử
dụng khai báo sau:
CONST Max = 1000;
TYPE Object = Record
Key : KeyTYPE;
OtherFields : OtherTYPE;
END
Tarray = aray [1..Max]of Object
VAR a: Tarray;
n:longint;
PROCEDURE Swap(VAR x,y:Object);
VAR temp : Object;
BEGIN
temp := x; x := y; y := temp;
END;


Cần thấy rằng thủ tục Swap có thời gian thực hiện là O(1)
18


4. Các thuật toán sắp xếp đơn giản:
a. Sắp xếp nổi bọt (sắp xếp tráo đổi - Bubble Sort):
* Ý tưởng: Chúng ta tưởng tượng rằng các mẩu tin được lưu trong một mảng dọc, qua quá
trình sắp xếp, mẫu tin nào có khóa “nhẹ” sẽ được nổi lên trên. Chúng ta duyệt tòan mảng, từ
dưới lên trên. Nếu hai phần tử ở cạnh nhau mà không đúng thứ tự tức là nếu phần tử “nhẹ hơn”
lại nằm dưới thì phải cho nó “nổi lên” bằng cách đổi chỗ hai phần tử này cho nhau. Việc này
lặp lại cho đến khi không còn phần tử nào đứng sai thứ tự.
* Mô tả cụ thể:
- Lượt thứ nhất: Xét các phần tử từ a[n] đến a[2], với mỗi phần tử a[j], so sánh khoá của nó
với khoá của phần tử a[j-1] đứng ngay trước nó. Nếu khoá của a[j] nhỏ hơn khoá của a[j-1] thì
hoán đổi a[j] và a[j-1] cho nhau. Sau luợt thứ nhất phần tử nhỏ có khóa nhỏ nhất sẽ “nổi lên” vị
trí thứ nhất
- Lượt thứ 2: Xét các phần tử từ a[n] đến a[3], và làm tương tự như trên. Sau lượt thứ 2
phần tử có khóa nhỏ thứ 2 sẽ “nổi lên” vị trí thứ 2

- Lượt thứ n-1: thì dãy đã được sắp xếp xong
Ví dụ: Sắp xếp mảng gồm 10 mẩu tin có khóa là các số nguyên: 5, 6, 2, 2, 10, 12, 9, 10, 9
và 3
Bảng sau ghi lại các giá trị khoá tương ứng với từng lượt thực hiện thuật toán trên.
Khóa

a[1] a[2]

a[3] a[4] a[5] a[6]

a[7] a[8]


a[9] a[10]

Ban đầu

5

6

2

2

10

12

9

10

9

3

Lượt 1

2

5


6

2

3

10

12

9

10

9

2

5

6

3

9

10

12


9

10

3

5

6

9

9

10

12

10

5

6

9

9

10


10

12

6

9

9

10

10

12

9

9

10

10

12

9

10


10

12

10

10

12

10

12

10

12

Lượt

Lượt 2
Lượt 3
Lượt 4
Lượt 5
Lượt 6
Lượt 7
Lượt 8
Lượt 9
Kết quả


2

2

3

5

6

9

9

10

19


* Chương trình:
PROCEDURE BubbleSort;
VAR i,j: integer;
BEGIN
FOR i := 1 TO n-1 DO
FOR j := n DOWNTO i+1 DO
IF a[j].key < a[j-1].key THEN
Swap(a[j],a[j-1]);
END;


* Đánh giá độ phức tạp:
Số phép toán so sánh a[j].key > a[j-1].key được dùng để đánh giá hiệu suất về mặt thời gian
cho thuật toán sắp xếp nổi bọt. Tại lượt thứ i cần n – i phép so sánh. Như vậy tổng số phép so
sánh cần thiết là:
n 1

T(n)   (n  i)  (n  1)  (n  2)  ...  1 
i 1

n(n  1)
 O( n 2 )
2

Vậy thuật toán có độ phức tạp là T(n) = O(n 2 )
* Một cách cài đặt khác của thuật toán trên cũng cho độ phức tạp O(n2)
FOR i := 1 TO n-1 DO
FOR j := i+1 TO n DO
IF a[j].key < a[i].key THEN
Swap(a[j],a[i]);

b. Sắp xếp chọn (Selection Sort):
* Ý tưởng: Tìm phần tử có khóa nhỏ nhất trong dãy sau đó tráo đổi nó với phần tử đầu tiên
của dãy. Sau đó lại tiếp tục tìm phần tử có khóa nhỏ nhất (trừ phần tử đầu tiên) rồi tráo đổi với
phần tử thứ 2. Việc này lặp lại cho đến khi không còn phần tử để tráo đổi.
* Mô tả cụ thể:
- Ðầu tiên chọn phần tử có khóa nhỏ nhất trong n phần tử từ a[1] đến a[n] và hoán vị nó với
phần tử a[1].
- Chọn phần tử có khóa nhỏ nhất trong n-1phần tử từ a[2] đến a[n] và hoán vị nó với a[2].
- Tổng quát ở bước thứ i, chọn phần tử có khoá nhỏ nhất trong n-i+1 phần tử từ a[i] đến a[n]
và hoán vị nó với a[i].

- Sau n-1 bước này thì mảng đã được sắp xếp.
Ví dụ: Sắp xếp mảng gồm 10 mẩu tin có khóa là các số nguyên: 5, 6, 2, 2, 10, 12, 9, 10, 9
và 3

20


Bảng sau ghi lại các giá trị khoá tương ứng với từng lượt thực hiện thuật toán trên.
Khóa
Lượt
Ban đầu
Lượt 1
Lượt 2
Lượt 3
Lượt 4
Lượt 5
Lượt 6
Lượt 7
Lượt 8
Lượt 9
Kết quả

a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10]
5
2

6
6
2


2
5
5
3

2
2
6
6
5

10
10
10
10
10
6

12
12
12
12
12
12
9

9
9
9
9

9
9
12
9

10
10
10
10
10
10
10
10
10

2

2

3

5

6

9

9

10


9
9
9
9
9
9
9
12
12
10
10

3
3
3
5
6
10
10
10
10
12
12

* Chương trình:
PROCEDURE SelectionSort;
VAR i,j,LowIndex: integer;
LowKey: KeyTYPE;
BEGIN

{1} FOR i := 1 TO n-1 DO
BEGIN
{2}
LowIndex := i;
{3}
LowKey := a[i].key;
{4}
FOR j := i+1 TO n DO
{5}
IF a[j].key < LowKey THEN
BEGIN
{6}
LowKey := a[j].key;
{7}
LowIndex := j;
END;
{8}
Swap(a[i],a[LowIndex]);
END;
END;

* Đánh giá độ phức tạp:
Các lệnh: {2}, {3}có thời gian thực hiện là O(1).
Vòng lặp FOR {4} – {7} thực hiện n-i lần vì j chạy từ i+1 đến n, mỗi lần lặp thực hiện hết
O(1) thời gian nên nó tốn O (n-i) thời gian. Mặc khác vòng lặp {1} thực hiện n-1 lần nên thời
gian tổng cộng để thực hiện chương trình là

21



n 1

T(n)   (n  i)  (n  1)  (n  2)  ...  1 
i 1

n(n  1)
 O( n 2 )
2

c. Sắp xếp thêm dần (sắp xếp xen - Insertion Sort):
* Ý tưởng: Lần lượt xét các phần tử trong dãy, tại mỗi phần tử đang xét ta sẽ chèn vào vị trí
thích hợp trong danh sách các phần tử được xét trước đó.
* Mô tả cụ thể:
Trước hết ta xem phần tử a[1] là một dãy đã có thứ tự.
- Lượt 1, xen phần tử a[2] vào danh sách đã có thứ tự a[1] sao cho a[1], a[2] là một danh
sách có thứ tự.
- Lượt 2, xen phần tử a[3] vào danh sách đã có thứ tự a[1], a[2] sao cho a[1], a[2], a[3] là
một danh sách có thứ tự.
- Tổng quát, lượt i, xen phần tử a[i+1] vào danh sách đã có thứ tự a[1],a[2],..a[i] sao cho
a[1], a[2],.. a[i+1] là một danh sách có thứ tự.
- Phần tử đang xét a[j] sẽ được xen vào vị trí thích hợp trong danh sách các phần tử đã được
sắp trước đó a[1],a[2],..a[j-1] bằng cách so sánh khoá của a[j] với khoá của a[j-1] đứng ngay
trước nó. Nếu khoá của a[j] nhỏ hơn khoá của a[j-1] thì hoán đổi a[j-1] và a[j] cho nhau và tiếp
tục so sánh khoá của a[j-1] (lúc này a[j-1] chứa nội dung của a[j]) với khoá của a[j-2] đứng
ngay trước nó...
- Sau n-1 lượt thực hiện việc chèn thì mảng đã được sắp xếp.
Ví dụ: Sắp xếp mảng gồm 10 mẩu tin có khóa là các số nguyên: 5, 6, 2, 2, 10, 12, 9, 10, 9
và 3
Bảng sau ghi lại các giá trị khoá tương ứng với từng lượt thực hiện thuật toán trên.
Khóa

Lượt
Ban đầu
Lượt 1
Lượt 2
Lượt 3
Lượt 4
Lượt 5
Lượt 6
Lượt 7
Lượt 8
Lượt 9

a[1]

a[2]

a[3]

a[4] a[5] a[6] a[7] a[8] a[9] a[10]

5
5
2
2
2
2
2
2
2
2


6
6
5
2
2
2
2
2
2
2

2

2

10

12

9

10

9

3

6
5

5
5
5
5
5
3

6
6
6
6
6
6
5

10
10
9
9
9
6

12
10
10
9
9

12
10

10
9

12
10
10

12
10

12

22


* Chương trình:
PROCEDURE InsertionSort;
VAR i,j: integer;
BEGIN
{1} FOR i := 2 TO n DO
BEGIN
{2}
J := i;
{3}
WHILE (j>1) AND (a[j].key < a[j-1].key) DO
BEGIN
{4}
swap(a[j], a[j-1]);
{5}
j := j-1;

END;
END;
END;

* Đánh giá độ phức tạp:
Vòng lặp {1} có i chạy từ 2 đến n nên nếu gọi T(n) là thời gian để sắp n phần tử thì ta có
n 1

T(n)   (n  i) 
i 1

n(n  1)
 O( n 2 )
2

5. Thuật toán sắp xếp nhanh (QuickSort):
Chúng ta vẫn xét mảng a các phần tử a[1]..a[n]. Giả sử x là 1 giá trị khóa mà ta gọi là
“chốt” . Ta phân hoạch dãy a[1]..a[n] thành hai mảng con "bên trái" và "bên phải". Mảng con
"bên trái" bao gồm các phần tử có khóa nhỏ hơn chốt, mảng con "bên phải" bao gồm các phần
tử có khóa lớn hơn hoặc bằng chốt.
Sắp xếp mảng con “bên trái” và mảng con “bên phải” thì mảng đã cho sẽ được sắp bởi vì tất
cả các khóa trong mảng con “bên trái“ đều nhỏ hơn các khóa trong mảng con “bên phải”.
Việc sắp xếp các mảng con “bên trái” và “bên phải” cũng được tiến hành bằng phương pháp
nói trên.
Một mảng chỉ gồm một phần tử hoặc gồm nhiều phần tử có khóa bằng nhau thì đã có thứ tự.
Để sắp xếp một dãy có chỉ số từ L đến H ta tiến hành các bước như sau:
- Chọn chốt x (có thể chọn ngẫu nhiên hoặc định sẵn thường là chọn x=a[(L+H)div 2])
- Phân hoạch mảng đã cho thành hai mảng con a[L]..a[x] và a[x+1]..a[H]. So sánh các phẩn
tử thuộc mảng bên trái và bên phải với chốt. Cho i chạy từ L sang phải, j chạy từ H sang trái;
nếu phát hiện một cặp ngược thứ tự: i<=j và a[i].key>=a[j].key thì phải đổi chổ hai phần tử đó

cho đến khi i>j. Lúc đó dãy ở trình trạng: khóa các phần tử đoạn L..i < khóa của x; khóa của
các phần tử đoạn j..H >= khóa của x.
- Sắp xếp mảng a[L]..a[j]
- Sắp xếp mảng a[i]..a[H]
Thủ tục QuickSort(L, H) sau dùng để sắp xếp đoạn từ L tới H. Để sắp xếp dãy số, ta gọi
QuickSort(1,n).
23


PROCEDURE QuickSort(L, H: longint);
VAR i,j : longint;
x, tmp: object;
BEGIN
i:=L;
j:=H;
x:=a[(L+H) div 2];
REPEAT
WHILE a[i].key < x.key DO inc(i);
WHILE a[j].key > x.key DO inc(j);
IF i<=j THEN
BEGIN
tmp:a[i];
a[i]:=a[j];
a[j]:=tmp;
inc(i);
dec(j);
END;
UNTIL i>j;
IF LIF i

END;

* Đánh giá độ phức tạp
QuickSort sẽ hoạt động rất kém trong trường hợp dãy đã được sắp sẵn. Khi đó phải mất đến
N lần gọi đệ quy và mỗi lần chỉ loại được 1 phần tử. Thời gian thực hiện trong trường hợp xấu
nhất này là khoảng N2/2 có nghĩa là O(N2)
Trong trường hợp tốt nhất phân chia sẽ được hai nữa dãy bằng nhau. Khi đó thời gian thực
hiện của thuật toán T(N) sẽ được tính là
T(N) = 2T(N/2) + N
Hay T(N) ≈ NlogN nghĩa là O(NlogN)
Trong trường hợp trung bình thuật toán cũng có độ phức tạp khoảng 2NlogN = O(NlogN).
6. Thuật toán sắp xếp hòa nhập (trộn) hai đƣờng trực tiếp (MergeSort):
Ý tưởng của thuật toán này bắt nguồn từ việc trộn hai dãy đã được sắp xếp thành một dãy
mới cũng được sắp xếp.
a. Trộn hai dãy đã sắp xếp:
Bài toán: Cho hai mảng A và B đã được sắp xếp theo chiều không giảm. Từ hai mảng đã
cho hãy đưa các giá trị của A và B vào mảng C mà vẫn đảm bảo trật trự sắp xếp.
Giả sử ta có hai dãy đã sắp xếp như sau:
17

32

49

89

06

25


53
24


Để giải quyết bài này ta dùng hai biến duyệt từ đầu mỗi dãy. Tại mỗi bước, tiến hành so
sánh giá trị của hai phần tử tại vị trí của hai biến duyệt. Nếu phần tử nào nhỏ hơn ta đưa
phần tử đó xuống dưới dãy mới và tăng biến duyệt tương ứng lên 1. Quá trình được lặp lại
cho tới khi tất cả các phần tử của hai dãy đã được duyệt và xét
Mô phỏng thuật toán của ví dụ trên:
Ban đầu:

Bước 1:

Bước 2:

Bước 3:

Bước 4:

Bước 5:

Bước 6:

25


×