You are on page 1of 129

Mục lục 1

CHƯƠNG 1. GIỚI THIỆU ................................................................ 5


1.1. Khái niệm thuật toán .................................................................................. 5
1.2. Giải bài toán bằng thuật toán...................................................................... 9

CHƯƠNG 2. PHÂN TÍCH HIỆU QUẢ THUẬT TOÁN.............. 13


2.1. Các khái niệm liên quan ........................................................................... 13
2.1.1. Kích thước dữ liệu vào ................................................................................13
2.1.2. Đơn vị đo thời gian chạy ............................................................................14
2.1.3. Trường hợp xấu nhất, tốt nhất và trung bình .............................................14
2.1.4. Độ phức tạp thuật toán và kí hiệu O lớn ....................................................15
2.2. Phân tích thuật toán không đệ quy ........................................................... 17
2.2.1. Các bước thực hiện .....................................................................................17
2.2.2. Các ví dụ minh họa .....................................................................................18
2.3. Phân tích thuật toán đệ quy ...................................................................... 20
2.3.1. Các bước thực hiện .....................................................................................20
2.3.2. Các ví dụ minh họa .....................................................................................20
2.4. Phân tích thuật toán bằng thực nghiệm .................................................... 22
2.5. Bài tập....................................................................................................... 22

CHƯƠNG 3. BRUTE-FORCE VÀ DUYỆT TOÀN BỘ ............... 27


3.1. Giới thiệu .................................................................................................. 27
3.2. Sắp xếp chọn và sắp xếp nổi bọt .............................................................. 27
3.2.1. Sắp xếp chọn ...............................................................................................27
3.2.2. Sắp xếp nổi bọt............................................................................................28
3.3. Tìm kiếm tuần tự và khớp chuỗi .............................................................. 29
3.3.1. Tìm kiếm tuần tự .........................................................................................29
3.3.2. Thuật toán khớp chuỗi ................................................................................30
3.4. Bài toán cặp điểm gần nhất và bao lồi ..................................................... 31
3.4.1. Cặp điểm gần nhau nhất .............................................................................31
3.4.2. Bao lồi nhỏ nhất..........................................................................................31
3.5. Duyệt toàn bộ ........................................................................................... 32
3.5.1. Bài toán Người đi du lịch ...........................................................................32
3.5.2. Bài toán Xếp ba lô ......................................................................................33
3.5.3. Bài toán Phân công công việc ....................................................................34
3.6. Bài tập....................................................................................................... 35

CHƯƠNG 4. GIẢM ĐỂ TRỊ ........................................................... 39


4.1. Giới thiệu .................................................................................................. 39

Lê Xuân Việt – Dương Hoàng Huyên


2 Phân tích và thiết kế thuật toán

4.2. Sắp xếp chèn............................................................................................. 41


4.3. Thuật toán sinh tổ hợp .............................................................................. 41
4.3.1. Sinh hoán vị ................................................................................................ 41
4.3.2. Sinh tập con ................................................................................................ 43
4.4. Giảm theo hệ số ........................................................................................ 44
4.4.1. Tìm kiếm nhị phân ...................................................................................... 44
4.4.2. Bài toán tiền xu giả..................................................................................... 44
4.4.3. Phương pháp nhân nông dân Nga.............................................................. 45
4.4.4. Bài toán Josephus ....................................................................................... 45
4.5. Giảm với kích thước thay đổi ................................................................... 47
4.5.1. Tìm trung vị và bài toán chọn .................................................................... 47
4.5.2. Tìm kiếm nội suy ......................................................................................... 50
4.5.3. Trò chơi Nim ............................................................................................... 51
4.6. Bài tập....................................................................................................... 53

CHƯƠNG 5. CHIA ĐỂ TRỊ ............................................................ 55


5.1. Giới thiệu .................................................................................................. 55
5.2. Sắp xếp trộn .............................................................................................. 56
5.3. Sắp xếp nhanh .......................................................................................... 58
5.4. Nhân số nguyên lớn và nhân ma trận ..................................................... 61
5.4.1. Nhân số nguyên lớn .................................................................................... 61
5.4.2. Phương pháp nhân ma trận Strassen ......................................................... 62
5.5. Bài tập....................................................................................................... 63

CHƯƠNG 6. BIẾN ĐỔI ĐỂ TRỊ .................................................... 65


6.1. Giới thiệu .................................................................................................. 65
6.2. Biến đổi bằng sắp xếp .............................................................................. 65
6.2.1. Bài toán phần tử duy nhất .......................................................................... 65
6.2.2. Phần tử xuất hiện nhiều nhất ..................................................................... 66
6.3. Phép khử Gaussian ................................................................................... 66
6.3.1. Giải hệ phương trình đại số tuyến tính ...................................................... 66
6.3.2. Tính ma trận nghịch đảo ............................................................................ 69
6.3.3. Tính định thức ............................................................................................. 69
6.4. Heap và HeapSort..................................................................................... 70
6.4.1. Định nghĩa Heap ........................................................................................ 70
6.4.2. Heap sort .................................................................................................... 73
6.5. Quy tắc Horner và số mũ nhị phân........................................................... 74
6.5.1. Quy tắc Horner ........................................................................................... 74
6.5.2. Hàm mũ nhị phân ....................................................................................... 75

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Mục lục 3
6.6. Bài tập....................................................................................................... 76

CHƯƠNG 7. QUY HOẠCH ĐỘNG ............................................... 79


7.1. Giới thiệu .................................................................................................. 79
7.2. Các ví dụ cơ bản ....................................................................................... 80
7.2.1. Bài toán chọn tiền xu ..................................................................................80
7.2.2. Bài toán đổi tiền..........................................................................................82
7.2.3. Bài toán thu thập đồng tiền ........................................................................83
7.2.4. Trò chơi chọn đồng tiền ..............................................................................85
7.3. Bài toán dãy con chung dài nhất .............................................................. 88
7.4. Bài toán Xếp ba lô và hàm bộ nhớ ........................................................... 89
7.4.1. Bài toán Xếp ba lô ......................................................................................89
7.4.2. Hàm bộ nhớ ................................................................................................91
7.5. Thuật toán Warshall và Floyd .................................................................. 92
7.5.1. Thuật toán Warshall ...................................................................................92
7.5.2. Thuật toán Floyd.........................................................................................94
7.6. Bài tập....................................................................................................... 96

CHƯƠNG 8. KỸ THUẬT THAM LAM ...................................... 101


8.1. Giới thiệu ................................................................................................ 101
8.2. Bài toán lịch phục vụ.............................................................................. 101
8.3. Bài toán lập lịch cho máy ....................................................................... 102
8.4. Mã hóa Huffman .................................................................................... 104
8.5. Thuật toán Prim ...................................................................................... 106
8.6. Thuật toán Kruskal ................................................................................. 107
8.7. Thuật toán Dijkstra ................................................................................. 108
8.8. Bài tập..................................................................................................... 110

CHƯƠNG 9. GIẢI QUYẾT CÁC GIỚI HẠN CỦA THUẬT


TOÁN ............................................................................................... 113
9.1. Giới thiệu ................................................................................................ 113
9.2. Kỹ thuật Quay lui ................................................................................... 113
9.2.1. Phương pháp chung ..................................................................................113
9.2.2. Bài toán N quân hậu .................................................................................114
9.2.3. Bài toán Tổng tập con...............................................................................116
9.2.4. Bài toán tìm chu trình Hamilton ...............................................................117
9.3. Nhánh và cận ....................................................................................... 118
9.3.1. Phương pháp chung ..................................................................................118

Lê Xuân Việt – Dương Hoàng Huyên


4 Phân tích và thiết kế thuật toán
9.3.2. Bài toán Xếp ba lô .................................................................................... 119
9.3.3. Bài toán Người đi du lịch ......................................................................... 121
9.3.4. Bài toán Phân công công việc .................................................................. 123
9.4. Bài tập..................................................................................................... 125

TÀI LIỆU THAM KHẢO .............................................................. 127


Tiếng Việt ...................................................................................................... 127
Tiếng Anh ...................................................................................................... 127

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 1. Giới thiệu 5

CHƯƠNG 1. GIỚI THIỆU

1.1. Khái niệm thuật toán


Thuật toán là tập các chỉ dẫn (Instructions) có thứ tự, không nhập nhằng để giải
bài toán, cụ thể hơn khi cho tập dữ liệu đầu vào hợp lệ, thuật toán sẽ cho kết quả đúng
yêu cầu trong khoảng thời gian hữu hạn. Để hiểu rõ khái niệm này, ta xét ba phương
pháp khác nhau để giải quyết cùng bài toán tìm ước số chung lớn nhất. Ví dụ này minh
họa một vài ý quan trọng sau:
- Tại mỗi bước của thuật toán phải rõ ràng, không được nhập nhằng.
- Mô tả đầy đủ, chính xác dữ liệu đầu vào.
- Thuật toán có thể biểu diễn bằng nhiều cách khác nhau.
- Có thể có nhiều hơn một thuật toán để giải quyết cùng một bài toán.
- Các thuật toán giải một bài toán có thể dựa vào những ý tưởng khác nhau và tốc
độ thực hiện khác nhau.
Quay lại bài toán tìm ước số chung lớn nhất, thuật toán đầu tiên là thuật toán
Euclid. Thuật toán này lặp lại đẳng thức gcd(m, n) = gcd(n, m mod n) cho đến khi m
mod n = 0. Trong đó, m và n không đồng thời bằng 0; gcd(m, n) là ước số chung lớn
nhất của m và n; m mod n là phép chia lấy phần dư của m cho n. Do gcd(m, 0) = m, nên
giá trị m sau cùng chính là ước số chung lớn nhất của m và n ban đầu. Ví dụ, gcd(60,
24) có thể được tính như sau: gcd(60, 24) = gcd(24, 12) = gcd(12,0) = 12. Ta có thể
mô tả lại thuật toán này theo cách rõ ràng hơn như sau:
Bước 1: Nếu n = 0, ước số chung lớn là m và kết thúc thuật toán, ngoài ra chuyển
đến bước 2.
Bước 2: r = m chia dư cho n

Bước 3: m  n và n  r, chuyển đến bước 1.


Hoặc ta cũng có thể biểu diễn thuật toán theo cách khác gọi là giả mã
(Pseudocode) như sau:
ALGORITHM Euclid(m, n)
Lê Xuân Việt – Dương Hoàng Huyên
6 Phân tích và thiết kế thuật toán

//Đầu vào: hai số nguyên m, n không âm và không đồng thời bằng 0


//Đầu ra: ước số chung lớn nhất của m và n
while (n ≠ 0) do
r ← m mod n
m ← n
n ←r
return m
Ngoài hai cách biểu diễn thuật toán như trên, ta cũng có thể dùng sơ đồ khối để
mô tả thuật toán. Sơ đồ khối dùng các kí hiệu với ý nghĩa như sau:

điểm bắt đầu hoặc kết thúc thuật toán.

kiểm tra điều kiện.

mô tả dữ liệu đầu vào/đầu ra.

thực hiện 1 thao tác, 1 bước của thuật toán.

chuyển đến bước tiếp theo của thuật toán.


Dựa vào ý nghĩa của các kí hiệu trên, ta có thể mô tả thuật toán Euclid tìm ước số
chung lớn nhất của hai số m, n như Hình 1-1.

Begin

Đầu vào: 2 số nguyên m, n

n0 Sai
return m End

Đúng

r ← m mod n
m ← n
n ← r
Hình 1-1. Sơ đồ khối mô tả thuật toán Euclid tìm ước số chung lớn nhất.
Thuật toán thứ hai để tìm ước số chung lớn nhất dựa vào định nghĩa ước số
chung lớn nhất của hai số m và n. Đặt t = min{m, n}, ta bắt đầu kiểm tra t có phải là
ước số của m và n hay không? nếu đúng, t là số cần tìm, nếu không đúng ta giảm t đi 1
và thử lại lần nữa. Thuật toán được mô tả bằng ngôn ngữ tự nhiên như sau:

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 1. Giới thiệu 7

Bước 1: Đặt t = min{m, n}.


Bước 2: Chia m cho t, nếu dư 0 chuyển đến bước 3, ngoài ra chuyển đến bước 4.
Bước 3: Chia n cho t, nếu dư 0 trả về giá trị t, ngoài ra chuyển đến bước 4.
Bước 4: Giảm t đi 1 và chuyển đến bước 2.
Thuật toán này cũng có thể biểu diễn bằng giả mã như sau:
ALGORITHM gcd2(m, n)
//Đầu vào: hai số nguyên m, n
//Đầu ra: ước số chung lớn nhất của m và n
t ← min{m, n}
while (m mod t  0) or (n mod t  0) do
t ← t – 1
return t
Thuật toán thứ hai tìm ước số chung lớn nhất của hai số m, n có thể biểu diễn
bằng sơ đồ khối như Hình 1-2.

Begin

Đầu vào: 2 số nguyên m, n

t ← min{m, n}

m mod t  0
Sai End
hoặc return t
n mod t  0

Đúng

t ← t - 1
Hình 1-2. Sơ đồ khối mô tả thuật toán thứ hai tìm ước số chung lớn nhất 2 số.
Không giống thuật toán Euclid, thuật toán này sẽ không làm việc nếu một trong
hai giá trị m hoặc n bằng 0. Điều này minh họa cho vấn đề tại sao ta phải mô tả dữ liệu
đầu vào thật rõ ràng và chính xác.

Lê Xuân Việt – Dương Hoàng Huyên


8 Phân tích và thiết kế thuật toán

Thuật toán thứ ba thực hiện như sau:


Bước 1: Phân tích m thành tích các thừa số nguyên tố.
Bước 2: Phân tích n thành tích các thừa số nguyên tố.
Bước 3: Xác định các thừa số nguyên tố chung đã tìm thấy trong bước 1 và 2.
Giả sử thừa số p xuất hiện pm và pn lần trong khai triển m, n tương ứng, khi đó thừa số
p xuất hiện min{pm, pn} lần trong ước số chung lớn nhất.
Bước 4: Tính tích tất cả các thừa số nguyên tố chung và đó chính là ước số chung
lớn nhất của hai số m và n.
Ví dụ minh họa cách tìm ước số chung lớn nhất của hai số 60 và 24 bằng thuật
toán thứ 3 như sau: Phân tích số 60 thành tích các thừa số nguyên tố: 60 = 2 × 2 × 3 ×
5. Tương tự, phân tích số 24 thành tích các thừa số nguyên tố: 24 = 2 × 2 × 2 × 3. Ta
thấy ở số 60, thừa số 2 xuất hiện 2 lần. Ở số 24, thừa số 2 xuất hiện 3 lần. Do đó thừa
số 2 sẽ xuất hiện trong ước số chung 2 lần. Thừa số 3 xuất hiện 1 lần trong phân tích
hai số 60 và 24, nên thừa số 3 xuất hiện trong ước số chung 1 lần. Vậy ước số chung
lớn nhất của 60 và 24 là 2 × 2 × 3 = 12.
Thuật toán thứ ba tìm ước số chung lớn nhất của hai số m, n biểu diễn bằng giả
mã như sau:
ALGORITHM gcd3(m, n)
//Đầu vào: hai số nguyên m, n
//Đầu ra: ước số chung lớn nhất của m và n
Phân tích m thành tích các thừa số nguyên tố:
m = p1.p2. … .pu, với p1 ≤ p2 ≤ … ≤ pu
Phân tích n thành tích các thừa số nguyên tố:
n = q1.q2. … .qv, với q1 ≤ q2 ≤ … ≤ qv
us ← 1; i ← 1; j ← 1;
while (i <= u) and (j <= v) do
if (pi = qj) then
us ← us * pi; i ← i + 1; j ← j + 1
else
if (pi > qj) then
j ← j +1
else
i ← i +1
return us
Thuật toán thứ 3 tìm ước số chung lớn nhất của hai số biểu diễn bằng sơ đồ khối
như Hình 1-3.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 1. Giới thiệu 9

Begin

Đầu vào: 2 số nguyên m, n

Phân tích m thành tích các thừa số nguyên tố:


m = p1.p2. … .pu, với p1 ≤ p2 ≤ … ≤ pu

Phân tích n thành tích các thừa số nguyên tố:


n = q1.q2. … .qv, với q1 ≤ q2 ≤ … ≤ qv

us ← 1; i ← 1; j ← 1

i ≤ u
và Sai
return us End
j ≤ v

Đúng

Đúng Sai
pi = qj

us ← us * pi Đúng pi > qj Sai


i ← i + 1
j ← j + 1
j ← j+ 1 i ←i + 1

Hình 1-3. Sơ đồ khối mô tả thuật toán thứ ba tìm ước số chung lớn nhất 2 số.

1.2. Giải bài toán bằng thuật toán

Lê Xuân Việt – Dương Hoàng Huyên


1 Phân tích và thiết kế thuật toán
0
Trình tự thiết kế và phân tích một thuật toán thể hiện qua Hình 1-4.

Đọc hiểu bài


toán.

Quyết định: phương tiện tính toán;


kết quả chính xác hay xấp xỉ; kỹ
thuật thiết kế thuật toán.

Thiết kế thuật
toán.

Chứng minh tính


đúng đắn.

Phân tích thuật


toán.

Cài đặt thuật


toán

Hình 1-4. Quá trình phân tích và thiết kế thuật toán.


Việc đầu tiên trước khi thiết kế thuật toán đó là ta phải hiểu bài toán. Đọc mô tả
bài toán cẩn thận và đưa ra một số câu hỏi nếu thấy nghi ngờ, giải một vài ví dụ đơn
giản, kiểm tra các trường hợp đặc biệt.
Khi đã hiểu đầy đủ bài toán, ta cần xác định khả năng tính toán của thiết bị để
thực hiện thuật toán. Rất nhiều thuật toán sử dụng hiện nay được thiết kế để cho lập
trình trên máy tính. Tùy theo bài toán cụ thể, ta có thể thiết kế thuật toán để thực hiện
tuần tự hoặc song song.
Một vấn đề quan trọng tiếp theo là chọn cách giải chính xác hay xấp xỉ (thuật
toán chính xác hay thuật toán xấp xỉ). Ta chọn thuật toán xấp xỉ khi không thể giải
chính xác bài toán, ví dụ như bài toán tính căn bậc hai, giải phương trình phi tuyến,
tính gần đúng tính phân xác định… hoặc thuật toán chính xác quá chậm.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 1. Giới thiệu 11

Kĩ thuật thiết kế thuật toán là phương pháp chung để giải bài toán được áp dụng
cho nhiều bài toán. Kĩ thuật thiết kế sẽ cung cấp một hướng dẫn chung để thiết kế
thuật toán cho bài toán mới.
Khi đã thiết kế xong thuật toán, ta cần phải mô tả lại thuật toán này. Có hai cách
thông dụng hiện nay để mô tả thuật toán là dùng ngôn ngữ tự nhiên và giả mã. Sử dụng
ngôn ngữ tự nhiên rất dễ gây nhầm lẫn trong lúc thực hiện. Giả mã là sự pha trộn ngôn
ngữ tự nhiên với một ngôn ngữ lập trình nào đó. Trong tài liệu này, ta sẽ sử dụng giả
mã pha trộn với ngôn ngữ lập trình C. Dùng giả mã để mô tả thuật toán sẽ ngắn gọn,
súc tích hơn.
Sau khi đã mô tả xong thuật toán, ta phải chứng minh tính đúng đắn của thuật
toán. Ta phải chứng minh thuật toán sẽ cho kết quả đúng yêu cầu với mỗi đầu vào hợp
lệ trong khoảng thời gian hữu hạn. Một kĩ thuật chung để chứng minh tính đúng đắn lệ
của thuật toán là sử dụng phương pháp quy nạp toán học bởi vì bản thân vòng lặp của
thuật toán đã chứa trình tự các bước để chứng minh.
Sau khi kiểm tra tính đúng đắn, bước quan trọng nhất là đánh giá hiệu quả của
thuật toán. Có hai tiêu chí để đánh giá hiệu quả đó là tiêu chí về không gian và thời
gian, sẽ được trình bày chi tiết trong chương 2.

Lê Xuân Việt – Dương Hoàng Huyên


Chương 2. Phân tích tính hiệu quả của thuật toán 13

CHƯƠNG 2. PHÂN TÍCH HIỆU QUẢ THUẬT TOÁN

2.1. Các khái niệm liên quan


Phân tích thuật toán là tìm hiểu tính hiệu quả của thuật toán đối với hai vấn đề là
thời gian chạy và không gian bộ nhớ. Hiệu quả thời gian (còn gọi là độ phức tạp về
thời gian) là thời gian thực hiện của thuật toán từ khi nhận dữ liệu đầu vào cho đến khi
có dữ liệu đầu ra. Hiệu quả không gian (còn gọi là độ phức tạp về không gian) là số
lượng đơn vị bộ nhớ sử dụng cho thuật toán kể cả dữ liệu vào/ra.
Trong những ngày đầu của máy tính điện tử, cả thời gian và không gian đều được
quan tâm. Tuy nhiên, hiện nay công nghệ không ngừng đổi mới về tốc độ và không
gian bộ nhớ của máy tính, không gian lưu trữ không còn được quan tâm nhiều. Hơn
nữa, thực tế nghiên cứu đã cho ta thấy rằng hầu hết các vấn đề nếu đạt hiệu quả thời
gian sẽ tốt hơn so với hiệu quả không gian. Vì vậy, những nghiên cứu phân tích thuật
toán chủ yếu tập trung vào hiệu quả thời gian. Nhưng mô hình phân tích này vẫn có
thể áp dụng trên không gian.

2.1.1. Kích thước dữ liệu vào


Hầu như tất cả các thuật toán có thể chạy trên đầu vào rất lớn, ví dụ như sắp xếp
mảng với số phần tử rất lớn, nhân hai ma trận với kích thước lớn, v.v… do đó ta có thể
coi hiệu quả thuật toán như là một hàm có 1 tham số n chỉ kích thước đầu vào. Trong
nhiều trường hợp lựa chọn một tham số như vậy là khá đơn giản. Ví dụ n là số phần tử
của bài toán sắp xếp, tìm kiếm, … trong một danh sách. Hoặc trong bài toán tính giá
trị của đa thức thì n có thể bậc của đa thức đó.
Việc lựa chọn kích thước đầu vào có thể ảnh hưởng đến số phép toán của thuật
toán đưa ra. Ví dụ, xác định kích thước đầu vào cho một thuật toán kiểm tra lỗi chính
tả. Nếu thuật toán kiểm tra dựa vào đặc điểm của từng kí tự, thì kích thước đầu vào
chính là số kí tự, nếu dựa vào xử lý các từ thì kích thước đầu vào chính là số từ.
Một số trường hợp đặc biệt chẳng hạn như xác định kích thước đầu vào thuật
toán kiểm tra một số nguyên dương n có phải là nguyên tố hay không? Ở đây, đầu vào
chính là số n cần kiểm tra. Trong tình huống này, kích thước đầu vào chính là số bit
Lê Xuân Việt – Dương Hoàng Huyên
14 Phân tích và thiết kế thuật toán

biểu diễn nhị phân của n.

2.1.2. Đơn vị đo thời gian chạy


Có thể sử dụng một số đơn vị đo thời gian như giây, hoặc phần nghìn giây, để đo
thời gian chạy của một chương trình thực hiện theo thuật toán. Tuy nhiên, có những
hạn chế rõ ràng cho cách tiếp cận như vậy. Ví dụ như phụ thuộc vào tốc độ của máy
tính, chất lượng của chương trình thực hiện, các trình biên dịch sử dụng trong việc tạo
ra các mã máy, và những khó khăn của thời gian chạy thực tế của chương trình. Do đó
ta cần có độ đo khác để đo hiệu quả của thuật toán mà không phụ thuộc vào các yếu tố
bên ngoài.
Một trong các phương pháp có thể là đếm số lần thực hiện của các phép toán cơ
bản trong thuật toán. Phép toán cơ bản là những phép toán góp phần lớn trong tổng
thời gian chạy của thuật toán. Ví dụ, các thuật toán sắp xếp dựa vào cách so sánh các
khóa của một danh sách được sắp xếp, phép toán cơ bản ở đây là phép so sánh. Một ví
dụ khác, các thuật toán liên quan đến một số hoặc tất cả bốn phép toán số học là: cộng,
trừ, nhân, chia thì phép toán tốn thời gian nhất là phép chia, tiếp theo là nhân và sau đó
cộng và trừ.
Cho cop là thời gian thực thi của một phép toán cơ bản của thuật toán trên máy
tính cụ thể, và cho C(n) là số phép toán cần để thực thi cho thuật toán. Khi đó, chúng
ta có thể ước lượng thời gian chạy t(n) của một chương trình thực hiện thuật toán này
trên máy tính bởi công thức t(n) copC(n).
Chú ý, giá trị t(n) không cho biết chính xác thời gian chạy của thuật toán trên
máy tính, tuy nhiên giá trị này có thể cho ta biết thuật toán nào nhanh hơn hoặc có thể
trả lời các câu hỏi ví dụ như: “khi tăng kích dữ liệu lên gấp đôi thì thời gian thực hiện
tăng lên bao nhiêu lần?”, v.v…

2.1.3. Trường hợp xấu nhất, tốt nhất và trung bình


Có nhiều thuật toán mà thời gian chạy không chỉ phụ thuộc vào kích thước đầu
vào mà còn vào phụ thuộc vào đặc điểm dữ liệu đầu vào. Ví dụ, tìm 1 phần tử có trong
một danh sách cho trước hay không? Dưới đây là giả mã của thuật toán.
ALGORITHM SequentialSearch(A[0..n - 1], K)
i ← 0
while (i < n) and (A[i] ≠ K) do
i ← i +1
if (i < n) then
return i
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 2. Phân tích tính hiệu quả của thuật toán 15

else
return -1
Rõ ràng, thời gian chạy của thuật toán này có thể khác nhau khi cho các danh
sách cùng một kích thước n. Trong trường hợp xấu nhất, khi không tìm thấy phần tử
thì số lượng phép so sánh đạt giá trị lớn nhất Cworst (n) = n.
Trường hợp xấu nhất của một thuật toán là thời gian chạy của thuật toán dài nhất
khi các dữ liệu đầu vào có cùng kích thước n. Cách để xác định trường hợp xấu nhất
của một thuật toán thường là xét những dữ liệu đầu vào mang lại giá trị lớn nhất cho
C(n). Rõ ràng, phân tích trường hợp xấu nhất cung cấp thông tin rất quan trọng là với
kích thước n bất kì, thời gian chạy sẽ không vượt quá Cworst (n).
Ngược lại, trường hợp tốt nhất của thuật toán là thời gian chạy của thuật toán
ngắn nhất khi các dữ liệu đầu vào có cùng kích thước n. Ví dụ thuật toán tìm kiếm ở
trên, nếu giá trị cần tìm ở đầu danh sách thì số phép toán so sánh cần thực hiện là 1.
Hiệu quả của thuật toán trong trường hợp trung bình là thời gian chạy trung bình
của thuật toán trên tất cả các dữ liệu đầu vào có kích thước n. Để phân tích hiệu quả
trường hợp trung bình của thuật toán, chúng ta phải chấp nhận một số giả thiết về kích
thước dữ liệu đầu vào n.
Xét thuật toán tìm kiếm tuần tự ở trên. Ta giả thiết: (a) xác suất tìm kiếm thành
công là p (0 ≤ p ≤ 1) và (b) các xác suất tìm thấy tại vị trí thứ i của danh sách là như
nhau đối với mọi i. Theo các giả thiết, ta có thể tính số phép toán so sánh trung bình
như Cavg (n) như sau. Trong trường hợp tìm kiếm thành công, xác suất tìm thấy tại vị
trí thứ i của danh sách là p / n với i phép so sánh. Trong trường hợp tìm kiếm không
thành công, số lượng so sánh sẽ là n với xác suất là (1  p). Do đó:

Cavg(n) = [1. p + 2. p + ⋯ + n. p] + n(1 − e) = p [1 + 2 + ⋯ + n] + n(1 − e)


n n n n

= p [n(n+1)] + n(1 − e) = p(n+1) + n(1 − e).


n 2 2

Nếu p = 1 (tìm kiếm thành công), số phép toán so sánh trung bình là (n + 1) / 2.
Nếu p = 0 (tìm kiếm không thành công), số phép toán so sánh trung bình là n.

2.1.4. Độ phức tạp thuật toán và kí hiệu O lớn


Độ phức tạp của thuật toán là hàm số t(n) (với n là kích thước dữ liệu đầu vào)
cho biết số phép toán cơ bản của thuật toán cần phải thực hiện khi nhận dữ liệu đầu
vào và cho kết quả đầu ra. Việc tính chính xác hàm t(n) trong rất nhiều trường hợp là
khó và không cần thiết. Ta sẽ quan tâm đến tốc độ tăng của hàm t(n) như thế nào khi n
Lê Xuân Việt – Dương Hoàng Huyên
16 Phân tích và thiết kế thuật toán

tăng bằng cách sử dụng kí hiệu O (đọc là Ô lớn) như sau.

Một hàm t(n) được gọi là thuộc lớp hàm O(g(n)), kí hiệu t(n)  O(g(n)), nếu t(n)
bị chặn trên bởi hằng số nhân với g(n) tức là tồn tại số c và n0 sao cho: t(n) ≤ c.g(n) 
n ≥ n0. Hàm t(n) được thể hiện qua Hình 2-1.

Hình 2-1. Hàm số t(n) thuộc lớp hàm O(g(n)).

Ví dụ, chứng minh hàm t(n) = 100n + 5  O(n2). Ta có 100n + 5 < 100n + n (n
≥ 5) = 101n ≤ 101n2, số c và n0 tương ứng trong trường hợp này là 101 và 5.
Một cách khác để kiểm tra hàm t(n) có thuộc lớp hàm O(g(n)) đó là ta sử dụng
giới hạn hàm số lim n→œ t(n)
= c, nếu c là một hằng số thì t(n) O(g(n)).
g(n)

Ví dụ, chứng minh hàm t(n) = 1 n(n − 1) ∈ 0( n2 ).


2
1
n(n–1)
1 n2–n 1 1 1
Ta có: limn→œ 2
n2
= limn→œ n2
= limn→œ (1 − )= , suy ra hàm t(n)
2 2 n 2
= 1 n(n − 1) ∈ 0( n2 ).
2

Kí hiệu O có tính chất quan trọng sau: t1(n)  O(g1(n)) và t2(n)  O(g2(n)), khi
đó t1(n) + t2(n)  O(max{g1(n), g2(n)}). Tính chất này cho ta biết tình huống sau: nếu
thuật toán thực hiện hai phần liên tiếp nhau, độ phức tạp của thuật toán chính là độ
phức tạp của phần có bậc cao hơn. Ví dụ, để kiểm tra một mảng có các phần tử bằng
nhau hay không? Ta có thể thiết kế thuật toán thành hai phần: phần thứ nhất sắp xếp
mảng tăng dần, phần thứ hai kiểm tra các phần tử kề nhau có bằng nhau không? Độ
phức tạp của phần sắp xếp mảng là O(n2) và độ phức tạp của phần kiểm tra phần tử
liền kề có bằng nhau hay không là O(n), vậy độ phức tạp của thuật toán trên là
O(max{n2, n}) = O(n2).

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 2. Phân tích tính hiệu quả của thuật toán 17

Hình 2-2. Một số lớp hàm thông dụng trong phân tích thuật toán.

2.2. Phân tích thuật toán không đệ quy


2.2.1. Các bước thực hiện
1. Xác định tham số chỉ kích thước dữ liệu đầu vào.
2. Xác định phép toán cơ bản của thuật toán.
3. Kiểm tra có hay không? Số phép toán cơ bản thực hiện chỉ phụ thuộc vào kích
thước dữ liệu đầu vào? Nếu có phụ thuộc vào những yếu khác ta phải xét thêm các
trường hợp xấu nhất, trung bình.
4. Thiết lập biểu thức tính tổng số lần phép toán cơ bản phải thực hiện.
5. Ước lượng mức độ tăng của biểu thức tính tổng này, bằng cách kiểm tra xem
biểu thức này thuộc lớp hàm nào ở Hình 2-2.
Một số công thức hỗ trợ tính tổng để phân tích thuật toán:
1. ui=S cai = c ∑ui=Sai
∑ u
2. i=S (ai ± bi) = ∑u i=S ai ± ∑ui=Sbi
∑ u
3. i=S 1 = u − l + 1

4. ni=1 i = 1 + 2 + ⋯ + n = n(n+1)
2

5. ni=1 i2 = 12 + 22 + ⋯ + n2 = n(n+1)(2n+1)
6
∑ n+1–1
6. ∑n a = a + a + ⋯+ a =
i 0 1 n a
a ≠ 1; ∑n 2i = 2n+1 − 1
i=0 a–1 i=0

7. ni=0 i2i = 1 × 20 + 2 × 21 + ⋯+ n × an = (n − 1) × 2n+1 + 2



8. ∑n 1 = 1 + 1 + ⋯ + 1 ≈ ln n + y, với y ≈ 0.5772
i=1 i 2 n

Lê Xuân Việt – Dương Hoàng Huyên


18 Phân tích và thiết kế thuật toán

9. ni=1 log i ≈ n log n


∑ n k
10. ∑ i = 1k + 2k + ⋯+ nk ≈ 1
nk+1
i=1 k+1

2.2.2. Các ví dụ minh họa


Ví dụ 2-1: Phân tích thuật toán tìm phần tử lớn nhất trong danh sách n phần tử.
ALGORITHM MaxElement(A[0..n − 1])
//Đầu vào: mảng A[0..n − 1] n số nguyên
//Đầu ra: Giá trị lớn nhất trong mảng
maxval ← A[0]
for i ← 1 to n − 1 do
if (A[i] > maxval) then
maxval ← A[i]
return maxval
Kích thước dữ liệu đầu vào là n (số phần tử của mảng). Phép toán cơ bản là phép
so sánh và được hiện chủ yếu trong vòng lặp for. Chú ý, trong vòng lặp for có phép
gán, tuy nhiên phép toán này không thực hiện thường xuyên nên ta chọn phép so sánh
là phép toán cơ bản. Ta thấy rằng số phép so sánh trong thuật toán này chỉ phụ thuộc
vào n nên không cần xét trường hợp xấu nhất, trung bình của thuật toán này. Đặt t(n)
là số phép toán cần phải thực hiện của thuật toán trên. Ta thấy thuật toán chỉ thực hiện
1 phép so sánh sau mỗi lần lặp và vòng lặp thực hiện từ 1 đến n  1, do đó t(n) =
i=1 1 = n  1  O(n). Độ phức tạp của thuật toán trên là O(n).
∑n–1

Ví dụ 2-2: Phân tích thuật toán kiểm tra có hay không tất cả các phần tử trong
một danh sách là riêng biệt, tức là không có phần tử nào xuất hiện hơn một lần trong
danh sách đó.
ALGORITHM UniqueElements(A[0..n − 1])
//Đầu vào: Mảng A[0..n − 1] n phần tử
//Đầu ra: Trả về “true” nếu tất cả các phần tử là phân biệt, ngược
lại “false”
for i ← 0 to n − 2 do
for j ← i + 1 to n − 1 do
if (A[i] = A[j]) then
return false
return true
Kích thước dữ liệu đầu vào là n, số phần tử của danh sách. Bên trong hai vòng
lặp này chỉ có 1 phép toán so sánh và ta coi đây là phép toán cơ bản của thuật toán.
Chú ý rằng số phép toán so sánh cần thực hiện không chỉ phụ thuộc vào n mà còn phụ
thuộc vào việc có hay không hai phần tử giống nhau trong mảng. Ta đánh giá thuật
toán trong trường hợp xấu nhất.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 2. Phân tích tính hiệu quả của thuật toán 19

Trường hợp xấu nhất với thuật toán này xảy ra khi không có phần tử nào trong
mảng xuất hiện hơn một lần. Khi đó phép so sánh phải thực hiện một lần sau mỗi lần
lặp. Đặt t(n) là số phép toán so sánh cần thực hiện. Ta có:
t(n) = ∑n–2 ∑n–1 1 = ∑n–2[(n − 1) − (i + 1) + 1)] = ∑n–2(n − i − 1) =
i=0 j=i +1 i=0 i=0
(n–1)(n–2) n2–3n+2
= ∑n–2(n − 1) − ∑n–2 i = (n − 1) ∑n–2 1 − = (n − 1)2 − =
i=0 i=0 i=0 2 2
2n2–4n+2–n2+3n–2 n2–n 2
= = ≤ 1 n2 ∈ 0( n 2).
2 2

Vì hàm t(n)  O(n2) nên ta kết luận độ phức tạp của thuật toán trên là O(n2).
Ví dụ 2-3: Phân tích thuật toán nhân hai ma trận vuông A và B cấp n × n.
Theo định nghĩa tích vô hướng của ma trận như sau: An × n × Bn × n = Cn × n với
phần tử Ci j được tính như sau: Cij = ∑n–1
k=0Aik × Bkj.

ALGORITHM MatrixMultiplication(A[0..n − 1, 0..n − 1], B[0..n − 1,


0..n − 1])
//Đầu vào: hai ma trận A và B với cấp n × n
//Đầu ra: Ma trận C = A × B
for i ← 0 to n − 1 do
for j ← 0 to n − 1 do
C[i, j] ← 0
for k ← 0 to n − 1 do
C[i, j] ← C[i, j] + A[i, k] ∗ B[k, j]
return C
Kích thước dữ liệu đầu vào của thuật toán là n. Có hai phép toán cơ bản là phép
nhân và phép cộng trong vòng lặp. Tuy nhiên hai phép toán này có số lượng như nhau
nên ta chỉ cần đếm số lượng của một phép toán (chẳng hạn phép nhân).
Đặt t(n) là số phép nhân của thuật toán. Ta thấy rằng số phép nhân của thuật toán
chỉ phụ thuộc vào kích thước dữ liệu đầu vào, do đó ta không cần xét trường hợp xấu
nhất của thuật toán. Tại mỗi vòng lặp chỉ thực hiện một phép nhân nên có biểu thức
tính t(n) như sau:
t(n) = ∑n–1 ∑n–1 ∑n–1 1 = ∑n–1 ∑n–1 n = ∑n–1 n2 = n3 ∈ 0(n 3 ).
i=0 j=0 k=0 i=0 j=0 i=0

Độ phức tạp của thuật toán trên là O(n3).


Ví dụ 2-4: Phân tích thuật toán tìm độ dài chuỗi nhị phân biểu diễn số nguyên n.
ALGORITHM Binary(n)
//Đầu vào: Số nguyên dương n
//Đầu ra : Độ dài chuỗi nhị phân biểu diễn n
count ← 1

Lê Xuân Việt – Dương Hoàng Huyên


20 Phân tích và thiết kế thuật toán

while (n > 1) do
count ← count + 1
n ← n / 2
return count
Kích thước dữ liệu đầu vào chính là số n, phép toán cơ bản là phép chia. Ta thấy
mỗi lần lặp n giảm đi một nửa nên số phép chia cần thực hiện là:
t(n) = ⌊ log2 n⌋  O( log n). Vậy độ phức tạp của thuật toán trên là O(log n).

2.3. Phân tích thuật toán đệ quy


2.3.1. Các bước thực hiện
1. Xác định tham số chỉ kích thước dữ liệu đầu vào.
2. Xác định phép toán cơ bản của thuật toán.
3. Kiểm tra có hay không số phép toán cơ bản chỉ phụ thuộc vào kích thước dữ
liệu đầu vào, nếu số phép toán cơ bản còn phụ thuộc vào những yếu tố khác ta phải xét
trường hợp xấu nhất, trung bình.
4. Thiết lập công thức đệ quy để tính tổng số lượng phép toán cơ bản.
5. Giải công thức đệ quy, xác định công thức tính tổng thuộc lớp hàm nào.

2.3.2. Các ví dụ minh họa


Ví dụ 2-5: Thiết kế thuật toán đệ quy tính F(n) = n!, với n là số nguyên dương.
Do n! = n × (n – 1)! và 0! = 1 ta có thể tính n! bằng công thức đệ quy như sau:
F(n) = F(n – 1) × n.
ALGORITHM F(n)
//Đầu vào: Số nguyên dương n
//Đầu ra: Giá trị F(n) = n!
if (n = 0) then
return 1
else
return F(n − 1) ∗ n
Ta có kích thước dữ liệu đầu vào là số nguyên n, phép toán cơ bản trong thuật
toán này là phép nhân. Đặt t(n) là số phép toán cơ bản của thuật toán cần phải thực
hiện khi kích thước dữ liệu là n. Tại mỗi lần gọi hàm F(n) ta chỉ thực hiện 1 phép nhân,
nên t(n) = t(n – 1) + 1, và t(0) = 0. Lần lượt thế các giá trị t(n – 1) = t(n – 2) + 1, t(n –
2) = t(n – 3) + 1, … vào công thức ban đầu, ta có: t(n) = t(n – 1) + 1 = t(n – 2) + 2 = t(n
– 3) + 3 = … = t(1) + n – 1 = t(0) + n = n  O(n). Vậy độ phức tạp của thuật toán là

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 2. Phân tích tính hiệu quả của thuật toán 21

O(n).
Ví dụ 2-6: Bài toán chuyển tháp Hà Nội. Có n tháp kích thước khác nhau được
đặt chồng lên nhau sao cho tháp to ở dưới và tháp nhỏ ở trên. Cần chuyển n tháp từ vị
trí A đến vị trí B, mỗi lần chuyển 1 tháp, chỉ được đặt tháp nhỏ lên trên tháp lớn và
trong quá trình chuyển lấy vị trí C làm trung gian.
ALGORITHM ChuyểnTháp(n, A, B, C)
//Đầu vào: Số nguyên dương n
//Đầu ra: Các bước chuyển tháp
if (n = 1) then
Chuyển 1 tháp từ A  B
else
ChuyểnTháp (n - 1, A, C, B)
Chuyển 1 tháp từ A  B
ChuyểnTháp(n - 1, C, B, A)
Kích thước dữ liệu đầu vào n chính là số tháp cần chuyển, phép toán cơ bản của
thuật toán này là phép chuyển một tháp từ vị trí này đến vị trí khác. Đặt t(n) là số phép
di chuyển cần thực hiện của thuật toán với kích thước dữ liệu đầu vào là n. Ta có:

t(n) = t(n – 1) + 1 + t(n – 1) và t(1) = 1. Lần lượt thế các giá trị t(n  1), t(n  2),
…, t(2), t(1) vào công thức đệ quy trên, ta có:
t(n) = 2t(n – 1) + 1 = 2[2t(n – 2) + 1] + 1 = 22t(n – 2) + 2 + 1 =
t(n) = 22[2t(n – 3) + 1] + 2 + 1 = 23t(n – 3) + 22 + 2 + 1

t(n) = 2it(n – i) + 2i  1 + 2i  2 + … + 21 + 20 = 2it(n – i) + 2i – 1

t(n) = 2n  1t(n – (n – 1)) + 2n  1 – 1 = 2n  1t(1) + 2n  1 – 1 = 2n  1 + 2n  1 – 1 = 2n –


1  O(2n). Vậy độ phức tạp của thuật toán trên là O(2n)
Ví dụ 2-7: Thuật toán đếm số bit nhị phân biểu diễn số nguyên n bằng đệ quy.
ALGORITHM BinRec(n)
//Đầu vào: Số nguyên dương n
//Đầu ra: Số bit nhị phân biểu diễn n
if (n = 1) then
return 1
else
return BinRec(n / 2) + 1
Phép toán cơ bản của thuật toán trên là phép chia. Công thức đệ quy để đếm số
lượng phép toán cơ bản là: t(n) = t(n / 2) + 1 và t(1) = 0. Để đơn giản, ta giả sử n = 2k,
thế vào công thức trên ta có:

Lê Xuân Việt – Dương Hoàng Huyên


22 Phân tích và thiết kế thuật toán

t(2k) = t(2k  1) + 1 = [t(2k  2) + 1] + 1 = … = [t(2k  i) + 1] + i – 1 = t(2k  k) + 1 + k


– 1 = t(1) + k = k, vì n = 2k nên k = log2(n), vậy t(n)  O(log n).

2.4. Phân tích thuật toán bằng thực nghiệm


Trong thực tế, rất nhiều thuật toán rất khó để tính chính xác số phép toán cơ bản.
Do đó, để đánh giá được hiệu quả của những thuật toán này ta có thể dùng phương
pháp thực nghiệm sau:
1. Chọn đơn vị đo hiệu quả của thuật toán: ví dụ như dùng đơn vị thời gian bằng
cách sử dụng các hàm trong máy tính lấy thời gian bắt đầu thực hiện và thời gian kết
thúc thuật toán. Hoặc có thể dùng một biến đếm để tính tổng số phép toán cơ bản phải
thực hiện.
2. Chọn tập dữ liệu mẫu làm dữ liệu đầu vào, tùy theo thuật toán cụ thể ta chọn
mẫu dữ liệu đầu vào với kích thước và giá trị khác nhau.
3. Cài đặt thuật toán bằng ngôn ngữ lập trình bất kì.
4. Cho chạy chương trình trên tập mẫu dữ liệu đã chuẩn bị
5. Phân tích dữ liệu thu được bằng cách tạo bảng sau:
Mẫu dữ liệu thứ D1 D2 … Dn
Thời gian hoặc
T1 T2 … Tn
Số phép toán cơ bản
Dựa vào bảng này, ta có thể ước lượng một hàm xấp xỉ bảng đã cho bằng phương
pháp nội suy và từ hàm vừa nhận được có thể đánh giá độ phức tạp của thuật toán.

2.5. Bài tập


Bài 1. Cho các bài toán sau, chỉ ra: (i). kích thước dữ liệu đầu vào, (ii). phép toán cơ
bản, (iii). phép toán cơ bản có thay đổi hay không nếu thay đổi dữ liệu đầu vào nhưng
giữ nguyên kích thước.
1. Tính tổng n số nguyên.
2. Tính n!.
3. Tìm số lớn nhất trong dãy số.
4. Thuật toán Euclid tìm ước số chung lớn nhất của a, b.
5. Nhân hai số nguyên có n chữ số.
Bài 2. Xét bài toán cộng hai ma trận cấp m × n. Phép toán cơ bản là gì? đánh giá độ
phức tạp của thuật toán trên.
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 2. Phân tích tính hiệu quả của thuật toán 23

Bài 3. Dãy số Fibonaci được định nghĩa đệ quy như sau: F(n) = F(n – 1) + F(n – 2)
với F(0) = 0, F(1) = 1. Cài đặt hai thuật toán đệ quy và không đệ quy để tính F(n) bằng
ngôn ngữ lập trình bất kì. Chạy chương trình với giá trị n = 50 và so sánh thời gian
thực hiện của hai thuật toán.
Bài 4. Cho các hàm sau, xác định các hàm này thuộc lớp hàm nào?
a. (n2 + 1)10

b. √10n2 + 7n + 3
c. 2n.log(n + 2)2 + (n + 2)2log(n / 2)
d. 2n + 1 + 3n − 1

e. ⌊ log2 n⌋
k1
Bài 5. Chứng minh rằng, với mỗi đa thức bậc k bất kì, p(n) = aknk + ak 1n + … + a0,
với ak > 0 thuộc lớp O(nk).
Bài 6. Tính các tổng sau đây:
a. 1 + 3 + 5 + … + 99
b. 2 + 4 + 6 + … + 100

c. ∑n+1
i=3 1

d. ∑n+1
i=3 i

e. ∑n–1
i=0 i(i + 1)

f. ∑in 3i+1
g. ni=1 ∑nj=1 i × j
∑ 1
h. ∑n+1
i=3 i(i+1)

Bài 7. Xét thuật toán sau:


ALGORITHM Mystery(n)
//Đầu vào: một số nguyên không âm n
S ← 0
for i ← 1 to n do
S ← S + i∗ i
return S
a. Thuật toán tính giá trị gì?
b. Phép toán cơ bản là gì?

Lê Xuân Việt – Dương Hoàng Huyên


24 Phân tích và thiết kế thuật toán

c. Phép toán cơ bản thực hiện bao nhiêu lần?


d. Độ phức tạp của thuật toán trên là gì?
e. Có thuật toán khác cũng tính giá trị S như trên mà nhanh hơn hơn hay không?
Bài 8. Xét thuật toán sau:
ALGORITHM Secret(A[0..n − 1])
//Đầu vào: một mảng A[0..n − 1] có n số thực
minval ← A[0]
maxval ← A[0]
for i ← 1 to n − 1 do
if (A[i] < minval) then
minval ← A[i]
if (A[i] > maxval) then
maxval ← A[i]
return maxval − minval
Trả lời tương tự như Bài 7 từ câu a-e.
Bài 9. Xét thuật toán sau:
ALGORITHM Enigma(A[0..n − 1, 0..n − 1])
//Đầu vào: một ma trận A[0..n − 1, 0..n − 1] các số thực
for i ← 0 to n − 2 do
for j ← i + 1 to n − 1 do
if (A[i, j]  A[j, i]) then
return false
return true
Trả lời tương tự như Bài 7 từ câu a-e.
Bài 10. Giải các công thức đệ quy sau:

a. x(n) = x(n  1) + 5 với n > 1, x(1) = 0.

b. x(n) = 3x(n  1) với n > 1, x(1) = 4.


c. x(n) = x(n − 1) + n với n > 0, x(0) = 0
d. x(n) = x(n / 2) + n với n > 1, x(1) = 1, n = 2k.
e. x(n) = x(n / 3) + 1 với n > 1, x(1) = 1, n = 3k.
Bài 11. Xét thuật toán đệ quy tính tổng lập phương n số nguyên đầu tiên S(n) = 13 + 23
+ . . . + n3 như sau:
ALGORITHM S(n)
//Đầu vào: Số nguyên dương n
//Đầu ra: Tổng lập phương n số nguyên đầu tiên
if (n = 1) then

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 2. Phân tích tính hiệu quả của thuật toán 25

return 1
else
return S(n − 1) + n * n * n
a. Đánh giá độ phức tạp của thuật toán đệ quy trên.
b. Hãy so sánh tính hiệu quả thuật toán này với thuật toán không đệ quy tính S(n).
Bài 12. Xét thuật toán đệ quy sau:
ALGORITHM Q(n)
//Đầu vào: Số nguyên n
if (n = 1) then
return 1
else
return Q(n − 1) + 2 * n – 1
a. Thiết lập hàm đệ quy và giải nó để xác định thuật toán trên tính giá trị gì.
b. Thiết lập công thức đệ quy tính số phép nhân và giải nó.
c. Thiết lập công thức đệ quy tính số phép cộng/trừ và giải nó.

Bài 13. Thiết kế thuật toán đệ quy tính giá trị 2n dựa vào công thức: 2n = 2n  1 + 2n  1.
a. Thiết lập công thức đệ quy tính số phép cộng của thuật toán và giải nó.
b. Đây có phải là thuật toán hiệu quả để giải quyết bài toán này?
Bài 14. Xét thuật toán đệ quy sau:
ALGORITHM Riddle(A[0..n − 1])
//Đầu vào: một mảng A[0..n − 1] các số thực
if (n = 1) then
return A[0]
else
temp ← Riddle(A[0..n − 2])
if (temp ≤ A[n − 1]) then
return temp
else
return A[n − 1]
a. Thuật toán trên tính giá trị gì?
b. Thiết lập công thức đệ quy để tính số phép toán cơ bản và giải nó.
Bài 15. Có n bánh cần nướng trên một vỉ nướng nhỏ chỉ có thể chứa một lúc 2 bánh.
Mỗi bánh phải nướng hai mặt và mỗi mặt nướng 1 phút bất kể có 1 hay 2 bánh trên vỉ.
Yêu cầu nướng n bánh với thời gian ngắn nhất. Xét thuật toán sau: nếu n < 2, nướng 1
hoặc 2 bánh cùng lúc với nhau. Nếu n > 2, nướng 2 bánh bất kì cùng lúc trên mỗi mặt
sau đó áp dụng đệ quy cho n – 2 bánh còn lại.

Lê Xuân Việt – Dương Hoàng Huyên


26 Phân tích và thiết kế thuật toán

a. Thiết lập công thức đệ quy để tính số số phút cần thiết để nướng n bánh.
b. Giải thích tại sao thuật toán trên nướng n bánh với thời gian không phải tối
thiểu.
c. Đưa ra một thuật toán nướng n bánh với thời gian ít nhất.
Bài 16. Xét thuật toán sắp xếp các phần tử trong mảng với 1 biến count dùng để đếm
số lượng phép toán so sánh giữa các phần tử trong mảng cần thực hiện.
ALGORITHM SortAnalysis(A[0..n − 1])
//Đầu vào: một mảng A[0..n − 1] n phần tử
//Đầu ra: Tổng số phép so sánh đã thực hiện
count ← 0
for i ← 1 to n − 1 do
v ← A[i]
j ← i −1
while (j ≥ 0) and (A[j] > v) do
count ← count + 1
A[j + 1] ← A[j]
j ← j − 1
A[j + 1] ← v
return count
a. Biến đếm count đã đặt đúng vị trí chưa? nếu đúng vị trí hãy chứng minh, nếu
sai hãy đặt lại vị trí đúng.
b. Cài đặt thuật toán trên và cho thực hiện với 20 mẫu dữ liệu ngẫu nhiên với
kích thước dữ liệu lần lượt là: 1.000, 2.000, …, 19.000, 20.000.
c. Phân tích dữ liệu nhận được để đưa ra nhận định về độ phức tạp của thuật toán.
d. Ước lượng số phép toán so sánh cần phải thực hiện khi kích thước dữ liệu đầu
vào là 25.000.
e. Thay biến count trong thuật toán trên bằng cách lấy thời gian bắt đầu và kết
thúc thuật toán của thuật toán.
Bài 17. Giả sử có 1 thuật toán sau khi thực hiện với kích thước dữ liệu và số phép toán
cơ bản như bảng sau:
Size 1000 2000 3000 4000 5000 6000 7000 8000 9000 10000
Count 11966 24303 39992 53010 67272 78697 91274 113063 129799 140538
Hãy đánh giá độ phức tạp của thuật toán trên.
Bài 18. Cài đặt 3 thuật toán tìm ước số chung lớn nhất với các biến đếm phép chia đã
trình bày trong chương 1. Ước lượng độ phức tạp của từng thuật toán.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 3. Brute-Force và duyệt toàn bộ 27

CHƯƠNG 3. BRUTE-FORCE VÀ DUYỆT TOÀN BỘ

3.1. Giới thiệu


Brute-Force là một phương pháp đơn giản để giải quyết bài toán, thường là trực
tiếp dựa trên phát biểu của bài toán, hoặc dựa vào các định nghĩa của các khái niệm
liên quan.
Ví dụ bài toán tính an, theo định nghĩa an = a × a × … × a (n lần). Rõ ràng dựa
vào định nghĩa an, ta có thể thiết kế được một thuật toán để tính giá trị của an. Những
kĩ thuật thiết kế thuật toán dựa trên định nghĩa của bài toán đó được gọi là kĩ thuật
Brute-Force. Một số tài liệu gọi phương pháp này là vét cạn.

3.2. Sắp xếp chọn và sắp xếp nổi bọt


3.2.1. Sắp xếp chọn
Cho trước một danh sách n phần tử, sắp xếp danh sách này theo trật tự không
giảm. Để thiết kế thuật toán sắp xếp này theo phương pháp Brute-Force, ta xét một
tính chất quan trọng sau đây: một dãy đã được sắp xếp tăng thì phần tử nhỏ nhất của
dãy sẽ đặt ở vị trí đầu tiên, tương tự phần tử nhỏ thứ hai sẽ được đặt ở vị trí thứ hai, …,
phần tử lớn nhất sẽ được đặt ở vị trí cuối cùng. Dựa vào tính chất này ta thiết kế thuật
toán sắp xếp chọn như sau: Tìm vị trí phần tử nhỏ nhất trong n phần tử của dãy, hoán
đổi phần tử ở vị trí này với phần tử ở vị trí đầu tiên của dãy. Sau đó ta coi dãy chỉ còn
n – 1 phần tử từ vị trí thứ hai trở đi, tiếp tục tìm vị trí phần tử nhỏ nhất trong n – 1
phần tử, hoán đổi phần tử này với phần tử đầu tiên, tiếp tục như thế cho đến phần tử
thứ n – 1. Thuật toán được biểu diễn bằng giả mã như sau:
ALGORITHM SelectionSort(A[0..n − 1])
//Đầu vào: mảng A[0..n − 1] có trật tự bất kì.
//Đầu ra: mảng A[0..n − 1] đã được sắp tăng dần.
for i ← 0 to n − 2 do
min ← i
for j ← i + 1 to n − 1 do
if (A[j] < A[min]) then
min ← j
swap(A[i], A[min])
Lê Xuân Việt – Dương Hoàng Huyên
28 Phân tích và thiết kế thuật toán

return A
Chú ý: swap(a, b) là thao tác đổi giá trị hai biến a, b.
Ví dụ dãy: 89 45 68 90 29 34 17. Các bước sắp xếp như sau:
Dãy ban đầu: 89 45 68 90 29 34 17
i = 0: 17| 45 68 90 29 34 89
i = 1: 17 29| 68 90 45 34 89
i = 2: 17 29 34| 90 45 68 89
i = 3: 17 29 34 45| 90 68 89
i = 4: 17 29 34 45 68| 90 89
i = 5: 17 29 34 45 68 89| 90

Kích thước dữ liệu đầu vào của thuật toán này là số n (số phần tử của dãy). Phép
toán cơ bản là phép so sánh. Ta thấy phép so sánh chỉ phụ thuộc vào kích thước dữ
liệu đầu vào nên ta không cần xét các trường hợp xấu nhất hoặc trường hợp trung bình.
Đặt t(n) là số phép so sánh của thuật toán với kích thước dữ liệu đầu vào là n. Ta có:
t(n) = ∑n–2 ∑n–1 1 = ∑n–2(n − 1 − i) = (n − 1) 2 − ∑n–2 i =
i=0 j=i+1 i=0 i=0

(n–1)(n–2) n(n–1)
= (n − 1)2 − = ∈ 0( n2).
2 2

Vậy độ phức tạp của thuật toán là O(n2).

3.2.2. Sắp xếp nổi bọt


Một dãy được sắp xếp tăng cũng có một tính chất nữa đó là: hai phần tử kề nhau
thì phần tử trước phải nhỏ hơn phần tử sau. Dựa vào tính chất này ta thiết kế thuật toán
sắp xếp như sau: duyệt các phần tử trong danh sách, nếu phát hiện cặp phần tử liền kề
không đúng trật tự ta đổi hai phần tử đó cho nhau, tiếp tục thao tác trên ta sẽ đưa phần
tử lớn nhất đến cuối dãy. Lặp lại thao tác trên n – 1 lần ta sẽ có danh sách đúng trật tự.
Chi tiết thuật toán được viết bằng giả mã như sau:
ALGORITHM BubbleSort(A[0..n − 1])
//Đầu vào: một mảng A[0..n − 1]
//Đầu ra: Mảng A[0..n - 1] đã được sắp xếp
for i ← 0 to n − 2 do
for j ← 0 to n – 2 − i do
if (A[j + 1] < A[j]) then
swap(A[j], A[j + 1])
return A

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 3. Brute-Force và duyệt toàn bộ 29

Chú ý, mỗi khi vòng lặp for bên trong thực hiện xong thì thuật toán chuyển phần
tử lớn nhất về cuối dãy, do đó vòng lặp for bên ngoài chỉ thực hiện từ 0 đến n – 2.
Kích thước dữ liệu đầu vào là n (số phần tử), phép toán cơ bản là phép so sánh.
Số phép toán so sánh được tính như sau:
t(n) = ∑n–2 ∑n–2–i 1 = ∑n–2(n − 2 − i − 0 + 1 ) = ∑n–2(n − i − 1) =
i=0 j=0 i=0 i=0
n(n–1)
∈ 0(n2).
2

Ví dụ dãy: 89 45 68 90 29 34 17 các bước sắp xếp như sau:

89 45 68 90 29 34 17
45 89 68 90 29 34 17
45 68 89 90 29 34 17
45 68 89 29 90 34 17
45 68 89 29 34 90 17
45 68 89 29 34 17 | 90

45 68 89 29 34 17 | 90
45 68 89 89 34 17 | 90
45 68 29 34 89 17 | 90
45 68 29 34 17 | 89 90

3.3. Tìm kiếm tuần tự và khớp chuỗi


3.3.1. Tìm kiếm tuần tự
Cho một dãy số và một số K, cần biết số K có trong dãy hay không, hay nói cách
khác cần tìm xem có K trong dãy hay không? Thuật toán tìm kiếm tuần tự làm việc
như sau: so sánh số K với phần tử đầu tiên, nếu bằng nhau thì trả vị trí tìm thấy, nếu
không bằng nhau tiếp tục so sánh với phần tử thứ hai tương tự cho đến hết danh sách.
Để đơn giản trong lúc thiết kế, ta bổ sung số K vào cuối danh sách và mô tả chi tiết
thuật toán như sau:
ALGORITHM SequentialSearch2(A[0..n], K)
//Đầu vào: Mảng A n phần tử và số K
//Đầu ra: Chỉ số phần tử đầu tiên tìm thấy, nếu không tìm thấy trả
về giá trị -1
A[n] ← K
i ← 0
while (A[i] ≠ K) do
i ← i +1
if (i < n) then
return i
else
Lê Xuân Việt – Dương Hoàng Huyên
30 Phân tích và thiết kế thuật toán

return −1
Ta dễ dàng đánh giá thuật toán này có độ phức tạp là O(n).

3.3.2. Thuật toán khớp chuỗi


Cho chuỗi n ký tự được gọi là text và chuỗi m kí tự (m ≤ n) gọi là pattern. Tìm
chuỗi con của text khớp với chuỗi pattern. Cụ thể hơn, tìm vị trí i ký tự bên trái nhất
của chuỗi con đầu tiên trong text sao cho: ti = p0, …, ti + j = pj, …, ti + m  1 = pm – 1

t0 … ti … ti + j … ti + m – 1 … tn  1 text T

p0 … pj … pm  1 pattern P
Thuật toán như sau: gióng hàng pattern với m kí tự đầu tiên của text, nếu khớp
thì kết luận tìm thấy và dừng thuật toán, nếu có một cặp kí tự nào đó không khớp thì
đẩy pattern sang phải một kí tự và tiếp tục gióng hàng. Tương tự cho đến khi phần còn
lại trong text ngắn hơn pattern. Thuật toán chi tiết như sau:
ALGORITHM BruteForceStringMatch(T [0..n − 1], P[0..m − 1])
//Đầu vào: Mảng T[0..n − 1] n kí tự biểu diễn text và mảng P[0..m −
1] m kí tự biểu diễn pattern
//Đầu ra: Chỉ số kí tự đầu tiên trong text mà pattern khớp với chuỗi
con trong đó.
for i ← 0 to n – m do
j ← 0
while (j < m) and (P[j] = T[i + j]) do
j ← j +1
if (j = m) then
return i
return −1
Xét ví dụ cụ thể sau:

Hình 3-1. Minh họa thuật toán khớp chuỗi.


Để đánh giá độ phức tạp của thuật toán khớp chuỗi, ta thấy kích thước dữ liệu
đầu vào là n × m (độ dài chuỗi text và pattern), phép toán cơ bản là phép so sánh các kí

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 3. Brute-Force và duyệt toàn bộ 31

tự, vì số phép so sánh này không những phụ thuộc m, n mà còn phụ thuộc vào dữ liệu
cụ thể do đó ta sẽ đánh giá thuật toán này trong trường hợp xấu nhất tức là chuỗi
pattern không có trong chuỗi text. Đặt t(n) là số phép so sánh cần thực hiện, ta có:
t(n) = ∑n–N–1 ∑N–1 1 = ∑n–N N = (n − N)( N) ≤ n. N ∈ O(n. N).
i=0 j=0 i=0

3.4. Bài toán cặp điểm gần nhất và bao lồi


3.4.1. Cặp điểm gần nhau nhất

Trong mặt phẳng cho n điểm P0, P2, …, Pn  1, tìm hai điểm trong n điểm đã cho
có khoảng cách nhỏ nhất. Công thức để tính khoảng cách của hai điểm Pi(xi, yi) và
2 2
P (x , y ) như sau: d(P , P ) = J(x − x ) − (y −y) .
j j j i j i j i j

Ý tưởng thuật toán như sau: tính khoảng cách tất cả các cặp điểm có thể trong n
điểm đã cho và tìm cặp điểm có khoảng cách nhỏ nhất. Để tránh trường hợp phải tính
lại khoảng cách của 1 cặp điểm nào đó, ví dụ d(Pi, Pj) và d(Pj, Pi) ta chỉ tính khoảng
cách những cặp điểm có tính chất i < j. Chi tiết thuật toán như sau:
ALGORITHM BruteForceClosestPair(P)
//Đầu vào: danh sách P có n ≥ 2 điểm p0(x0, y0), . . ., pn - 1(xn - 1, yn
- 1)
//Đầu ra: Khoảng cách nhỏ nhất giữa hai điểm.
d ← ∞
for i ← 0 to n - 2 do
for j ← i + 1 to n - 1 do
d ← Nin = {d, ƒ(xi − xj ) 2 + (yi − yj ) 2 }
return d
Kích thước dữ liệu đầu vào của thuật toán này là n (số điểm trên mặt phẳng),
phép toán cơ bản là căn bậc hai. Tổng số phép toán cơ bản t(n) được tính như sau:
t(n) = ∑n–1 ∑n 1 = ∑n–1(n − i) = n(n–1) ∈ 0( n 2).
i=1 j=i+1 i=1 2

3.4.2. Bao lồi nhỏ nhất


Trong mặt phẳng cho n điểm bất kì, tìm một đa giác lồi có diện tích nhỏ nhất
chứa n điểm đã cho. Để thiết kế thuật toán giải quyết bài trên, ta xét một vài tính chất
quan trọng như sau:
1. Khi kéo dài một cạnh bất kì của đa giác lồi, tất cả các đỉnh còn lại của đa giác
đều nằm về một phía của cạnh đó;
2. Các đỉnh của đa giác lồi cần tìm phải là các điểm trong n điểm đã cho.

Lê Xuân Việt – Dương Hoàng Huyên


32 Phân tích và thiết kế thuật toán

Thuật toán tìm đa giác lồi nhỏ nhất như sau: duyệt từng cặp điểm bất kì giả sử là
A, B trong n điểm đã cho, kiểm tra nếu n – 2 điểm còn lại đều nằm về một phía của
đường thẳng AB thì AB là một cạnh của đa giác lồi cần tìm. Tương tự, sau khi duyệt
hết các cặp điểm sẽ tìm được tất cả các cạnh của đa giác cần tìm. Thuật toán mô tả chi
tiết như sau:
ALGORITHM ConvexHull(P)
//Đầu vào: Danh sách P có n điểm p0(x0, y0), …, pn - 1(xn - 1, yn - 1)
//Đầu ra: Danh sách Q tập cạnh của đa giác cần tìm.
for i ← 0 to n − 2 do
for j ← i + 1 to n - 1 do
for k ← 0 to n - 1 do
if (k≠i,k≠j và pk nằm 1 phía của đường thẳng pipj) then
Bổ sung cạnh pipj vào Q.
return Q
Để kiểm tra một điểm nằm phía nào của đường thẳng ta xét các tính chất toán
học sau: phương trình tổng quát đường thẳng qua hai điểm (x1, y1) và (x2, y2) có dạng
ax + by + c = 0 với a = y2 – y1, b = x1 – x2, c = x2y1 – x1y2. Đường thẳng qua (x1, y1) và
(x2, y2) chia mặt phẳng thành hai nửa, một nửa là tập hợp các điểm (x, y) sao cho ax +
by – c > 0 và một nữa còn lại là các điểm (x, y) sao cho ax + by – c < 0. Các điểm (x, y)
mà ax + by – c = 0 sẽ nằm trên đường thẳng. Độ phức tạp của thuật toán này là O(n3).

3.5. Duyệt toàn bộ


Duyệt toàn bộ là kĩ thuật Brute-Force liên quan đến các bài toán tối ưu tổ hợp. Kĩ
thuật này sinh ra tập hợp các phần tử, từ các phần tử thỏa mãn các ràng buộc ta chọn ra
phần tử mong muốn (phần tử tối ưu).

3.5.1. Bài toán Người đi du lịch


Cho n thành phố, yêu cầu tìm đường đi với chi phí ít nhất qua các thành phố, mỗi
thành phố chỉ đến duy nhất một lần sau đó trở về thành phố đã xuất phát. Ta có thể
chuyển bài toán này về một dạng bài toán trong đồ thị bằng cách cho mỗi đỉnh của đồ
thị là tên thành phố, mỗi cạnh nối hai đỉnh của đồ thị là chi phí đi lại giữa hai thành
phố đó. Bài toán bây giờ là yêu cầu tìm trình chu trình Hamilton ngắn nhất của đồ thị.
Để giải quyết bài toán tìm chu trình Hamilton ngắn nhất bằng kĩ thuật duyệt toàn
bộ, ta thực hiện các bước như sau: sinh ra tất cả các hoán vị của n đỉnh, tính toán tổng
khoảng cách giữa các đỉnh và tìm khoảng cách nhỏ nhất trong các hoán vị đó. Ví dụ
minh họa ở Hình 3-2. Thuật toán này sẽ sinh ra hoán vị của n thành phố, do đó độ
phức tạp của thuật toán này là O(n!).

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 3. Brute-Force và duyệt toàn bộ 33

Hình 3-2. Minh họa bài toán Người đi du lịch với 4 thành phố.

3.5.2. Bài toán Xếp ba lô


Cho n đồ vật có trọng lượng lần lượt là w1, w2, …, wn và giá trị tương ứng v1, v2,
…, vn và một ba lô với khả năng chứa là W. Tìm tập con có tổng giá trị lớn nhất của n
đồ vật đã cho để bỏ vừa vào ba lô.
Kĩ thuật duyệt toàn bộ để giải bài toán này như sau: sinh ra tất cả các tập con của
tập n đồ vật, tính tổng trọng lượng của tập con các đồ vật này, chọn tập con có tổng
trọng lượng của các đồ vật không vượt quá W từ đó chọn ra tập con có giá trị nhất.
Xét ví dụ như sau:
Đồ vật Trọng lượng Giá trị Ba lô
1 7 42
2 3 12
W = 10
3 4 40
4 5 25
Các tập con, tổng trọng lượng và tổng giá trị như sau:
Lê Xuân Việt – Dương Hoàng Huyên
34 Phân tích và thiết kế thuật toán

Tổng
Tập con Tổng giá trị
trọng lượng
 0 0
{1} 7 42
{2} 3 12
{3} 4 40
{4} 5 25
{1, 2} 10 54
{1, 3} 11 Không khả thi
{1, 4} 12 Không khả thi
{2, 3} 7 52
{2, 4} 8 37
{3, 4} 9 65 (lớn nhất)
{1, 2, 3} 14 Không khả thi
{1, 2, 4} 15 Không khả thi
{1, 3, 4} 16 Không khả thi
{2, 3, 4} 12 Không khả thi
{1, 2, 3, 4} 19 Không khả thi
Dựa vào bảng trên, ta thấy tập con {3, 4} cho tổng giá trị 65 là lớn nhất, và đó
chính là phương án tối ưu. Do phải sinh ra tất cả các tập con nên bài toán Xếp ba lô
giải theo kĩ thuật duyệt toàn bộ có độ phức tạp là O(2n).

3.5.3. Bài toán Phân công công việc


Có n người cần phải thực hiện n công việc, mỗi người một công việc. Giá trị
tương ứng với người thứ i được phân công thực hiện công việc thứ j là Ci j i, j = 1..n.
Bài toán đặt ra là tìm cách phân công sao cho tổng giá trị là nhỏ nhất. Giá trị ở đây có
thể hiểu là thời gian phải thực hiện, hoặc chi phí nhân công, v.v…Ví dụ có 4 người và
4 công việc với giá trị phân công cụ thể như sau:

Công việc 1 Công việc 2 Công việc 3 Công việc 4


Người 1 9 2 7 8
Người 2 6 4 3 7
Người 3 5 8 1 8
Người 4 7 6 9 4
Để giải quyết bài toán này bằng kĩ thuật duyệt toàn bộ, ta giữ nguyên thứ tự công
việc là 1, 2, …, n. Tiếp theo, sinh hoán vị n người sau đó gán công việc cho người
tương ứng. Cụ thể trong ví dụ trên, ta giữ nguyên thứ tự công việc là 1, 2, 3, 4, tiếp
theo ta sinh hoán vị 4 người, chẳng hạn 2, 3, 1, 4, cuối cùng ta phân công người 2 làm
việc 1, người 3 làm việc 2, người 1 làm việc 3 và người 4 làm việc 4. Tất cả các
phương án phân công và giá trị của ví dụ trên như bảng sau:

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 3. Brute-Force và duyệt toàn bộ 35

Phương án
Tổng giá trị
phân công
{1, 2, 3, 4} 9+4+1+4=18
{1, 2, 4, 3} 9+4+9+8=30
{1, 3, 2, 4} 9+8+3+4=24
{1, 3, 4, 2} 9+8+9+7=33
{1, 4, 2, 3} 9+6+3+8=26
{1, 4, 3, 2} 9+6+1+7=23
{2, 1, 3, 4} 6+2+1+4=13
{2, 1, 4, 3} 6+2+9+8=25
{2, 3, 1, 4} 6+8+7+4=25
{2, 3, 4, 1} 6+8+9+8=31
{2, 4, 1, 3} 6+6+7+8=27
{2, 4, 3, 1} 6+6+1+8=21
{3, 1, 2, 4} 5+2+3+4=14
{3, 1, 4, 2} 5+2+9+7=23
{3, 2, 1, 4} 5+4+7+4=20
{3, 2, 4, 1} 5+4+9+8=26
{3, 4, 1, 2} 5+6+7+7=25
{3, 4, 2, 1} 5+6+3+8=22
{4, 1, 2, 3} 7+2+3+8=20
{4, 1, 3, 2} 7+2+1+7=17
{4, 2, 1, 3} 7+4+7+8=26
{4, 2, 3, 1} 7+4+1+8=20
{4, 3, 1, 2} 7+8+7+7=29
{4, 3, 2, 1} 7+8+3+8=26
Nhìn vào tất cả các phương án phân công ở bảng trên, ta thấy phương án {2, 1, 3,
4} tức là người 2 làm việc 1, người 1 làm việc 2, người 3 làm việc 3 và người 4 làm
việc 4 cho giá trị thấp nhất là 13. Vì thuật toán trên dẫn đến thuật toán sinh hoán vị n
phần tử, nên độ phức tạp của thuật toán Phân công công việc bằng kĩ thuật duyệt toàn
bộ là O(n!).

3.6. Bài tập


Bài 1. Thiết kế thuật toán tính giá trị đa thức p(x) = anxn + an - 1xn – 1 + … + a1x1 + a0
dựa vào kĩ thuật Brute-Force. Nếu thuật toán vừa thiết kế có độ phức tạp là O(n2), hãy
thiết kế thuật toán có độ phức tạp tuyến tính (tức là O(n)) cho bài toán này?
Bài 2. Một Topo mạng cho biết các máy tính, máy in và các thiết bị khác kết nối với
nhau như thế nào. Hình sau mô tả 3 Topo mạng hay sử dụng nhất: vòng, sao và đầy đủ.

Lê Xuân Việt – Dương Hoàng Huyên


36 Phân tích và thiết kế thuật toán

Cho ma trận logic A[0..n  1, 0..n  1] với n > 3 là ma trận kề biểu diễn đồ thị thể
hiện các Topo mạng trên. Yêu cầu thiết kế thuật toán dựa vào ma trận kề, xác định đồ
thị thuộc loại nào trong ba loại topo ở trên. Đánh giá độ phức tạp của thuật toán vừa
thiết kế.
Bài 3. Cài đặt thuật toán khớp chuỗi bằng ngôn ngữ lập trình bất kì.
Bài 4. Thiết kế một thuật toán tính số chuỗi con bắt đầu bằng kí tự A và kết thúc bằng
kí tự B trong một chuỗi cho trước. Đánh giá độ phức tạp của thuật toán vừa thiết kế.
Có thuật toán khác hiệu quả hơn thuật toán vừa thiết kế hay không?
Bài 5. Trong mặt phẳng Oxy, cho n điểm, tìm 1 điểm trong n điểm đã cho sao cho
tổng khoảng cách từ điểm đó đến các điểm còn lại nhỏ nhất.
Bài 6. Trong mặt phẳng Oxy, cho n điểm, tìm 1 điểm trong n điểm đã cho sao cho
khoảng cách từ điểm đó đến điểm xa nó nhất là nhỏ nhất.
Bài 7. Có cách khác định nghĩa khoảng cách trên mặt phẳng Oxy. Khoảng cách
Manhatan định nghĩa như sau:

dM(p1, p2) = |x1  x2| + |y1  y2|


Chứng minh dM thỏa các điều kiện:
1. dM(p1, p2) ≥ 0 với mọi điểm p1, p2 và = 0 khi p1 = p2.
2. dM(p1, p2) = dM(p2, p1).
3. dM(p1, p2) ≤ dM(p1, p3) + dM(p3, p2).
Kết quả của Bài 5, Bài 6 có thay đổi không nếu ta thay cách tính khoảng cách
bằng khoảng cách Manhatan?
Bài 8. Khoảng cách Hamming giữa hai chuỗi kí tự có độ dài bằng nhau được định
nghĩa là số kí tự khác nhau tại vị trí tương ứng. Khoảng cách Hamming được định
nghĩa như trên có thỏa ba điều kiện ở Bài 7 hay không? Thiết kế thuật toán và đánh giá
độ phức tạp của thuật toán tìm hai điểm gần nhau nhất trong trường hợp các điểm được

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 3. Brute-Force và duyệt toàn bộ 37

thay bằng các chuỗi có độ dài bằng nhau và khoảng cách sử dụng là khoảng cách
Hamming.
Bài 9. Có n (n ≥ 3) người trên 1 cánh đồng, mỗi người có duy nhất một láng giềng gần
nhất. Khi có tín hiệu, mọi người đều ném 1 mẫu kem về phía người gần với mình nhất.
Giả sử n là số lẻ và không có ai ném trật mục tiêu. Luôn luôn còn lại ít nhất một người
không dính kem trên người, đúng hay sai?
Bài 10. Cài đặt thuật toán tìm bao lồi của tập điểm n điểm trên mặt phẳng bằng ngôn
ngữ lập trình bất kì.
Bài 11. Giả sử thời gian để sinh ra một chu trình Hamilton trong bài toán người đi du
lịch là hằng số. Hãy đánh giá độ phức tạp của thuật toán tìm chu trình ngắn nhất của
bài người đi du lịch ở trên. Giả sử thuật toán này đã được cài đặt trên máy tính với tốc
độ xử lí là 10 tỉ phép toán/1 giây. Hãy ước lượng số thành phố tối đa mà thuật toán này
có thể giải được trong thời gian lần lượt là: 1 giờ, 1 ngày, 1 năm, 1 thế kỉ.
Bài 12. Hình vuông ma thuật bậc n là một ma trận vuông n × n số nguyên từ 1 đến n2.
Mỗi số nguyên chỉ xuất hiện duy nhất một lần sao cho tổng các số trên từng dòng, từng
cột và đường chéo chính bằng nhau.
a. Chứng minh rằng nếu hình vuông trên tồn tại thì tổng trong điều kiện ở trên
bằng n(n2 + 1) / 2.
b. Thiết kế thuật toán dựa vào kĩ thuật duyệt toàn bộ để tìm một hình vuông ma
thuật bậc n.
Bài 13. Bài toán phân hoạch. Cho n số nguyên dương, chia các số trên thành hai phần
sao cho tổng các số ở mỗi phần bằng nhau (có thể có trường hợp không có lời giải).
Thiết kế thuật toán duyệt toàn bộ để giải quyết bài toán trên. Chú ý, cố gắng cực tiểu
hóa số lượng tập con cần phải sinh.
Bài 14. Cho đồ thị G = <V, E> và một số nguyên dương k. Xác định có hay không đồ
thị con đầy đủ có k đỉnh. Đồ thị đầy đủ là đồ thị mà một đỉnh bất kì đều có cạnh nối
trực tiếp đến các đỉnh còn lại. Thiết kế thuật toán duyệt toàn bộ cho bài toán này.
Bài 15. Bài toán 8 quân hậu. Trên bàn cờ vua kích thước 8 × 8, đặt 8 quân hậu sao cho
không có quân hậu nào cùng dòng, cùng cột, cùng đường chéo. Giả sử máy tính có tốc
độ thực hiện là 10 tỉ phép kiểm tra/giây, ước lượng thời gian để tìm được tất cả các
nghiệm của bài toán trên khi cài đặt thuật toán duyệt toàn bộ trên máy tính đó.

Lê Xuân Việt – Dương Hoàng Huyên


Chương 4. Giảm để trị 39

CHƯƠNG 4. GIẢM ĐỂ TRỊ

4.1. Giới thiệu


Kĩ thuật thiết kế thuật toán giảm để trị dựa vào khai thác mối liên hệ giữa nghiệm
của bài toán hiện tại với nghiệm của bài toán tương tự nhưng có kích thước nhỏ hơn.
Khi mối liên hệ như vậy được thiết lập, nó có thể được khai thác hoặc từ trên xuống
(top-down) hoặc từ dưới lên (bottom-up). Cách khai thác từ trên xuống thường gọi là
đệ quy. Khai thác mối liên hệ từ dưới lên thường cài đặt dưới dạng các vòng lặp, tức là
bắt đầu giải quyết bài toán với kích thước nhỏ nhất, sau mỗi lần lặp sẽ tăng kích thước
lên. Có ba loại chính trong kĩ thuật giảm để trị:
+ Giảm bằng một hằng số.
+ Giảm bằng một hệ số.
+ Giảm với kích thước thay đổi.
Giảm một hằng số là kích thước của bài toán sẽ được giảm bởi hằng cố định sau
mỗi lần lặp của thuật toán, thông thường là giảm 1, xem Hình 4-1. Ví dụ như bài toán
tính an với a ≠ 0 và n là số nguyên không âm. Mối quan hệ giữa nghiệm bài toán kích
thước n với n  1 thể hiện qua công thức sau: an = a × an  1. Thuật toán tính an có thể
thực hiện “từ trên xuống” bằng cách sử dụng công thức đệ quy được định nghĩa như
sau: f(n) = f(n  1) × a, với f(0) = 1. Nếu thực hiện “từ dưới lên” ta sẽ nhân giá trị a n
lần (sử dụng vòng lặp).
Giảm một hệ số là giảm kích thước của bài toán bằng hệ số chia cố định sau mỗi
vòng lặp, thông thường hệ số giảm là 2, minh họa trong Hình 4-2. Ví dụ tính an, ta có
thể tính an / 2 với mối liên hệ như sau: an = (an / 2)2 nếu n chẵn. và an = a(n  1) / 2 × a nếu
n lẻ. Thuật toán có thể thực hiện “từ trên xuống” bằng công thức đệ quy như sau:
2
[ƒ (n)] nếu n chẵn
2
2
ƒ(n) = [ƒ ( n–1
)] . a nếu n lẻ
2
⎩ 1 nếu n = 0

Lê Xuân Việt – Dương Hoàng Huyên


40 Phân tích và thiết kế thuật toán

Bài toán kích


thước n

Bài toán con


kích thước n  1

Giải bài toán con


kích thước n  1

Nghiệm của bài


toán kích thước n

Hình 4-1. Kĩ thuật giảm (một) để trị.

Bài kích
toán thước n

Bài toán con


kích thước n / 2

Giải bài toán con


kích thước n / 2

Nghiệm của bài


toán kích thước n

Hình 4-2. Kĩ thuật giảm (một nửa) để trị.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 4. Giảm để trị 41

Giảm với kích thước thay đổi là giá trị cần giảm sau mỗi lần lặp của thuật toán là
khác nhau. Ví dụ bài toán tìm ước số chung lớn nhất của hai số a và b theo thuật toán
Euclid được cho bởi công thức sau: gcd(a, b) = gcd(b, a mod b). Khi đó, sau mỗi lần
lặp các giá trị của a, b sẽ giảm dần với giá trị mỗi lần giảm khác nhau.

4.2. Sắp xếp chèn


Trong phần này ta xét kĩ thuật giảm một để sắp xếp mảng A[0..n – 1] n phần tử.
Ý tưởng của thuật toán như sau: giả sử bài toán với kích thước nhỏ hơn đã được giải
quyết, tức là mảng A[0..n – 2] đã được sắp xếp. Ta có thể lấy nghiệm của bài toán với
kích thước n – 1 này để tìm nghiệm bài toán với kích thước n bằng cách chèn phần tử
còn lại A[n – 1] vào mảng A[0..n – 2] sao cho mảng mới hình thành vẫn đảm bảo trật
tự tăng dần, đây là cách tiếp cận từ trên xuống. Tuy nhiên ta cũng có thể thiết kế thuật
toán sắp xếp chèn theo cách tiếp cận từ dưới lên như sau: xuất phát từ mảng có 1 phần
tử, ta chèn phần tử thứ hai vào mảng sao cho mảng tăng dần, tiếp tục chèn phần tử thứ
ba, thứ tư, v.v … cho đến phần tử thứ n – 1. Thuật toán sắp xếp chèn dựa theo cách
tiếp cận từ dưới lên như sau:
ALGORITHM InsertionSort(A[0..n − 1])
//đầu vào: mảng A[0..n − 1] n phần tử
//đầu ra: mảng A[0..n − 1] đã sắp xếp tăng dần
for i ← 1 to n − 1 do
v ← A[i]
j ← i − 1
while (j ≥ 0) and (A[j] > v) do
A[j + 1] ← A[j]
j ← j − 1
A[j + 1] ← v
return A
Phép toán cơ bản của thuật toán này là phép so sánh A[j] > v, số lượng phép so
sánh này không chỉ phụ thuộc vào kích thước dữ liệu đầu vào mà còn phụ thuộc vào
dữ liệu cụ thể. Do đó ta sẽ đánh giá độ phức tạp của thuật toán trong trường hợp xấu
nhất, tức là trường hợp dữ liệu đầu vào có trật tự giảm dần. Tổng số phép toán so sánh
phải thực hiện là:
(n–1)n
t(n) = ∑n–1 ∑i–1 1 = ∈ 0( n2 ).
i=1 j=0 2

4.3. Thuật toán sinh tổ hợp


4.3.1. Sinh hoán vị
Để đơn giản ta coi tập hợp cần sinh hoán vị là tập các số nguyên 1..n, để sử dụng

Lê Xuân Việt – Dương Hoàng Huyên


42 Phân tích và thiết kế thuật toán

trong tập hợp bất kì, ta thay các phần tử i này thành chỉ số của tập hợp đó ví dụ như:
{a1, a2, …, an}. Ý tưởng giảm để trị thực hiện như sau: giả sử ta đã sinh được hoán vị
n – 1 phần tử. Hoán vị n phần tử nhận được bằng cách chèn phần tử thứ n vào các vị trí
của n – 1 phần tử trước đó. Có thể chèn từ phải qua trái hoặc từ trái qua phải. Ví dụ n =
3 ta có các bước thực hiện như sau:
Bắt đầu: 1
Chèn 2 từ phải qua trái của 1 ta có: 12 và 21
Chèn 3 từ phải qua trái vào 12 ta có: 123, 132, 312
Chèn 3 từ trái qua phải 21 ta có: 321, 231, 213
Thuật toán này có điểm thuận tiện là hoán vị mới được sinh ra từ hoán vị trước
đó chỉ cần đảo hai phần tử liền kề nhau, điều này rất có ý nghĩa trong vấn đề tăng tốc
độ tính toán và ứng dụng của hoán vị. Ví dụ như bài toán người đi du lịch, ta cần tìm
tất cả các hoán vị của n thành phố sau đó tìm hoán vị có tổng khoảng cách giữa các
thành phố nhỏ nhất. Việc chỉ đổi vị trí của hai phần tử kề nhau sẽ giảm đáng kể thao
tác tính khoảng cách giữa hai thành phố (tại sao?).
Có thuật toán khác để sinh hoán vị mà không cần phải sinh hoán vị với số phần
tử nhỏ hơn. Thuật toán này bổ sung thêm thông tin về hướng của mỗi phần tử để hỗ trợ
sinh hoán vị. Ví dụ 1 hoán vị như sau: 3̄⃖ ⃖2̄ 4̄⃖ ⃖1̄. Ta xét định nghĩa sau: phần tử k
được gọi là phần tử di động nếu mũi tên trên phần tử k chỉ về phần tử có giá trị nhỏ
hơn kề với nó. Trong ví dụ trên, 3 và 4 là phần tử di động, 1 và 2 không phải là phần tử
di động. Sử dụng định nghĩa phần tử di động, ta có thể mô tả thuật sinh hoán vị
Johnson- Trotter như sau:
ALGORITHM JohnsonTrotter(n)
//Đầu vào: Số nguyên dương n
//Đầu ra: danh sách các hoán vị của {1, . . . , n}
Khởi tạo hoán vị đầu tiên: ⃖1̄ ⃖2̄ … n⃖¯.
while (hoán vị cuối cùng có phần tử di động) do
Tìm phần tử di động lớn nhất k
đổi phần tử k với phần tử kề với k mà có mũi tên trỏ đến
đảo hướng mũi tên của tất cả các phần tử lớn hơn k
bổ sung hoán vị mới vào danh sách
Ví dụ n = 3 ta có các hoán vị lần lượt như sau: ⃖ ⃖1¯⃖3¯⃖ ⃖3¯⃖1¯⃖ ¯3⃖⃖2¯⃖ ⃖2¯¯3⃖⃖1¯,
1¯ ⃖ 2¯ ⃖ 3¯ , 2¯, 2¯, 1¯,
⃖2¯ ⃖1¯ ¯3⃖.Đây là thuật toán tốt nhất để sinh hoán vị, thời gian chạy xấp xỉ với số hoán
vị cần sinh tức là độ phức tạp O(n!).

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 4. Giảm để trị 43
Ta thấy các hoán vị được sinh ra bởi thuật toán Johnson-Trotter không theo một

Lê Xuân Việt – Dương Hoàng Huyên


44 Phân tích và thiết kế thuật toán

trật tự nhất định, do đó khó quan sát các hoán vị được sinh ra. Ta có thể liệt kê các
hoán vị theo thứ tự từ điển, tức là hoán vị “nhỏ” đặt trước hoán vị “lớn”. Ví dụ n = 3 ta
có thể liệt kê 6 hoán vị này như sau: 123, 132, 213, 231, 312, 321. Thuật toán sinh
hoán vị theo thứ tự từ điển như sau:
ALGORITHM LexicographicPermute(n)
//Đầu vào: Số nguyên dương n
//Đầu ra: Danh sách các hoán vị của {1, . . ., n} theo thứ tự từ
điển.
Khởi gán hoán vị đầu tiên 12 . . . n
while (hoán vị có 2 phần tử kề nhau có thứ tự tăng) do
Đặt i chỉ số lớn nhất mà ai < ai + 1 //ai + 1> ai + 2 > . . . > an
Tìm chỉ số j lớn nhất sao cho ai < aj //j ≥ i + 1
Đổi ai với aj //ai + 1, ai + 2,. . ., an có trật tự giảm
Đảo trật tự từ ai + 1 đến an
Thêm hoán vị mới vào danh sách.
Ví dụ n = 4, ta có các hoán vị sau:
1234, 1243, 1324, 1342, 1423, 1432
2134, 2143, 2314, 2341, 2413, 2431
3124, 3142, 3214, 3241, 3412, 3421
4123, 4132, 4213, 4231, 4312, 4321

4.3.2. Sinh tập con


Cho tập hợp A = {a1, a2, …, an} có n phần tử. Để sinh tất cả các tập con của A
dựa vào ý tưởng giảm để trị ta thực hiện hai bước sau: Đầu tiên sinh tất cả tập con n 
1 phần tử đầu tiên trong A. Sau đó giữ nguyên số tập con này, tiếp tục bổ sung phần tử
an vào tất cả các tập con tìm được để tao ra các tập con mới. Xét ví dụ cụ thể sau: cho
A = {a1, a2, a3}.

n Tất cả các tập con Giải thích


0 {} Tập rỗng
Giữ nguyên tập {}, bổ sung a1 để tạo tập
1 {}, {a1}
con mới
{}, {a1} Giữ nguyên tập {}, {a1}, bổ sung a2 để
2
{a2}, {a1, a2} tạo tập con mới
{}, {a1}, {a2}, {a1, a2} Giữ nguyên tập {}, {a1}, {a2}, {a1, a2},
3
{a3}, {a1, a3}, {a2, a3}, {a1, a2, a3} bổ sung a3 để tạo tập con mới
Tuy nhiên ta cũng có một thuật toán khác sinh tập con hiệu quả hơn bằng cách
dựa vào chuỗi nhị phân có độ dài n. Theo chuỗi nhị phân này, nếu bit thứ i bằng 1 có
nghĩa là phần tử ai có xuất hiện trong tập con và ngược lại. Xét ví dụ cụ thể như sau:
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 4. Giảm để trị 45

Chuỗi bit 000 001 010 011 100 101 110 111
Tập con {} {a3} {a2} {a2, a3} {a1} {a1, a3} {a1, a2} {a1, a2, a3}
Chú ý rằng mặc dầu chuỗi bit sinh ra theo trật tự từ điển, nhưng tập con dựa vào
chuỗi bit này lại không có thứ tự từ điển.

4.4. Giảm theo hệ số


4.4.1. Tìm kiếm nhị phân

Cho dãy số A[0..n  1] đã được sắp xếp tăng (giảm), tìm số x có trong dãy hay
không? Thuật toán thực hiện như sau: so sánh phần tử x với phần tử vị trí giữa của dãy,
giả sử là A[m]. Nếu x = A[m] thì kết thúc thuật toán. Nếu x > A[m] thì tìm kiếm trên
nửa cuối của dãy tức là từ A[m + 1] đến A[n  1]. Nếu x < A[m] thì tìm trên nữa đầu
của dãy tức là từ A[0] đến A[m  1]. Chi tiết thuật toán như sau:
ALGORITHM BinarySearch(A[0..n − 1], x)
//Đầu vào: mảng A[0..n − 1] sắp xếp theo trật tự tăng dần, 1 số x
//Đầu ra: chỉ số của phần tử đầu tiên bằng x, hoặc −1 nếu không có
phần tử nào
l ← 0; r ← n − 1
while (l ≤ r) do
m ← (l + r) / 2 //lấy phần nguyên
if (x = A[m]) then
return m
else
if (x < A[m]) then
r ← m −1
else
l ← m +1
return −1
Kích thước dữ liệu đầu vào là n, phép toán cơ bản là phép so sánh. Vì số phép
toán cơ bản không chỉ phụ thuộc vào kích thước dữ liệu đầu vào mà còn phụ thuộc vào
chính dữ liệu của các phần tử. Do đó để phân tích hiệu quả của thuật toán này, ta sẽ
đếm số phép toán so sánh trong trường hợp xấu nhất, đó là trường hợp x không xuất
hiện trong dãy. Công thức cụ thể như sau: t(n) = t(n / 2) + 1 và t(1) = 1. Giả sử n = 2k,
ta có t(2k) = t(2k / 2) + 1 = t(2k  1) + 1 = … = k + 1 = log(2k) + 1  O(log n).

4.4.2. Bài toán tiền xu giả


Cho n đồng tiền xu có hình thức giống nhau, trong đó có một đồng xu giả, đồng
xu giả sẽ nặng hơn đồng xu thật. Bằng cách sử dụng cân cân bằng, ta có thể so sánh
trọng lượng của hai đồng xu. Cân cân bằng có 3 trạng thái đó là bằng nhau, nghiêng về
bên trái hoặc nghiêng về bên phải. Hãy thiết kế một thuật toán tìm được đồng tiền xu
Lê Xuân Việt – Dương Hoàng Huyên
46 Phân tích và thiết kế thuật toán

giả trong n đồng xu đã cho.


Thuật toán như sau: chia n đồng xu thành hai phần mỗi phần có số tiền xu bằng
nhau, nếu n lẻ thì để lại 1 đồng không cân. Sau đó để mỗi phần lên cân cân bằng. Nếu
hai phần bằng nhau, đồng tiền giả chính là đồng tiền để lại không cân, nếu bên nào
nặng hơn thì đồng xu giả nằm ở trong phần đó. Tiếp tục chia phần đó ra hai phần có số
đồng xu và thực hiện tương tự.

4.4.3. Phương pháp nhân nông dân Nga


Phương pháp nhân dưới đây được cho là bắt nguồn từ Ai Cập cổ đại, nhưng ngày
nay người ta gọi phương pháp này là phương pháp nhân của nông dân Nga.
Ta có thể tính tích n × m dựa vào ý tưởng giảm để trị như sau:
n
2
× 2N, nếu n cℎ ẳn
n× N= {
n–1
× 2N + N, nếu n lẻ
2

Lặp lại công thức trên cho đến khi n = 1 thì dừng. Ví dụ tính 50 × 65 như sau:
n m kết quả trung gian khi n lẻ
50 65
25 130
12 260 + 130
6 520
3 1040
1 2080 + 1040

kết quả 2080 + 130 + 1040 = 3250

4.4.4. Bài toán Josephus


Bài toán Josephus là một trong những bài toán rất nổi tiếng, ra đời từ rất sớm,
khoảng năm 370 sau công nguyên bởi Aurelius Ambrosius (Ambrôsiô trong tiếng Việt),
là một Tiến sĩ Hội thánh sống vào khoảng năm 340 đến 397 sau công nguyên. Ông
cũng được xem là vị thánh bảo trợ của thành phố Milan, nước Ý. Bài toán đưa ra bởi
Ambrôsiô như sau:
Titus Flavius Vespasianus (Vespasian, tiếng Anh) là một vị tướng lãnh đạo quân
đội tài năng, người về sau đã trở thành hoàng đế La Mã (từ năm 69 đến năm 79), đã

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 4. Giảm để trị 47

cùng với quân đội dưới sự chỉ huy của ông ta dập tắt cuộc nổi loạn của người Do Thái
chống lại đế quốc La Mã. Trong cuộc vây hãm Yodfat, Vespasian đã bắt được Flavius
Josephus, một nhà lãnh đạo kháng chiến của người Do Thái. Sau khi bắt được các tù
nhân Do Thái, người La Mã quyết định xử tử tù nhân bằng cách cho toàn bộ xếp thành
vòng tròn và bắt đầu đếm từ một, cứ người nào đến 3 là bị giết cho đến khi chỉ còn lại
2 người. Trước yêu cầu khắc nghiệt như trên, Josephus đã nhanh chóng tìm ra vị trí để
mình và người bạn thân không bị giết. Hỏi rằng ông đã chọn vị trí nào cho ông và
người bạn?
Cho n người đánh số từ 1 đến n xếp thành một vòng tròn, bắt đầu đếm với người
thứ nhất, loại trừ người thứ hai. Tiếp tục đến người thứ ba, loại người thứ tư. Tương tự
cho đến khi chỉ còn một người. Bài toán đặt ra là xác định chỉ số người sống sót. Ví dụ
n = 6, ta có vòng tròn như sau:

1
6 2

5 3
4
Bắt đầu từ người thứ 1, khi đó người bị loại tương ứng là 2, tiếp tục đến người
thứ 3 loại người thứ 4, cuối cùng đến người thứ 5 loại người thứ 6 và vòng tròn mới
như sau:

5 3

Vì người thứ 6 là người cuối cùng bị loại trong vòng đầu, ta tiếp tục bắt đầu vòng
2 từ người thứ 1. Khi đó số người bị loại sẽ là 3, tiếp tục đến người thứ 5 loại người
thứ 1. Vậy người sống sót cuối cùng là J(6) = 5.
Tiếp tục ví dụ khác với n = 7 và bắt đầu bởi người thứ 1. Vòng tròn tương ứng
của lượt thứ nhất như sau:

Lê Xuân Việt – Dương Hoàng Huyên


48 Phân tích và thiết kế thuật toán

1
7 2

6 3

5 4

Khi đó vị trí người bị loại sẽ là: 2, 4, 6, 1 và vòng tròn lượt thứ hai như sau:

7 5

Vì người vị trí thứ 1 bị loại sau cùng ở vòng thứ nhất, nên người thứ 3 sẽ bắt đầu
ở vòng thứ hai, khi đó người thứ 5, 3 sẽ bị loại và người thứ 7 sẽ sống sót.
Tổng quát, ta xét hai trường hợp n chẵn lẻ khác nhau. Khi n chẵn (n = 2k), vòng
đầu tiên sẽ loại một nửa số người, khi đó bài toán quay lại phát biểu ban đầu nhưng với
số người giảm một nửa và chỉ số lần lượt thay đổi như sau: người thứ 3 sẽ trở thành
người thứ 2, người thứ 5 sẽ trở thành người thứ 3, …công thức tổng quát để tính vị trí
mới như sau: J(2k) = 2J(k)  1.
Nếu n lẻ (n = 2k + 1), số người bị loại ở vòng đầu tiên sẽ đứng ở vị trí chẵn và
một người ở vị trí thứ 1. Lúc này bài toán quay lại phát biểu ban đầu với kích thước là
k và vị trí mới như sau: thứ 3 trở thành thứ 1, thứ 5 trở thành thứ 2, thứ 7 trở thành thứ
3, … Công thức tổng quát để tính vị trí mới như sau: J(2k + 1) = 2J(k) + 1, với J(1) = 1.
Một nhận xét thú vị đó là nghiệm của bài toán này chính là lấy biểu diễn nhị phân
của n đẩy các bit về trái 1 lần và chèn số 1 vào cuối. Ví dụ J(610) = J(1102) = 101 = 5;
J(710) = J(1112) = 111 = 7.

4.5. Giảm với kích thước thay đổi


Trường hợp thứ ba của kĩ thuật giảm để trị đó là giá trị giảm sau mỗi lần lặp có
thể khác nhau. Ví dụ điển hình cho trường hợp này là bài toán tìm ước số chung lớn
nhất của hai số a, b.

4.5.1. Tìm trung vị và bài toán chọn

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 4. Giảm để trị 49

Bài toán chọn là bài toán tìm phần tử nhỏ thứ k trong dãy n số. Nếu k = 0 và k = n
 1 bài toán chính là tìm số nhỏ nhất và lớn nhất của dãy số. Khi k = [n / 2], bài toán
chính là tìm trung vị của dãy. Phần tử M trong dãy là trung vị nếu có một nửa phần tử
lớn hơn M và một nửa phần tử trong dãy nhỏ hơn M. Để giải quyết bài toán này, ta có
thể sắp xếp danh sách theo trật tự giả sử tăng dần. Khi đó phần tử nhỏ thứ k chính là
phần tử thứ k trong danh sách. Độ phức tạp của thuật toán tìm phần tử thứ k chính
bằng độ phức tạp của thuật toán sắp xếp. Có cách khác để tìm phần tử nhỏ thứ k trong
danh mà không cần phải sắp xếp toàn bộ các phần tử. Đó là thuật toán chia Lomuto.
Ý tưởng thuật toán chia Lomuto trên mảng A[l..r] như sau: dựa vào phần tử ứng
viên p (thông thường ta chọn phần tử ứng viên là A[l]), chia mảng các phần tử thành 3
đoạn, một đoạn các phần tử nhỏ hơn p, một đoạn các phần tử lớn hơn hoặc bằng p và
một đoạn là các phần tử chưa so sánh với p. Chú ý, các đoạn này có thể là rỗng.

Hình 4-3. Minh họa thuật toán chia Lomuto.


Bắt đầu với i = l + 1, thuật toán sẽ quét mảng con A[l..r] từ bên trái qua phải, tại
mỗi bước lặp, nó so sánh phần tử ứng viên p với đoạn chưa so sánh (xác định bởi chỉ
số i). Nếu A[i] ≥ p thì tăng i để mở rộng đoạn ≥ p và thu nhỏ đoạn chưa so sánh. Nếu
A[i] < p thì mở rộng đoạn < p bằng cách tăng s (chỉ số của phần tử cuối cùng trong
đoạn < p). Đổi hai giá trị A[i] và A[s] và tăng i để trỏ đến phần tử mới tiếp theo của
đoạn chưa so sánh. Sau khi không còn phần tử nào để xử lí, thuật toán sẽ đổi giá trị
phần tử ứng viên p với A[s]. Chi tiết thuật toán như sau:
ALGORITHM LomutoPartition(A[l..r])

Lê Xuân Việt – Dương Hoàng Huyên


50 Phân tích và thiết kế thuật toán

//Đầu vào: mảng con A[l..r] của mảng A[0..n − 1], được định nghĩa
bởi hai chỉ số trái (l) và phải (r), l ≤ r
//Đầu ra: mảng A[l..r] đã được chia với các phần tử < p ở bên trái
và các phần tử ≥ p ở bên phải của p và trả về vị trí mới của phần tử
ứng viên p
p ← A[l]
s ← l
for i ← l + 1 to r do
if (A[i] < p) then
s ← s + 1; swap(A[s], A[i])
swap(A[l], A[s])
return s
Ta sẽ sử dụng thuật toán chia Lomuto này để tìm phần tử lớn thứ k (0 ≤ k ≤ n  1)
như sau: giả sử mảng bắt đầu từ 0 và s là vị trí sau khi chia, tức là chỉ số của phần tử
ứng viên xuất hiện sau khi chia. Nếu s = k, phần tử ứng viên p chính là phần tử nhỏ thứ
k trong dãy. Nếu s > k, phần tử nhỏ thứ k trong dãy có thể tìm thấy trong dãy con bên
trái của dãy đã chia, tức là các phần tử A[0] đến A[s  1]. Và nếu s < k, phần tử nhỏ thứ
k có thể tìm thấy trong dãy con bên phải phần tử đã chia, tức là các phần tử A[s + 1]
đến A[n  1]. Chi tiết thuật toán tìm phần tử nhỏ thứ k trong dãy dựa vào thuật toán
chia Lomuto như sau:
ALGORITHM Quickselect(A[0..n - 1], k)
//Đầu vào: mảng con A[l..r] của A[0..n − 1],số k (0 ≤ k ≤ n - 1)
//Đầu ra: Giá trị của phần tử nhỏ thứ k trong mảng A[l..r]
s ← LomutoPartition(A[l..r])
if (s = k) then
return A[s]
else
if (s > k) then
return Quickselect(A[l..s − 1], k)
else
return Quickselect(A[s + 1..r], k)
Để đánh giá độ phức tạp của thuật toán này, ta sẽ xét các trường hợp xấu nhất, tốt
nhất và trung bình. Trường hợp xấu nhất đó là mảng A[0..n  1] có thứ tự tăng (giảm)
dần. Khi đó thuật toán chia Lomuto sẽ chia mảng thành hai phần với một phần rỗng và
một phần còn lại có n  1 phần tử. Do đó số phép so sánh cần thực hiện là: t(n) = t(n 
1) + n, giải công thức đệ quy này ta có t(n) = n(n  1) / 2  O(n2). Trường hợp tốt nhất
đó là sau một lần chia, ta đã tìm thấy phần tử nhỏ thứ k mong muốn, khi đó độ phức
tạp thuật toán là O(n) (chính là độ phức tạp của thuật toán chia Lomuto). Trường hợp
trung bình, mảng sau khi chia tạo ra hai phần bằng nhau. Khi đó t(n) = t(n / 2) + n, giải
công thức đệ quy này ta được t(n) = 2n  1  O(n).

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 4. Giảm để trị 51

Để hiểu rõ hơn thuật toán này, xét ví dụ sau: cho dãy số 4, 1, 10, 8, 7, 12, 9, 2, 15.
Số k = [9 / 2] = 4, tìm số nhỏ thứ 5 trong dãy. Bước chia thứ nhất như sau (chú ý, phần
tử in đậm là phần tử ứng viên):
0 1 2 3 4 5 6 7 8
s i
4 1 10 8 7 12 9 2 15
s i
4 1 10 8 7 12 9 2 15
s i
4 1 10 8 7 12 9 2 15
s i
4 1 2 8 7 12 9 10 15
s i
2 1 4 8 7 12 9 10 15
Do s = 2 < k = 4, nên ta tiếp tục tìm nửa bên phải của mảng cụ thể là từ phần tử s
+ 1 đến r tức là từ phần tử thứ 3 đến 8. Cụ thể như sau (phần tử in đậm là phần tử ứng
viên):
0 1 2 3 4 5 6 7 8
s i
8 7 12 9 10 15
s i
8 7 12 9 10 15
s i
8 7 12 9 10 15
s i
7 8 12 9 10 15
Ta thấy s = k = 4, do đó trung vị của dãy là 8.

4.5.2. Tìm kiếm nội suy


Tìm kiếm nhị phân là lấy giá trị của phần tử ở giữa danh sách so sánh với giá trị
cần tìm để xác định bước tìm kiếm tiếp theo. Gần giống như tìm kiếm nhị phân, tìm
kiếm nội suy lấy giá trị cần tìm (giả sử là x) để xác định vị trí phần tử cần so sánh. Cụ
thể, cho dãy các phần tử nằm giữa A[l] và A[r]. Thuật toán giả sử rằng dãy tăng tuyến
tính, tức là các phần tử của dãy nằm trên đường thẳng nối hai điểm (l, A[l]) và (r, A[r])
như Hình 4-4. Giá trị tìm kiếm sẽ được so sánh với phần tử có chỉ số m được tính như
sau: N = l + ⎝ ( s – Æ[S]) (r – S) [. Sau khi so sánh x với A[m], thuật toán sẽ dừng nếu x =
Æ[r] – Æ[S]

A[m], hoặc sẽ tiếp tục tìm kiếm x trên phạm vi từ l đến m  1, hoặc từ m + 1 đến r. Ta
thấy kích thước của bài toán đã giảm nhưng giá trị giảm sẽ thay đổi mỗi lần lặp. Chi

Lê Xuân Việt – Dương Hoàng Huyên


52 Phân tích và thiết kế thuật toán

tiết thuật toán như sau:


ALGORITHM InterpolationSearch(A[0..n - 1], x)
//Đầu vào: mảng A[0..n - 1] và số x
//Đầu ra: chỉ số đầu tiền x xuất hiện trong A, -1 nếu không xuất
hiện.
l ← 0; r ← n − 1
while (l ≤ r) do
( s–Æ[S]) ( r–S)
N ← ⎝l + [
Æ[r]–Æ[S]
if (x = A[m]) then
return m
else
if (x < A[m]) then
r ← m −1
else
l ← m +1
return −1
Giá trị

A[r]

A[l]
Chỉ số
l m r
Hình 4-4. Minh họa thuật toán tìm kiếm nội suy.

4.5.3. Trò chơi Nim


Cho n đồng xu xếp chồng lên nhau. Có hai người chơi, mỗi người sẽ lần lượt
chọn trong n đồng xu đã cho ra x đồng xu, nhưng không được chọn quá m đồng xu (1
≤ x ≤ m ≤ n). Người chọn đồng xu cuối cùng sẽ là người chiến thắng. Người đi trước
hoặc là người đi sau, ai sẽ là người chiến thắng? Ví dụ, giả sử n = 10, m = 4 và có hai
người chơi A đi trước và B đi sau. Một tình huống cụ thể như sau:
1. A chọn 4, khi đó n còn 6.
2. B chọn 4, khi đó n còn 2.
3. A chọn 2, khi đó n còn 0  A là người chọn đồng xu cuối cùng nên A thắng.
Một tình huống khác như sau:
1. A chọn 4, khi đó n còn 6.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 4. Giảm để trị 53

2. B chọn 1, khi đó n còn 5.


3. A chọn 4, khi đó n còn 1.
4. B chọn 1, khi đó n còn 0  B là người chọn đồng xu cuối cùng nên B thắng.
Bài toán đặt ra là đưa ra cách chọn đồng xu để sao cho người chọn tiếp theo ở
trạng thái bị thua. Bài toán đã được chứng minh là nếu số đồng xu còn lại là m + 2 ≤ n
≤ 2m + 1 thì người chơi tiếp theo sẽ bị thua. Cụ thể hơn, nếu đến lượt người A chọn mà
số đồng xu hiện tại là n với m + 2 ≤ n ≤ 2m + 1, thì A chọn n mod (m + 1) đồng xu sẽ
làm người B sẽ bị thua.
Trò chơi này cũng được mở rộng trong trường hợp có n chồng đồng xu. Quy luật
như sau: Có n chồng đồng xu đánh số từ 1 đến I và số lượng mỗi chồng tương ứng là
n1, n2, …, nI, tại mỗi lượt đi người chơi có thể chọn số lượng đồng xu bất kì nhưng chỉ
trong một chồng (có thể chọn hết đồng xu trong một chồng). Cách thức để người đi
trước chiến thắng trò chơi này dựa vào biểu diễn nhị phân của số lượng đồng xu ở các
chồng. Cụ thể như sau: gọi b1, b2, …, bI là biểu diễn nhị phân của số lượng đồng xu ở
mỗi chồng tương ứng là n1, n2, …, nI. Ta tính tổng bs (gọi là tổng Nim) các bit nhị phân
không nhớ của các số nhị phân này. (nói cách khác, chữ số nhị phân ở bit thứ i của
tổng bs bằng 0 nếu số bit nhị phân bằng 1 tại vị trí thứ i của các toán hạng là số chẵn,
ngược lại bằng 1 nếu lẻ). Người chơi tiếp theo sẽ thắng nếu tổng Nim có chứa ít nhất là
1 chữ số 1. Tương ứng, người chơi tiếp theo sẽ thua nếu tổng Nim bs chỉ chứa các số 0.
Ví dụ: có ba chồng mỗi chồng có tương ứng n1 = 3, n2 = 4, n3 = 5 đồng xu và biểu diễn
nhị phân b1 = 011, b2 = 010, b3 = 101. Khi đó tổng Nim tương ứng là:
b1 = 011
b2 = 100
b3 = 101
bc = 010
Do tổng Nim bs có chứa số 1 nên người chơi trước sẽ thắng. Để tìm nước đi mang
lại chiến thắng, người chơi phải chọn số đồng xu sao cho tổng Nim chỉ còn lại các số 0.
Trong ví dụ trên, để chuyển số 1 trong chuỗi nhị phân tổng bs thành số 0, ta chuyển số
1 thứ 2 trong chuỗi nhị phân đầu tiên thành số 0. Điều này có nghĩa là ta chọn 2 đồng
xu từ chồng thứ nhất. Cụ thể các bước chọn để A (chơi trước) thắng B (chơi sau) như
bảng sau:
Các bước chọn Chồng 1 Chồng 2 Chồng 3 Tổng Nim
Số đồng xu (nhị phân) 3 (011) 4 (100) 5 (101) (010)
A chọn 2 từ chồng 1 1 (001) 4 (100) 5 (101) (000)
B chọn 2 từ chồng 2 1 (001) 2 (010) 5 (101) (110)
Lê Xuân Việt – Dương Hoàng Huyên
54 Phân tích và thiết kế thuật toán

Các bước chọn Chồng 1 Chồng 2 Chồng 3 Tổng Nim


A chọn 2 từ chồng 3 1 (001) 2 (010) 3 (011) (000)
B chọn 1 từ chồng 1 0 (000) 2 (010) 3 (011) (001)
A chọn 1 từ chồng 3 0 (000) 2 (010) 2 (010) (000)
B chọn 2 từ chồng 3 0 (000) 2 (010) 0 (000) (010)
A chọn 2 từ chồng 2 0 (000) 0 (000) 0 (000) (000)

4.6. Bài tập


Bài 1. Giả sử có n đội thi đấu vòng tròn một lượt, mỗi trận đấu có kết quả là thắng-
thua hoặc hòa. Thiết kế thuật toán liệt kê trật tự các đội sao cho mỗi đội luôn thắng
hoặc hòa đội liền kề phía sau. Đánh giá độ phức tạp của thuật toán trên.

Bài 2. Cho mảng A[0..n  1] các phần tử có trật tự bất kì (để đơn giản, ta giả sử các
phần tử này là khác nhau từng đôi một). Một cặp phần tử A[i] và A[j] gọi là cặp nghịch
đảo nếu i < j mà A[i] > A[j]. Mảng A có đặc điểm gì để số cặp nghịch đảo là lớn nhất
và số cặp lớn nhất này là bao nhiêu? Câu hỏi tương tự với số cặp nhỏ nhất?
Bài 3. Cài đặt thuật toán sắp xếp chèn theo cách đệ quy và không đệ quy.
Bài 4. Sinh hoán vị của tập 4 phần tử {1, 2, 3, 4} bằng thuật toán: giảm để trị,
Johnson-Trotter, thứ tự từ điển.
Bài 5. Cho thuật toán sinh hoán vị của tác giả B. Heap như sau:
ALGORITHM HeapPermute(n)
//Đầu vào: số nguyên n và mảng A[1..n]
//Đầu ra: tất cả các hoán vị của A.
if (n = 1) then
Print A
else
for i ← 1 to n do
HeapPermute(n − 1)
if (n lẻ) then
swap(A[1], A[n])
else
swap(A[i], A[n])
a. Áp dụng thuật toán trên tìm hoán vị của tập hợp có 2, 3 và 4 phần tử.
b. Chứng minh thuật toán trên cho kết quả đúng.
c. Đánh giá độ phức tạp của thuật toán trên.
Bài 6. Sinh tất cả các tập con của tập A = {a1, a2, a3, a4} dựa vào kĩ thuật giảm để trị
và thuật toán dựa chuỗi bit nhị phân.
Bài 7. Viết thuật toán (giả mã) dựa vào ý tưởng của kĩ thuật giảm để trị tính giá trị an

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 4. Giảm để trị 55

với n là số nguyên dương. Thiết lập và giải công thức đệ quy tính số lượng phép nhân
của thuật toán. Thuật toán này so với thuật Brute-Force như thế nào?
Bài 8. Thiết kế thuật toán đệ quy và không đệ quy sinh ra 2n chuỗi bit nhị phân có độ
dài n.
Bài 9. Thiết kế thuật toán giảm để trị để sinh ra tất cả các tổ hợp k phần tử từ tập n
phần tử.
Bài 10. Cài đặt thuật toán tìm kiếm nhị phân theo cách đệ quy và không đệ quy.

Bài 11. Cho một mảng A[0..n  2] chứa n  1 số nguyên từ 1 đến n theo thứ tự tăng
dần. Trong đó có 1 số nguyên bị thiếu. Thiết kế thuật toán tìm số nguyên bị thiếu đó.
Bài 12. Cài đặt thuật toán sinh hoán vị và thuật toán sinh tập con.
Bài 13. Dựa vào thuật toán tìm kiếm nhị phân, viết chương trình tìm nghiệm gần đúng
của phương trình f(x) = 0 trên đoạn [a, b] theo định lí Role. Định lí Role phát biểu như
sau: cho hàm f(x) liên tục trên đoạn [a, b]; f(a) × f(b) < 0. Khi đó tồn tại ít nhất một số
thực c sao cho: f(c) = 0.

Lê Xuân Việt – Dương Hoàng Huyên


Chương 5. Chia để trị 55

CHƯƠNG 5. CHIA ĐỂ TRỊ

5.1. Giới thiệu


Chia để trị là một kĩ thuật thiết kế thuật toán thông dụng. Ý tưởng chính của kĩ
thuật chia để trị như sau:
1. Chia bài toán ban đầu thành các bài toán con cùng loại với kích thước đầu vào
nhỏ hơn, thông thường các bài toán con có cùng kích thước.
2. Giải các bài toán con này (thông thường dùng đệ quy để giải, hoặc khi bài toán
con đủ nhỏ để giải bằng thuật toán khác).
3. Tổng hợp các nghiệm của các bài toán con để nhận nghiệm của bài toán ban
đầu.
Hình 5-1 thể hiện ý tưởng của kĩ thuật chia để trị.

Bài kích
toán thước n

Bài toán con 1 Bài toán con 2


kích thước n/2 kích thước n/2

Giải bài Giải bài


toán con 1 toán con 2

Tổng hợp nghiệm


của bài toán ban đầu
Hình 5-1. Sơ đồ mô tả kĩ thuật thiết kế thuật toán chia để trị.

Lê Xuân Việt – Dương Hoàng Huyên


56 Phân tích và thiết kế thuật toán

Ví dụ xét bài toán tính tổng các số nguyên a0, …, an  1. Giả sử n lớn, ta có thể
thiết kế thuật toán như sau:
1. Chia bài toán thành hai phần, mỗi phần có n / 2 số nguyên.
2. Tính tổng các số nguyên trên từng phần.
3. Cộng hai giá trị tổng của hai phần để nhận giá trị tổng của dãy số.

5.2. Sắp xếp trộn


Ý tưởng của thuật toán sắp xếp trộn như sau: chia dãy số A[0..n  1] thành hai
phần A[0..n/2  1] và A[n/2..n  1], sắp xếp mỗi phần (bằng đệ quy), sau đó trộn hai
dãy đã sắp xếp thành một dãy sắp xếp. Chi tiết thuật toán như sau:
ALGORITHM Mergesort(A, l, r)
//Đầu vào: mảng A[0..n - 1] n phần tử và hai số nguyên l, r
//Đầu ra: mảng A đã được sắp xếp tăng dần
if (r > l) then
m = (l + r) / 2
Mergesort(A, l, m)
Mergesort(A, m + 1, r)
Merge(A, l, m, r)
Thuật toán trộn mảng A với hai phần, nửa phần tử đầu và nửa phần tử cuối đã sắp
xếp tăng, thành một mảng tăng như sau:
ALGORITHM Merge(A, l, m, r)
//Đầu vào: mảng A với các phần tử từ l → m đã sắp xếp tăng và các
phần tử m + 1 → r đã sắp xếp tăng.
//Đầu ra: mảng A với các phần tử từ l → r đã sắp xếp tăng
//Mảng B[l..r] là mảng phụ
s ← l; t ← m + 1; k ← l
while (s ≤ m) and (t ≤ r) do
if (A[s] ≤ A[t]) then
B[k] ← A[s]; s ← s + 1
else
B[k] ← A[t]; t ← t + 1
k ← k +1
if (s = m + 1) then
for i ← t to r do
B[i] ← A[i]
else
for i ← s to m do
B[k + i] ← A[i]
A ← B
return A
Để đơn giản cho việc đánh giá hiệu quả của thuật toán sắp xếp trộn, giả sử n = 2k.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 5. Chia để trị 57

Công thức đệ quy để tính thời gian chạy t(n) của thuật toán như sau:

t(n) = 2t(n / 2) + ttrộn(n) với t(1) = 0, thế n = 2k ta có: t(2k) = 2t(2k  1) + ttrộn(2k).

Ta thấy thời gian dùng để trộn hai mảng là ttrộn(n) = n  1 phép so sánh, do đó:

t(2k) = 2t(2k  1) + 2k – 1 =

= 2[2t(2k  2) + 2k  1 – 1] + 2k – 1 =

= 22t(2k  2) + 2k – 2 + 2k –1 = 22t(2k  2) + 2×2k – 2 – 1 =

= 22[2t(2k  3) + 2k  2 –1] + 2×2k – 2 – 1 = 23t(2k  3) + 3×2k – 22 – 2 – 1 =


=…

= 2kt(2k  k) + k×2k – 2k  1 … – 2 – 1 = 2kt(1) + k×2k – (2k  1 + … +21 + 20) =


= k×2k – (2k – 1)

Thay k = log2(n) và 2k = n, ta có:

t(n) = n.log2(n) – (n  1) ≤ n.log2(n), vậy t(n)  O(nlogn).


Ví dụ minh họa thuật toán sắp xếp trộn như Hình 5-2.

Hình 5-2. Minh họa thuật toán sắp xếp trộn với 8 phần tử.

Lê Xuân Việt – Dương Hoàng Huyên


58 Phân tích và thiết kế thuật toán

5.3. Sắp xếp nhanh


Ý tưởng sắp xếp nhanh như sau: chọn một phần tử làm ứng viên giả sử là A[s],
chia mảng thành 2 phần sao cho tất cả các phần tử ở bên trái A[s] sẽ nhỏ hơn hoặc
bằng A[s], các phần tử bên phải A[s] sẽ lớn hơn hoặc bằng A[s]. Khi đó phần tử A[s] đã
đúng vị trí. Sau đó ta tiếp tục sắp xếp mảng con bên trái và bên phải độc lập bằng thuật
toán sắp xếp nhanh. Chú ý, trong thuật toán này không cần bước tổng hợp nghiệm. Chi
tiết thuật toán như sau:
ALGORITHM Quicksort(A[l..r])
//Đầu vào: mảng con của mảng A[0..n-1], hai chỉ số l, r
//Đầu ra: mảng A[l..r] đã được sắp xếp tăng
if (l < r) then
s ← HoarePartition(A[l..r]) //s vị trí chia mảng làm 2 phần.
Quicksort(A[l..s − 1])
Quicksort(A[s + 1..r])
return A
Thuật toán chia mảng làm hai phần với phần tử ứng viên p = A[l] là phần tử đầu
tiên của dãy như sau: chọn chỉ số i để quét mảng từ trái qua phải, khi gặp phần tử đầu
tiên lớn hơn p, thì dừng. Chỉ số j quét mảng từ phải qua trái, khi gặp phần tử đầu tiên
nhỏ hơn hoặc bằng p thì dừng. Khi cả hai đều dừng, có ba tình huống xảy ra, nếu i < j
ta hoán đổi A[i] với A[j] và tăng i, giảm j. Minh họa ở hình sau:

Nếu i > j ta hoán đổi đổi vị trí p với A[j] và lúc này mảng đã được chia thành hai
phần, phần bên trái lớn hơn hoặc bằng p, phần bên phải nhỏ hơn hoặc bằng p. Minh
họa ở hình sau:

Nếu i = j, khi đó A[i] = A[j] = p và lúc đó mảng đã được chia. Ta có thể tích hợp
trường hợp này vào trường hợp i > j. Minh họa như hình:

Chi tiết thuật toán chia như sau:


ALGORITHM HoarePartition(A[l..r])

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 5. Chia để trị 59

//Thuật toán chia mảng làm 2 phần lấy phần tử đầu làm ứng viên.
//Đầu vào: mảng con A[l..r] của mảng A[0..n − 1], 2 số l và r (l <
r).
//Đầu ra: mảng A[l..r] đã được chia, trả về vị trí chia.
p ← A[l]; i ← l + 1; j ← r
while (i ≤ j) do
while (A[i] < p) and (i ≤ r) do
i ← i +1
while (A[j] > p) do
j ← j –1
if (i < j) then
swap(A[i], A[j])
swap(A[l], A[j])
return j
Chú ý, thuật toán trên có thể cho chỉ số i vượt qua mảng, ta có thể khắc phục
bằng cách bổ sung phần tử p vào cuối mảng hoặc kiểm tra chỉ số i đã đến cuối mảng
hay chưa. Hình 5-3 thể hiện một ví dụ của thuật toán sắp xếp nhanh.

Hình 5-3. Minh họa thuật toán sắp xếp nhanh 8 phần tử.

Lê Xuân Việt – Dương Hoàng Huyên


60 Phân tích và thiết kế thuật toán

Đánh giá độ phức tạp của thuật toán sắp xếp nhanh tương đối phức tạp. Ta phải
xét thuật toán trong ba trường hợp: tốt nhất, xấu nhất và trung bình.
Trường hợp tốt nhất của thuật toán sắp xếp nhanh là khi chia mảng thành hai
phần mà số phần tử mỗi phần bằng nhau. Số phép so sánh trong bước chia mảng thành
2 phần là n, khi đó số phép so sánh của toàn bộ thuật toán là: t(n) = t(n / 2) + n. Giải
công thức đệ quy này tương tự như trường hợp sắp xếp trộn, ta có độ phức tạp của
thuật toán sắp xếp nhanh sẽ là O(nlogn).
Trường hợp xấu nhất của thuật toán sắp xếp nhanh là khi mảng dữ liệu đầu vào
đã sắp xếp tăng (hoặc giảm). Khi đó thuật toán chia sẽ chia mảng thành 2 phần: một
phần rỗng và một phần còn lại có n  1 phần tử. Tại mỗi bước chia ta cần n + 1 phép
so sánh. Khi đó t(n) = t(n – 1) + (n + 1) với t(1) = 0. Giải công thức đệ quy này như
sau:
t(n) = t(n – 1) + (n + 1) = t(n – 2) + (n) + (n + 1) = t(n – 3) + (n – 1) + (n) + (n +
1) = ... = t(n – (n – 1)) + 3 +...+ (n – 1) + (n) + (n + 1) = (n + 1)(n + 2) − 3  O(n2).
2

Vậy trường xấu nhất thì thuật toán sắp xếp nhanh không nhanh hơn các thuật
toán sắp xếp khác như sắp xếp chọn, sắp xếp nổi bọt và sắp xếp chèn.
Trường hợp trung bình, là trường hợp thường gặp nhất trong thực tế. Khi đó phần
chia có thể xảy ra ở mọi vị trí trong mảng sau khi đã thực hiện n + 1 phép so sánh. Sau
khi chia, giả sử vị trí chia là s, khi đó nửa bên trái có s phần tử, nửa bên phải có n – 1 –
s phần tử. Giả sử phần chia xuất hiện ở vị trí s với xác suất là như nhau bằng 1 / n, ta
có công thức tính t(n) như sau:
t(n) = 1 ∑n –1 [(n + 1) + t(s) + t(n − 1 − s)] với t(0) = 0, t(1) = 0.
n c =0

Ta có thể viết lại công thức:


t(n) = (n + 1) + 1 ∑n – 1[t(s) + t( n − 1 − s)] = (n + 1) + 2 ∑n–1 t(s).
n c =0 n c=0

Nhân hai vế cho n và trừ cho giá trị (n – 1)t(n – 1), ta có:
n. t(n) – (n – 1)t(n – 1) = n(n + 1) + n 2 ∑n – 1 t(s) − (n − 1)t(n − 1) =
n c =0

= n(n + 1) + 2 ∑n – 1 t(s) − (n − 1) [n + 2 ∑n – 2 t(s)] =


c =0 n–1 c =0

= n(n + 1) − n(n − 1) + 2 ∑n – 1 t(s) − 2 ∑n – 2 t(s)=


c=0 c=0

= n(n + 1) − n(n − 1) + 2t(n − 1).

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 5. Chia để trị 61

Vậy n. t(n) – (n – 1)t(n – 1) = n(n + 1) − n(n − 1) + 2t(n − 1). Tiếp tục

biến đổi ta có:

n. t(n) = n(n + 1) − n(n − 1) + 2t(n − 1) + (n – 1)t(n – 1) =

= 2n + (n + 1) t( n − 1).
Chia hai vế cho n(n + 1) ta có:
t(n) t(n – 1) 2
= + .
n+1 n n+1

Lần lượt thế các giá trị t(n  2), t(n  3), ..., t(1) ta có:
t(n) n t(n – 1) 2 t(n – 2) 2 2
= + = + + =
+1 n n+1 n–1 n n +1
2 2 2 2 t(1)
= + +⋯ + + + =
n+1 n 4 3 2
2 1 n1
= 1 + ∑n = + 2∑ n 1
≅ 2 ∑n 1 ≅2∫ dx ≈ 2 ln n.
2 k=2k+1 2 k=3 k k =3 k 1 s

Tóm lại: t(n)  2n.ln(n)  1,39n.log2(n). Vậy với trường hợp trung bình độ phức
tạp của thuật toán sắp xếp nhanh là O(nlogn) và thuật toán chỉ thêm khoảng 39% phép
so sánh so với trường hợp tốt nhất.

5.4. Nhân số nguyên lớn và nhân ma trận


5.4.1. Nhân số nguyên lớn
Một số ứng dụng, chẳng hạn như mật mã, yêu cầu phải nhân các số nguyên rất
lớn, có thể hơn 100 chữ số. Do số nguyên lớn nên không thể biểu diễn bằng kiểu dữ
liệu thông thường của máy tính. Vì vậy cần phải có thuật toán hiệu quả để hai nhân số
nguyên lớn này. Nếu sử dụng nguyên tắc nhân thông thường ta phải mất n2 phép nhân
từng chữ số với n là số chữ số, tức là độ phức tạp là O(n2). Dựa vào kĩ thuật chia để trị,
ta có thể thiết kế một thuật toán có độ phức tạp nhỏ hơn O(n2). Xét ví dụ cụ thể 23×14
như sau:
Các số 23 và 14 có thể biểu diễn lại như sau: 23 = 2 × 101 + 3 × 100 và 14 = 1 ×
101 + 4 × 100, khi đó: 23 × 14 = (2 × 101 + 3 × 100) × (1 × 101 + 4 × 100) = (2 × 1) ×
102 + (2 × 4 + 3 × 1) × 101 + (3 × 4) × 100, có tất cả là 4 phép nhân. Tuy nhiên ta có: 2
× 4 + 3 × 1 = (2 + 3) × (1 + 4)  2 × 1  3 × 4.
Tổng quát, số a = a1a0, b = b1b0, khi đó c = a × b = c2 × 102 + c1 × 101 + c0 × 100
với: c2 = a1 × b1, c0 = a0 × b0, c1 = (a1 + a0) × (b1 + b0)  (c2 + c0).

Lê Xuân Việt – Dương Hoàng Huyên


62 Phân tích và thiết kế thuật toán

Mở rộng trường hợp a, b có n chữ số và n chẵn. Ta chia các chữ số của a và b


thành hai phần, nửa đầu của a là a1 và nửa sau của a là a0, tương tự b1 và b0, mỗi phần
có n / 2 chữ số. Tức là a = a1a0 = a1 × 10n / 2 + a0, tương tự b = b1b0 = b1 × 10n / 2 + b0.
Ta có:
c = a × b = (a1 × 10n / 2 + a0) × (b1 × 10n / 2 + b0) = a1 × b1 × 10n + (a1 × b0 + a0 ×
b1) × 10n / 2 + a0 × b0 = c2 × 10n + c1 × 10n / 2 + c0. Với c2 = a1 × b1, c0 = a0 × b0, c1 = (a1
+ a0) × (b1 + b0) – (c2 + c0).
Nếu n = 2k, ta có thuật toán đệ quy để tính tích 2 số có n chữ số, mỗi lần gọi đệ
quy số chữ số của mỗi số được tính tích sẽ giảm một nữa, thuật toán sẽ dừng khi n = 1.
Để đánh giá độ phức tạp của thuật toán nhân hai số nguyên lớn, ta thấy phép toán
cơ bản của thuật toán là phép nhân, kích thước dữ liệu đầu vào là n (số chữ số) và công
thức đệ quy để tính số phép nhân như sau:

t(n) = 3t(n / 2) và t(1) = 1, giả sử n = 2k ta có: t(2k) = 3.t(2k  1) = 32.t(2k  2) = ... =


3k.t(1). Do k = log2(n) nên: t(n) = 3log2 n = nlog2 3 ≈ n1.585.

5.4.2. Phương pháp nhân ma trận Strassen


Tương tự như thuật toán nhân hai số nguyên lớn ta có phương pháp nhân hai ma
trận theo kĩ thuật chia để trị. Bắt đầu với ví dụ đơn giản là nhân hai ma vuông cấp 2 ×
2 như sau:
c c01 a a01 b00 b01
[ 00 ] = [ 00 ]× [ ]=
c10 c11 a10 a11 b10 b11
N1 + N4 − N5 + N7 N3 + N5
[ N2 + N4 N1 + N3 − N2 + N6
].

Trong đó:
m1 = (a00 + a11) × (b00 + b11),
m2 = (a10 + a11) × b00,
m3 = a00 × (b01 − b11),
m4 = a11 × (b10 − b00),
m5 = (a00 + a01) × b11,
m6 = (a10 − a00) × (b00 + b01),
m7 = (a01 − a11) × (b10 + b11).
Để nhân ma trận theo cách này ta cần 7 phép nhân và 18 phép cộng trừ, trong khi
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 5. Chia để trị 63

đó với thuật toán thông thường dựa vào định nghĩa ta cần 8 phép nhân và 4 phép cộng.
Mở rộng trường hợp nhân hai ma trận cấp n × n với n = 2k (nếu n ≠ 2k ta có thể
thêm các dòng, cột số 0 vào cho đủ). Ta chia ma trận A, B, C thành 4 ma trận con mỗi
ma trận con cấp n/2 × n/2 như sau:
C C01 A A01 B00 B01
[ 00 ] = [ 00 ]× [ ].
C10 C11 A10 A11 B10 B11
Trong đó các ma trận C00, C01, C10, C11 được tính trực tiếp hoặc tương tự như
nhân hai ma trận cấp 2 × 2 ở trên. Ví dụ C00 = A00 × B00 + A01 × B10 hoặc C00 = M1 + M4
 M5 + M7 với các giá trị M1, M2, ..., M7 được tính như công thức ở trên. Tiếp tục tính
7 tích các ma trận M1, M2, ..., M7 bằng phương pháp tương tự ta có ma trận tích cần tìm.
Đánh giá hiệu quả của thuật toán Strassen nhân hai ma trận: ta thấy phép toán cơ
bản của thuật toán là phép nhân, kích thước dữ liệu đầu vào là n (cấp của ma trận), và
công thức tính số phép nhân là:
 1  2
t(n) = 7.t(n / 2), t(1) = 1, giả sử n = 2k, ta có t(2k) = 7.t(2k ) = 7.7.t(2k ) ... =
7k.t(1), do k = log2(n) nên: t(n) = 7log2 n = nlog2 7 ≈ n2.807.

5.5. Bài tập


Bài 1. Viết thuật toán (giả mã) dựa vào ý tưởng của kĩ thuật chia để trị tìm vị trí của
phần tử lớn nhất trong mảng có n phần tử. Kết quả của thuật toán là gì nếu trong mảng
có nhiều hơn 1 phần tử lớn nhất? Thiết lập và giải công thức đệ quy để tính số lượng
phép so sánh trong thuật toán. Thuật toán này so với thuật toán Brute-Force như thế
nào?
Bài 2. Viết thuật toán (giả mã) dựa vào ý tưởng của kĩ thuật chia để trị kiểm tra một số
x bất kì có trong dãy n số hay không? Kết quả của thuật toán là gì nếu trong dãy có
nhiều hơn 1 phần tử giống x? Thiết lập công thức đệ quy tính số phép toán so sánh
trong thuật toán. Thuật toán này so với thuật toán Brute-Force như thế nào?
Bài 3. Viết thuật toán (giả mã) dựa vào ý tưởng của kĩ thuật chia để trị tính giá trị an
với n là số nguyên dương. Thiết lập và giải công thức đệ quy tính số lượng phép nhân
của thuật toán. Thuật toán này so với thuật toán Brute-Force như thế nào?

Bài 4. Cho A[0..n  1] là dãy n số thực. Một cặp phần tử (A[i], A[j]) được gọi là nghịch
đảo nếu i < j và A[i] > A[j]. Thiết kế thuật toán chia để trị đếm số cặp nghịch đảo này.
Đánh giá độ phức tạp của thuật toán vừa thiết kế.
Bài 5. Thiết kế một thuật toán sắp xếp lại các phần tử của một mảng n phần tử sao cho

Lê Xuân Việt – Dương Hoàng Huyên


64 Phân tích và thiết kế thuật toán

tất cả các số âm đứng trước các số dương. Đánh giá độ phức tạp của thuật toán vừa
thiết kế.
Bài 6. Có n người và n đôi giày với kích thước các đôi chân và các đôi giày khác nhau,
tuy nhiên n người này chắc chắn vừa với n đôi giày đã cho. Ta chỉ có một cách thử
người với giày và từ đó đưa ra kết luận giày lớn hơn, giày nhỏ hơn hoặc giày vừa với
chân. Tuy nhiên ta không có cách so sánh hai chân với nhau hoặc hai đôi giày với
nhau. Thiết kế thuật toán để tìm giày tương ứng cho từng người sao cho số phép thử
giày là ít nhất có thể.
Bài 7. Cài đặt thuật toán sắp xếp trộn bằng ngôn ngữ lập trình bất kì.
Bài 8. Cài đặt thuật toán sắp xếp nhanh bằng ngôn ngữ lập trình bất kì.
Bài 9. Cần tổ chức lịch thi đấu có n người tham gia, mỗi người thi đấu một lần với một
đối thủ, mỗi người phải thi đấu nhiều nhất một trận trong ngày. Hãy đưa ra lịch thi đấu
sao cho cuộc thi kết thúc sau n  1 ngày.
Hướng dẫn: chia n người thành hai phần bằng nhau giả sử là A, B. Xếp lịch thi
đấu độc lập cho hai phần, cuối cùng xếp lịch thi đấu cho 1 người ở phần A đấu với 1
người ở phần B.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 6. Biến đổi để trị 65

CHƯƠNG 6. BIẾN ĐỔI ĐỂ TRỊ

6.1. Giới thiệu


Biến đổi để trị là kĩ thuật thiết kế thuật toán theo hai giai đoạn. Giai đoạn biến
đổi, bài toán sẽ được biến đổi về dạng dễ dàng hơn để giải. Giai đoạn tiếp theo là giải
bài toán sau khi đã biến đổi.
Kĩ thuật này chia thành ba dạng chính sau:
1. Biến đổi về dạng đơn giản hơn hoặc thuận tiện hơn.
2. Biến đổi thành biểu diễn khác của bài toán.
3. Biến đổi bài toán về dạng khác mà đã có thuật toán để giải.

6.2. Biến đổi bằng sắp xếp


6.2.1. Bài toán phần tử duy nhất
Bài toán kiểm tra phần tử duy nhất trong mảng. Bài toán này có cách giải đơn
giản theo kĩ thuật Brute-Force là quét các cặp phần tử trong mảng cho đến khi hoặc là
có 1 cặp phần tử bằng nhau hoặc là không có. Độ phức tạp trong trường hợp xấu nhất
là O(n2). Mặc khác ta cũng có thể sắp xếp mảng và sau đó chỉ kiểm tra các phần tử kề
nhau. Chi tiết thuật toán như sau:
ALGORITHM PresortElementUniqueness(A[0..n − 1])
//Đầu vào: Mảng A[0..n−1] các phần tử đã sắp xếp
//Đầu ra: Trả về “true” nếu A không có cặp phần tử bằng nhau, ngược
lại trả về “false”
Sắp xếp mảng A
for i ← 0 to n − 2 do
if (A[i] = A[i + 1]) then
return false
return true
Thời gian chạy của thuật toán này là tổng thời gian sắp xếp và kiểm tra các phần
tử kề nhau. Do đó ta cần n.logn phép so sánh để sắp xếp và n  1 phép so sánh để kiểm
tra. Thời gian sắp xếp sẽ là thời gian chạy của cả thuật toán, vì vậy nếu ta sử dụng
thuật toán sắp xếp có độ phức tạp O(n2) thì thuật toán này không hiệu quả hơn kĩ thuật
Lê Xuân Việt – Dương Hoàng Huyên
66 Phân tích và thiết kế thuật toán

Brute-Force. Tuy nhiên nếu ta sử dụng thuật toán sắp xếp có độ phức tạp là O(nlogn)
như MergeSort, QuickSort thì thuật toán trên có độ phức tạp như sau:

t(n) = tsort(n) + tscan(n)  O(nlogn) + O(n)  O(nlogn).

6.2.2. Phần tử xuất hiện nhiều nhất


Tìm phần tử xuất hiện nhiều nhất trong dãy. Thuật toán theo kĩ thuật Brute-Force
sẽ quét tất cả các phần tử của dãy, sau đó lưu phần tử đã xuất hiện cùng với số lần xuất
hiện của từng phần tử trong một danh sách riêng.
Tuy nhiên, ta cũng có cách khác để tìm phần tử xuất hiện nhiều nhất, đầu tiên sắp
xếp dãy, khi đó các phần tử giống nhau sẽ nằm liền kề nhau. Để tìm phần tử xuất hiện
nhiều nhất ta chỉ cần tìm dãy con mà các phần tử giống nhau dài nhất. Thuật toán chi
tiết như sau:
ALGORITHM PresortMode(A[0..n − 1])
//Đầu vào: mảng A[0..n − 1] các phần tử đã sắp xếp
//Đầu ra: giá trị xuất hiện nhiều nhất
Sắp xếp mảng A
i ← 0
modefrequency ← 0
while (i ≤ n – 1) do
runlength ← 1
runvalue ← A[i]
while (i + runlength ≤ n – 1) and (A[i + runlength] = runvalue)
do
runlength ← runlength + 1
if (runlength > modefrequency) then
modefrequency ← runlength
modevalue ← runvalue
i ← i + runlength
return modevalue

6.3. Phép khử Gaussian


6.3.1. Giải hệ phương trình đại số tuyến tính
Cho hệ phương trình sau:
a11x1 + a12x2 + ... + a1nxn = b1
a21x1 + a22x2 + ... + a2nxn = b2
...
an1x1 + an2x2 + ... + annxn = bn

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 6. Biến đổi để trị 67

Có nhiều cách để giải hệ phương trình này, một trong những cách đó là biến đổi
hệ phương trình này về hệ phương trình khác tương đương (có cùng nghiệm) mà các
hệ số nằm ở dưới đường chéo chính của ma trận bằng 0. Cụ thể như sau:
a11x1 + a12x2 + ... + a1nxn = b1
a21x1 + a22x2 + ... + a2nxn = b2
...
an1x1 + an2x2 + ... + annxn = bn
Biến đổi về dạng:
au x1 + au x2 + ⋯ + au xn = bu
11 12 1n 1
au x 2 + ⋯ + a u x n = b u
22 1n 2
… u u
a x =b
nn n n

Ta có thể viết hệ phương trình này dưới dạng ma trận như sau:
Ax = b  A'x = b'
Trong đó:
a11 a12 … a1n b au au … au bu
a21 a22 … a2n 1
b2 ⎡ 011 au12 1n
… au ⎤ bu
1
2n⎥
A = [… ], b = [ ], Au = ⎢ 22
, bu = [ 2]
… ⎢ … ⎥ …
an1 an2 … ann bn 0 0 … a nn⎦
u b nu


Rõ ràng hệ phương trình sau khi biến đổi thành A'x = b' dễ giải hơn hệ phương
trình ban đầu bởi vì ta chỉ cần tìm xn từ phương trình thứ n, sau đó thế xn vào phương
trình thứ n  1 để tìm xn  1, ... tiếp tục thay thế cho đến phương trình đầu tiên để tìm x1.
Khi biến đổi hệ phương trình Ax = b về hệ phương trình A'x = b' ta dựa vào một
số tính chất sau:
+ Có thể thay đổi vị trí của hai phương trình bất kì trong hệ phương trình.
+ Nhân một số khác 0 vào một phương trình bất kì để tạo ra một phương trình
mới trong hệ.
+ Thay thế một phương trình trong hệ bằng một phương trình mới là tổng hoặc
hiệu của phương trình này với một phương trình khác trong hệ.
Để tạo ra hệ phương trình A'x = b' từ hệ phương trình Ax = b ta thực hiện các
bước sau đây:
Lê Xuân Việt – Dương Hoàng Huyên
68 Phân tích và thiết kế thuật toán

Sử dụng hệ số a11 để tạo ra các hệ số x1 của các phương trình 2 đến phương trình
n bằng 0. Cụ thể ta thay thế phương trình 2 bằng hiệu của nó với phương trình 1 nhân
với a21/a11, khi đó ta nhận được phương trình mới với hệ số x1 của phương trình 2 bằng
0. Tương tự với các phương trình 2, 3, ..., n ta nhận được các phương trình mới với hệ
số x1 bằng 0.
Sau đó ta khử các hệ số x2 của các phương trình 3, 4, ..., n bằng cách lấy hiệu của
phương trình đó với phương trình 2 nhân với hệ số tương ứng. Tương tự, ta khử hệ số
x3, x4, ..., xn  1 sẽ nhận được hệ phương trình mới với các hệ số nằm phía dưới ma trận
đường chéo chình bằng 0. Ví dụ giải hệ phương trình như sau:

2x1  x2 + x3 = 1

4x1 + x2  x3 = 5
x1 + x2 + x3 = 0
Chi tiết biến đổi như sau:
2 −1 1 1
[4 1 −1 5], thay thế phương trình 2 bằng cách lấy phương trình 2 – 4/2
1 1 1 0
phương trình 1, thay thế phương trình 3 bằng cách lấy phương trình 3 – 1/2 phương
trình 1 ta có:
2 −1 1 1
[0 3 −3 3 ], tiếp tục thay thế phương trình 3 bằng cách lấy phương trình
3 1 —1
0 2 2 2

3 – ½ phương trình 2, ta có:


2 −1 1 1
[0 3 −3 3], từ hệ phương trình ta có thể tìm được x3, x2, x1 như sau:
0 0 2 −2
x3 = 2 / 2 = 1, x2 = (3  (3x3)) / 3 = 0, x1 = (1  (x3 + x2)) / 2 = 1.
Chi tiết thuật toán như sau:
ALGORITHM ForwardElimination(A[1..n, 1..n], b[1..n])
//Đầu vào: ma trận A[1..n,1..n] và vecto cột b[1..n]
//Đầu ra: ma trận A với các phần tử ở dưới đường chéo chính bằng 0
for i ← 1 to n do
A[i, n + 1] ← b[i] //bổ sung b vào cột cuối của ma trận A
for i ← 1 to n − 1 do
for j ← i + 1 to n do
for k ← i to n + 1 do
A[j, k] ← A[j, k] − A[i, k] ∗ A[j, i] / A[i, i]
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 6. Biến đổi để trị 69

return A

6.3.2. Tính ma trận nghịch đảo


Phương pháp khử Gaussian cũng được sử dụng để tính ma trận nghịch đảo trong
đại số tuyến tính. Ma trận nghịch đảo của ma trận A cấp n × n, kí hiệu A-1, có tính chất:
A.A-1 = I, trong đó I là ma trận đơn vị (ma trận mà các phần tử ở đường chéo
chính bằng 1 và các phần tử còn lại bằng 0). Chú ý, không phải ma trận vuông nào
cũng có ma trận nghịch đảo, nhưng nếu có thì ma trận nghịch đảo là duy nhất. Nếu ma
trận không có nghịch đảo, ta gọi là ma trận suy biến.
Để biết ma trận vuông có ma trận nghịch đảo hay không, ta dựa vào tính chất
sau: ma trận vuông không có ma trận nghịch đảo khi và chỉ khi tồn tại một dòng nào
đó là tổ hợp tuyến tính của những dòng khác. Một cách khác để kiểm tra ma trận
vuông có nghịch đảo hay không đó là áp dụng phép khử Gaussian để biến đổi ma trận
về dạng “tam giác trên” (ma trận các phần tử nằm dưới đường chéo chính bằng không)
và sau đó kiểm tra các phần tử ở đường chéo chính, nếu tất cả các phần tử đều khác
không thì ma trận có nghịch đảo, ngoài ra thì ma trận không có nghịch đảo.
Để tìm ma trận nghịch đảo X của ma trận A, ta dựa vào định nghĩa để tính n2
phần tử xij như sau:
a11 a12 … a1n x11 x12 … x1n 1 0 … 0
a21 a22 … a2n x21 x22 … x2n … 0 ].
AX = I ↔ [ ][ ] = [0 1
⋮ ⋮ ⋮
an1 an2 … ann xn1 xn2 … xnn 0 0 … 1
Ta có thể tìm các phần tử xij bằng cách giải n hệ phương trình đại số tuyến tính.
Cụ thể để tìm cột 1 của ma trận X, ta giải hệ phương trình:
a11 a12 … a1n x11 1
a21 a22 … a2n x21 0
[ ] [ ] = [ ] ↔ Ax1 = e1, trong đó x1 là cột thứ 1 của ma
⋮ ⋮ ⋮
an1 an2 … ann xn1 0
trận X, e1 là cột thứ 1 của ma trận đơn vị I. Tổng quát, để tìm cột thứ j của ma trận X,
ta giải hệ phương trình: Axj = ej.

6.3.3. Tính định thức


Một ứng dụng khác của phép khử Gaussian là tính định thức của ma trận vuông.
Định thức của ma trận vuông A cấp n × n, kí hiệu det(A) hoặc |A| là một số thực được
tính bằng công thức đệ quy như sau: nếu n = 1, tức là ma trận A chỉ có 1 phần tử a11 và
det(A) = a11, nếu n > 1 det(A) được tính như sau:
Lê Xuân Việt – Dương Hoàng Huyên
70 Phân tích và thiết kế thuật toán

det(A) = ∑n j=1 −1j – 1 a1j ) × det(Aj) trong đó a1j là phần tử ở dòng 1 cột j, Aj là
ma trận cấp (n  1) × (n  1) nhận được từ ma trận A bằng cách xóa đi dòng 1 cột j. Ví
dụ ma trận A cấp 2 × 2, khi đó:
a11 a12
det ([ ]) = a × det([a ]) − a × det([a ]) = a a −a a ,
a21 a22 11 22 12 21 11 22 12 21

Khi ma trận A cấp 3 × 3, ta có:


a11 a12 a13 a22 a23 a21 a23
det ([a21 a33]) − a12det ([a31
a22 a23]) = a11det ([
a32 a33]) +
a21 a31 a22a32 a 33
a det ([ ])= a a a + a a a + a a a  a a a  a a a 
13 a31 a32 11 22 33 12 23 31 13 21 32 11 23 32 12 21 33

a13a22a31.
Để tính định thức bằng công thức đệ quy như trên, ta phải tính n! số hạng, ví dụ n
= 3 ta phải tính 6 lần tích các phần tử. Tuy nhiên khi sử dụng phép khử Gaussian, ta
biến đổi ma trận về dạng “tam giác trên”, lúc đó định thức của ma trận A là tích các
phần tử ở đường chéo chính. Rõ ràng thuật toán có độ phức tạp O(n3).

6.4. Heap và HeapSort


6.4.1. Định nghĩa Heap
Heap là một cấu trúc dữ liệu quan trọng, được định nghĩa là cây nhị phân có hai
tính chất sau:
+ Tại nút bất kì của cây luôn luôn có hai nhánh, ngoại trừ nút cuối cùng.
+ Giá trị tại nút cha luôn lớn hơn hoặc bằng nút con.
Một vài tính chất của Heap như sau:
1. Tồn tại duy nhất một cây nhị phân đầy đủ với n nút có độ cao là O(logn)
2. Gốc của cây nhị phân này luôn chứa giá trị lớn nhất.
3. Mỗi một nhánh bất kì của cây nhị phân này cũng là một Heap.
4. Một Heap có thể được biểu diễn trong một mảng bằng cách lưu các phần tử
theo trật tự từ trên xuống và từ trái qua phải. Cụ thể như sau:

a. Các nút cha sẽ được lưu trong mảng ở n / 2 vị trí đầu tiên, các nút lá sẽ được
lưu ở n / 2 vị trí sau cùng
b. Tại phần tử thứ i, các nút con lần lượt là 2i và 2i + 1.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 6. Biến đổi để trị 71

Như vậy, ta có thể lưu Heap trong mảng H[1..n], trong đó mỗi phần tử ở vị trí i
trong nửa đầu của mảng sẽ lớn hơn hoặc bằng các phần tử 2i và 2i + 1, tức là:

H[i] ≥ Max{H[2i], H[2i + 1]} với 1 ≤ i ≤ n / 2.

Hình 6-1. Minh họa cách biểu diễn Heap bằng mảng.
Để xây dựng được Heap từ danh sách các phần tử, ta dựa vào một trong hai cách:.
bottom-up hoặc top-down.
Thuật toán xây dựng Heap từ dưới lên (bottom-up). Khởi tạo cây nhị phân đầy đủ
với trật tự các phần tử như ban đầu. Tiếp theo, bắt đầu với nút cha cuối cùng, kiểm tra
có hay không giá trị một trong hai nút lá của nó lớn hơn, nếu có đổi vị trí của nút cha
với nút con này. Tiếp tục cho các nút cha còn lại trong danh sách cho đến khi gặp nút
cha cuối cùng (nút gốc). Để hiểu rõ hơn thuật toán này, xét ví dụ cụ thể với các bước
thực hiện như Hình 6-2. Chi tiết thuật toán như sau:
ALGORITHM HeapBottomUp(H[1..n])
//Đầu vào: mảng H[1..n]
//Đầu ra: một heap H[1..n]
for i ← n / 2 downto 1 do
k ← i
v ← H[k]
heap ← false
while (not heap) and (2 ∗ k ≤ n) do
j ← 2 ∗ k
if (j < n) then //có hai nút con
if (H[j] < H[j + 1]) then
j ← j +1
if (v ≥ H[j]) then
heap ← true
else
H[k] ← H[j]; k ← j
H[k] ← v
return H
Để đánh giá hiệu quả của thuật toán tạo Heap này, ta giả sử n = 2k  1, khi đó
Heap là cây nhị phân đầy đủ. Đặt h là chiều cao của cây nhị phân, khi đó h = log2(n).

Lê Xuân Việt – Dương Hoàng Huyên


72 Phân tích và thiết kế thuật toán

trong trường hợp xấu nhất, mỗi nút cha ở mức i của cây, ta phải duyệt h nút lá. Do mỗi
phép di chuyển đến nút tiếp theo cần phải có hai phép so sánh, nên số phép so sánh của
nút cha tại mức i của cây là 2(h  i), vậy tổng số phép so sánh của thuật toán trong
trường hợp xấu nhất là:
Cworct (n) = ∑h–1 ∑Nức i 2(ℎ − i) = ∑h–1 2( ℎ − i) 2i = 2(n − log 2( n − 1)).
i=0 i=0

Hình 6-2. Minh họa cách tạo Heap theo kĩ thuật bottom-up.
Một thuật toán khác tạo Heap là chèn phần tử tiếp theo vào danh sách đã có cấu
trúc là Heap, đây là kĩ thuật top-down. Thuật toán như sau: đầu tiên bổ sung nút mới
giá trị K vào sau nút lá cuối cùng của cấu trúc Heap hiện tại, sau đó đẩy K đến vị trí
thích hợp bằng cách so sánh K với giá trị nút cha, nếu nút cha lớn hơn hoặc bằng K thì
dừng, ngoài ra đổi giá trị K với nút cha và so sánh với nút cha mới. Tiếp tục đổi vị trí
cho đến khi K không lớn hơn nút cha nào hoặc K đã đến gốc.

Hình 6-3. Chèn số 10 vào Heap theo kĩ thuật top-down.


Số phép so sánh của thuật toán chèn này không vượt quá độ cao của cây, do đó
độ phức tạp O(logn).
Để xóa một phần tử trong Heap, ta thực hiện như sau:
1. Hoán đổi nút cần xóa với nút lá cuối cùng (giả sử là K).
2. Giảm kích thước của Heap xuống 1.
3. Đẩy K đến vị trí thích hợp bằng kĩ thuật bottom-up.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 6. Biến đổi để trị 73

Hình 6-4. Minh họa xóa nút gốc trong Heap.

6.4.2. Heap sort


Thuật toán thực hiện qua hai bước sau:
1. Xây dựng Heap từ mảng đã cho.

2. Xóa nút gốc n  1 lần.


Thuật toán xây dựng Heap có độ phức tạp là O(n), bây giờ ta đánh giá thuật toán
sắp xếp Heap ở bước thứ hai, xóa nút gốc n  1 lần. Giả sử C(n) là số phép so sánh cần
thực hiện để xóa phần tử cuối của Heap với kích thước giảm từ n xuống 2. Ta có:

C(n) ≤ 2⌊ log 2(n − 1)⌋ + 2⌊ log 2(n − 2)⌋ + ⋯ + 2⌊ log 2 (1)⌋ ≤ 2 ∑n–1 logi=12 i ≤ 2 ∑n–1
(n − 1) = 2(n − 1) log2 (n − 1) ≤ 2n log2 n  O(n log n) . Vậy độ phức tạp của thuật
log2i=1
toán Heapsort là O(n) + O(nlogn) = O(nlogn). Hình 6-5 minh họa thuật toán Heapsort.
Hình 6-5. Ví dụ minh họa thuật toán Heapsort.

Lê Xuân Việt – Dương Hoàng Huyên


74 Phân tích và thiết kế thuật toán

6.5. Quy tắc Horner và số mũ nhị phân


6.5.1. Quy tắc Horner
Quy tắc Horner là một thuật toán rất hiệu quả để tính giá trị của đa thức, nó được
đặt tên theo nhà toán học người Anh W. G. Horner (ông đã công bố quy tắc này vào
n  1
cuối thế kỉ 19). Quy tắc Horner biểu diễn lại đa thức: P(x) = anxn + an  1x +…+
a1x + a0 thành đa thức với bậc giảm dần như sau: P(x) = (...(anx + an  1)x + ... a0).

Ví dụ: P(x) = 2x4  x3 + 3x2 + x  5, ta có thể biểu diễn lại thành đa thức với bậc
giảm dần như sau:

P(x) = 2x4  x3 + 3x2 + x  5

= x(2x3  x2 + 3x + 1)  5

=x(x(2x2 – x + 3) + 1)  5

=x(x(x(2x  1) + 3) + 1)  5
Một cách khác để hiểu rõ hơn quy tắc này, ta tổ chức một bảng gồm 2 dòng,
dòng 1 là các hệ số của đa thức an, an  1, ..., a0, dòng thứ hai được tính từ trái qua phải
như sau: giá trị hiện tại bằng giá trị của x nhân với giá trị trước đó cộng với giá trị
tương ứng ở dòng 1. Giá trị cuối cùng của dòng hai chính là giá trị của đa thức. Ví dụ
cụ thể như sau:

P(x) = 2x4  x3 + 3x2 + x  5 với x = 3

Hệ số ai 2 1 3 1 5
x=3 2 3×21=5 3 × 5 + 3 = 18 3 × 18 + 1 = 55 3 × 55  5 = 160
Chi tiết thuật toán như sau:
ALGORITHM Horner(P[0..n], x)
//đầu vào: mảng P[0..n] chứa hệ số của đa thức bậc n, số x
//Đầu ra: giá trị của đa thức tại x
p ← P[n]
for i ← n − 1 downto 0 do
p ← x ∗ p + P[i]
return p
Số lượng phép nhân M(n) và phép cộng A(n) của thuật toán trên là:

M(n) = A(n) = ∑0 i=n–1 1 = n.


Quy tắc Horner cũng có một vài ứng dụng phụ, các giá trị trung gian trong quá
trình tính P(x) tại x0 chính là hệ số của đa thức thương của phép chia P(x) cho (x  x0).

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 6. Biến đổi để trị 75

Cụ thể ví dụ trên, đa thức (2x4  x3 + 3x2 + x – 5) ÷ (x  3) = 2x3 + 5x2 + 18x + 55.

6.5.2. Hàm mũ nhị phân


Quy tắc Horner sẽ không hiệu quả nếu sử dụng để tính an hay đúng hơn là tính xn
với x = a. Do việc tính an (thực tế là tính an mod m) là thao tác cơ bản trong một số
ứng dụng quan trọng trong lí thuyết mật mã, nên ta xét riêng hai thuật toán tính an dựa
vào ý tưởng biểu diễn lại. Cả hai phương pháp này dựa vào biểu diễn nhị phân của số n.
Đặt n = bI...bi...b0 là biểu diễn nhị phân của n, khi đó n có thể biểu diễn bằng đa
thức sau: p(x) = bIxI + ... + bixi + ... + b0 tại x = 2.
Ví dụ n = 13 biểu diễn nhị phân của 13 là 1101, ta có 13 = p(x = 2) = 1 × 23 + 1 ×
22 + 0 × 21 + 1 × 20. Ta sẽ tính đa thức này theo quy tắc Horner và tương tự tính
I i
an = ap(2) = abI2 + … + bi2 + … + b0.
Quy tắt Horner tính p(2) Quy tắt Horner tính an = ap(2)
p ← 1 ap ← a1
for i ← I - 1 downto 0 do for i ← I - 1 downto 0 do
p ← 2p + bi ap ← a2p + bi
2p + bi 2p bi p 2 bi ( ap) 2 nếu bi = 1
Chú ý: a = a × a = (a ) × a = { .
( ap) 2 × a nếu bi = 0
Chi tiết thuật toán như sau:
ALGORITHM LeftRightBinaryExponentiation(a, b[0..I])
//Đầu vào: Số a và danh sách b[0..I] biểu diễn nhị phân của số n
//Đầu ra: giá trị an
product ← a
for i ← I − 1 downto 0 do
product ← product ∗ product
if (b[i] = 1) then
product ← product ∗ a
return product
Ví dụ tính a13 theo thuật toán trên như sau:
Biểu diễn nhị phân của 13 b3 = 1 b2 = 1 b1 = 0 b0 = 1
Tích (product) a a × a = a3
2
(a3)2 = a6 (a ) × a = a13
6 2

Do thuật toán chỉ có 1 hoặc 2 phép nhân trong mỗi lần lặp, nên tổng số phép
nhân để tính an là: I ≤ M(n) ≤ 2I với I là độ dài chuỗi nhị phân biểu diễn n (I = log2 n).
Cách tính trên gọi là cách tính từ trái qua phải, ta cũng có thể tính an từ phải qua trái
I i I i
như sau: an = ap(2) = abI2 + … + bi2 + … + b0 = abI2 × … × abi2 × … × ab0.

Lê Xuân Việt – Dương Hoàng Huyên


76 Phân tích và thiết kế thuật toán

với: a bi2i = { a nếu b i= 1


2i

1 nếu bi = 0
ALGORITHM RightLeftBinaryExponentiation(a, b[0..I]))
//Đầu vào: Số a và danh sách b[0..I] biểu diễn nhị phân của n
//Đầu ra: giá trị an
term ← a
if (b[0] = 1) then
product ← a
else
product ← 1
for i ← 1 to I do
term ← term ∗ term
if (b[i] = 1) then
product ← product ∗ term
return product
Ví dụ tính a13 từ phải qua trái như sau:

Biểu diễn nhị phân của 13 b0 = 1 b1 = 0 b2 = 1 b3 = 1


temp a a × a = a2 a × a2 = a4
2
a × a4 = a8
4

Tích (product) a a a × a4 = a5 a5× a8 = a13

6.6. Bài tập


Bài 1. Thiết kế 2 thuật toán tìm hai số gần nhau nhất trong dãy n số thực, một thuật
toán dựa vào sắp xếp và một thuật toán dựa vào kĩ thuật Brute-Force. Đánh giá độ
phức tạp của hai thuật toán trên. (Khoảng cách giữa hai số x và y là |x  y|).
Bài 2. Cho hai tập A = {a1, a2, ..., an} và B = {b1, b2, ..., bm}. Xét thuật toán tìm tập
giao của hai tập trên, tức là tìm tập C mà các phần tử của nó xuất hiện trong cả hai tập
A và B. Thiết kế hai thuật toán để tìm tập C dựa vào kĩ thuật Brute-Force và kĩ thuật
sắp xếp.
Bài 3. Có n hóa đơn tiền điện và m tấm séc (m ≤ n) trả tiền cho các hóa đơn đó. Mỗi
tấm séc chỉ trả cho 1 hóa đơn tiền điện. Thiết kế thuật toán tìm những hóa đơn chưa
cho có tấm séc nào trả tiền.
Bài 4. Cho một mảng gồm n số thực và một số nguyên s, tìm trong mảng có hay không
hai phần tử mà có tổng bằng s. Thiết kế hai thuật toán để thực hiện yêu cầu trên, đánh
giá độ phức tạp của từng thuật toán vừa thiết kế.
Bài 5. Trong kì thi tuyển sinh đại học, sau khi đã có danh sách thí sinh trúng tuyển.
Thí sinh muốn học trường nào phải nộp bản gốc phiếu báo điểm cho trường cần học.
Giả sử trường A có n thí sinh trúng tuyển, tuy nhiên chỉ có m (m ≤ n) thí sinh nộp bản
gốc. Hãy đưa ra thuật toán tìm ra những thí sinh không nộp bản điểm gốc (tức là
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 6. Biến đổi để trị 77

những thí sinh trúng tuyển trường A nhưng không học). Đánh giá độ phức tạp của
thuật toán đó.
Bài 6. Cho tập n điểm trong mặt phẳng (n ≥ 3). Hãy kết nối các điểm này để tạo ra một
đa giác đơn đóng sao cho các cạnh không cắt nhau. Ví dụ như hình sau:

a. Bài toán này luôn luôn có nghiệm? và nó luôn luôn có nghiệm duy nhất?
b. Thiết kế thuật toán hiệu quả chấp nhận được để giải quyết bài toán này?
Bài 7. Cho một danh sách n khoảng mở (a1, b1), (a2, b2), ..., (an, bn) trên đường thẳng
thực. Chỉ ra nhiều nhất các khoảng có điểm chung nhau. Ví dụ cho 4 khoảng (1, 4); (0,
3); (1.5, 2); (3.6, 5) khi đó số khoảng nhiều nhất chứa điểm chung là 3: (1, 4); (0, 3);
(1.5, 2);
Bài 8. Cho n số nguyên dương phân biệt và n hộp với các dấu >, < chèn vào giữa hai
hộp. Thiết kế thuật toán đặt các số này vào hộp để thỏa bất đẳng thức đã cho. Ví dụ 4,
6, 3, 1, 8 ta có thể đặt vào 5 hộp như sau:

Bài 9. Thiết kế thuật toán tìm tập các từ đảo chữ cái trong Tiếng Anh, ví dụ eat, ate,
tea, ... Viết chương trình cài đặt thuật toán trên.

Lê Xuân Việt – Dương Hoàng Huyên


Chương 7. Quy hoạch động 79

CHƯƠNG 7. QUY HOẠCH ĐỘNG

7.1. Giới thiệu


Quy hoạch động là cách giải quyết các bài toán mà có các bài toán con trùng lặp.
Những bài toán con này phát sinh từ mối liên hệ đệ quy giữa nghiệm của bài toán đang
xét với nghiệm của các bài toán cùng loại với kích thước nhỏ hơn. Quy hoạch động sẽ
tránh trường hợp giải lại các bài toán con này nhiều lần bằng cách giải một lần và lưu
lại kết quả của các bài toán con trong một bảng để sử dụng lại.
Để hiểu rõ hơn ý nghĩa của kỹ thuật này, ta xét bài toán tính phần tử thứ n của
dãy số Fibonaci. Dãy Fibonaci được định nghĩa đệ quy như sau: F(n) = F(n  1) + F(n
 2) n ≥ 2 và F(0) = 0, F(1) = 1. Giả sử tính phần tử F(5), khi đó số lượng các phần tử
nhỏ hơn 5 sẽ phải tính lại nhiều lần, cụ thể ta phải tính 2 lần giá trị F(3) và 3 lần giá trị
F(2) như Hình 7-1.

Hình 7-1. Minh họa cách tính phần tử thứ 5 của dãy số Fibonaci.
Trong ví dụ trên, ta có thể dùng một bảng để tính và lưu các giá trị của các phần
tử lần lượt F(2), F(3), …, F(n), phương pháp này gọi là bottom-up. Một biến đổi khác
của kĩ thuật quy hoạch động đó là tìm cách tránh giải các bài toán con không cần thiết.
Phương pháp này được gọi là top-down.
Một chú ý nữa khi sử dụng kỹ thuật quy hoạch động, đó là nguyên lí tối ưu của
Richard Bellman: nghiệm tối ưu của bài toán được tổng hợp từ nghiệm tối ưu của bài
toán con.

Lê Xuân Việt – Dương Hoàng Huyên


80 Phân tích và thiết kế thuật toán

Các bước cơ bản để giải bài toán bằng kĩ thuật quy hoạch động như sau:
1. Thiết lập công thức đệ quy để tìm nghiệm của bài toán. Đây là bước quan
trọng cũng là bước khó nhất của kĩ thuật này.
2. Từ công thức đệ vừa thiết lập, tạo bảng giá trị để lưu các kết quả trung gian.
3. Dựa vào bảng giá trị vừa tạo được, tìm phương án tối ưu của bài toán.

7.2. Các ví dụ cơ bản


7.2.1. Bài toán chọn tiền xu
Cho một dãy n đồng tiền xu với các giá trị tương ứng c1, c2, …, cn không nhất
thiết phải phân biệt. Yêu cầu chọn ra số tiền lớn nhất từ n đồng xu này sao cho không
có hai đồng tiền nào kề nhau từ trật tự ban đầu.
Đặt F(n) là số tiền lớn nhất chọn được từ dãy các đồng tiền ban đầu. Để tìm mối
liên hệ đệ quy cho F(n) ta chia cách đã chọn đồng xu thành hai nhóm: một là chứa
đồng xu cuối cùng và hai là không chứa đồng xu cuối cùng. Số tiền lớn nhất ta có thể
nhận được từ cách chọn đầu tiên là cn + F(n  2) (giá trị lớn nhất của đồng xu thứ n
cộng với cách chọn từ n  2 đồng xu trước đó). Số tiền lớn nhất có thể nhận được từ
cách chọn thứ hai là F(n  1). Khi đó ta có mối liên hệ đệ quy thỏa điều kiện ban đầu
như sau: F(n) = Max{cn + F(n  2); F(n  1)}, F(0) = 0, F(1) = c1. Thuật toán mô tả
như sau:
ALGORITHM CoinRow(c[1..n])
//Đầu vào: mảng c[1..n] số nguyên dương giá trị các đồng xu.
//Đầu ra: Số tiền lớn nhất chọn được.
F[0] ← 0; F[1] ← c[1]
for i ← 2 to n do
F[i] ← max{c[i] + F[i − 2], F[i − 1]}
return F[n]
Ví dụ cho 6 đồng xu lần lượt như sau: 5, 1, 2, 10, 6, 2. Ta chọn được số tiền lớn
nhất là 17 thỏa yêu cầu bài toán, cụ thể từng bước của thuật toán như sau:
Bước 1: F(0) = 0, F(1) = 5
Chỉ số i 0 1 2 3 4 5 6
c[i] 5 1 2 10 6 2
F[i] 0 5
Bước 2: F(2) = Max{1 + 0, 5} = 5
Chỉ số i 0 1 2 3 4 5 6
c[i] 5 1 2 10 6 2

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 7. Quy hoạch động 81

F[i] 0 5 5
Bước 3: F(3) = Max{2 + 5, 5} = 7
Chỉ số i 0 1 2 3 4 5 6
c[i] 5 1 2 10 6 2
F[i] 0 5 5 7
Bước 4: F(4) = Max{10 + 5, 7} = 15
Chỉ số i 0 1 2 3 4 5 6
c[i] 5 1 2 10 6 2
F[i] 0 5 5 7 15
Bước 5: F(5) = Max{6 + 7, 15} = 15
Chỉ số i 0 1 2 3 4 5 6
c[i] 5 1 2 10 6 2
F[i] 0 5 5 7 15 15
Bước 6: F(6) = Max{2 + 15, 15} = 17
Chỉ số i 0 1 2 3 4 5 6
c[i] 5 1 2 10 6 2
F[i] 0 5 5 7 15 15 17
Để chỉ ra phương án đã chọn, ta tính toán ngược trở lại bằng cách xem F(n) bằng
cn + F(n  2) hay bằng F(n  1). Cụ thể như sau: bắt đầu từ F(6), ta thấy F(6) = c6 +
F(4) do đó ta chọn đồng xu c6 = 2 và chuyển đến F(4). Tại F(4) ta thấy F(4) = c4 +
F(2) ta tiếp tục chọn đồng xu c4 = 10 chuyển đến F(2), tương tự F(2) = F(1) khi đó
đồng xu c2 = 1 không chọn mà ta chọn c1 = 5 và cuối cùng nghiệm tối ưu là {c1, c4, c6}
= 5 + 10 + 2 = 17. Thuật toán tìm phương án tối ưu của bài toán chọn tiền xu như sau:
Algorithm FindCoin(F[0..n], c[1..n])
//Đầu vào: mảng F[0..n] chứa các giá trị vừa tìm được, mảng c[1..n]
chứa giá trị của các đồng xu.
//Đầu ra: danh sách S chứa chỉ số của các đồng tiền xu đã chọn
i ← n; k ← 0
while (i > 1) do
if (F[i] = F[i - 2] + c[i]) then
k ← k + 1; S[k] ← i; i ← i - 2
else
i ← i – 1
if (i = 1) then
k ← k + 1; S[k] ← i
return S
Độ phức tạp của thuật toán này là O(n) với n là số đồng tiền xu vì ta chỉ cần
duyệt một lần n đồng xu đã tìm ra đáp án. Rõ ràng đây là thuật toán tốt hơn nhiều so
với cách gọi đệ quy hoặc duyệt toàn bộ.

Lê Xuân Việt – Dương Hoàng Huyên


82 Phân tích và thiết kế thuật toán

7.2.2. Bài toán đổi tiền


Cho số tiền n, đổi số tiền trên từ các mệnh giá tiền xu 1 = d1 < d2 < … < dm sao
cho số đồng tiền là ít nhất.
Đặt F(n) là số đồng tiền xu ít nhất đổi số tiền n, rõ ràng F(0) = 0. Số tiền n nhận
được khi thêm 1 đồng tiền với mệnh giá dj vào số tiền n  dj với j = 1..m và n ≥ dj. Ta
có công thức đệ quy như sau:

F(n) = minj:dj Sn {F(n − dj )} + 1 với n > 0, F(0) = 0.

ALGORITHM ChangeMaking(d[1..m], n)
//Đầu vào: Số nguyên n và mảng d[1..m] tăng dần và d[1] = 1
//Đầu ra: Số đồng tiền ít nhất F[n]
F[0] ← 0
for i ← 1 to n do
temp ← ∞; j ← 1
while (j ≤ m) and (i ≥ d[j]) do
temp ← min{F[i − d[j]], temp}; j ← j + 1
F[i] ← temp + 1
return F[n]
Ví dụ n = 6 và d1, d2, d3 lần lượt là 1, 3, 4.
Bước khởi tạo: F(0) = 0

n 0 1 2 3 4 5 6
F 0
Bước 1: F(1) = min{F(1  1)} + 1 = F(0) + 1 = 1.

n 0 1 2 3 4 5 6
F 0 1
Bước 2: F(2) = min{F(2  1)} + 1 = F(2  1) + 1 = 2

n 0 1 2 3 4 5 6
F 0 1 2
Bước 3: F(3) = min{F(3  1), F(3  3)} + 1 = F(3  3) + 1 = 1

n 0 1 2 3 4 5 6
F 0 1 2 1
Bước 4: F(4) = min{F(4  1), F(4  3), F(4  4)} + 1 = F(4  4) + 1 = 1

n 0 1 2 3 4 5 6
F 0 1 2 1 1
Bước 5: F(5) = min{F(5  1), F(5  3), F(5  4)} + 1 = F(5  1) = 2

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 7. Quy hoạch động 83

n 0 1 2 3 4 5 6
F 0 1 2 1 1 2
Bước 6: F(6) = min{F(6  1), F(6  3), F(6  4)} + 1 = F(6  3) + 1 = 2

n 0 1 2 3 4 5 6
F 0 1 2 1 1 2 2
Để tìm số đồng tiền cho phương án tối ưu ở trên ta tính ngược theo cách sau: bắt
đầu với i = n, j = m. Nếu F[i] = F[i  d[j]] + 1, điều này có nghĩa là ta chọn đồng tiền
mệnh giá d[j], khi đó chọn d[j] vào trong phương án tối ưu và giảm i = i  d[j]. Nếu
F[i]  F[i  d[j]] + 1, điều này có nghĩa là đồng tiền d[j] không có trong phương án tối
ưu, khi đó ta giảm j xuống 1. Tiếp tục thực hiện các bước trên cho đến khi i hoặc j
bằng 0 thì dừng. Chi tiết thuật toán tìm phương án tối ưu như sau:
ALGORITHM FindCoin(F[0..n], d[1..m)
//Đầu vào: Mảng F[0..n] chứa thông tin về số tờ tiền và d[1..m] chứa
thông tin mệnh giá
//Đầu ra: Mảng Dem[1..m] chứa thông tin số tờ của từng mệnh giá
d[1..m]
for j ← 1 to m do
Dem[j] ← 0
j ← m; i ← n
while (j >= 1) and (i >= 1) do
if(F[i - d[j]] + 1 = F[i]) then
Dem[j] ← Dem[j] + 1; i ← i - d[j]
else
j ← j - 1
return Dem[1..m]
Ta dễ dàng kiểm chứng được độ phức tạp của thuật toán đổi tiền là O(n.m). Đối
với thuật toán tìm phương án tối ưu cho bài toán đổi tiền độ phức tạp là O(n). Vậy độ
phức tạp cho cả thuật toán (vừa tìm số đồng tiền ít nhất, vừa tìm phương án tối ưu) là
O(n.m).

7.2.3. Bài toán thu thập đồng tiền


Có một số đồng tiền đặt vào bảng m × n ô, mỗi ô chỉ có không quá 1 đồng tiền.
Một con Robot đặt ở góc trên, trái của bảng. Cần thu thập nhiều đồng tiền nhất có thể
và mang đến ô dưới, phải của bảng. Tại mỗi bước Robot chỉ có thể di chuyển sang ô
hoặc là bên phải hoặc phía dưới của ô hiện tại. Khi Robot đến ô nào thì phải chọn đồng
tiền ở ô đó. Thiết kế thuật toán tìm số đồng tiền nhiều nhất và chỉ ra đường đi của
phương án tối ưu đó.

Lê Xuân Việt – Dương Hoàng Huyên


84 Phân tích và thiết kế thuật toán

Hình 7-2. (a). Cách bố trí đồng tiền; (b). Kết quả tính toán; (c). Đường đi cụ thể.
Đặt F(i, j) là số đồng tiền lớn nhất mà Robot đã thu thập được khi đi đến ô (i, j).
Robot đi đến được ô (i, j) chỉ từ một trong hai vị trí là ô (i  1, j) hoặc (i, j  1). Số
đồng tiền lớn nhất mà Robot mang đến các ô này lần lượt là: F(i  1, j) và F(i, j  1).
Tất nhiên, không có ô liền kề ở phía trên của dòng đầu tiên và ô liền kề bên trái của cột
đầu tiên. Đối với những ô này ta giả sử các ô liền kề của nó bằng 0. Vậy số đồng tiền
lớn nhất mà Robot mang đến ô (i, j) là giá trị lớn nhất của ô phía trên hoặc ô bên trái
cộng với 1 đồng tiền tại ô (i, j) nếu có. Công thức đệ quy cho F(i, j) như sau:
F(i, j) = Max{F(i − 1, j ); F(i, j − 1)} + cij với 1 ≤ i ≤ m, 1 ≤ j ≤ n.
F(0, j) = 0 với 1 ≤ j ≤ n và F(i, 0) = 0 với 1 ≤ i ≤ m, cij = 1 nếu ô (i, j) có đồng
tiền và bằng 0 nếu không có đồng tiền.
ALGORITHM RobotCoinCollection(c[1..m, 1..n])
//Đầu vào: Ma trận c[1..n, 1..m] các phần tử 1 và 0 ứng với ô có và
không có đồng tiền
//Đầu ra: Số đồng tiền lớn nhất mà Robot mang đến ô m x n
F[1, 1] ← c[1, 1]
for i ← 2 to m do
F[i, 1] ← F[i - 1, 1] + c[i, 1]

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 7. Quy hoạch động 85

for j ← 2 to n do
F[1, j] ← F[1, j − 1] + c[1, j]
for i ← 2 to m do
F[i, 1] ← F[i − 1, 1] + c[i, 1]
for j ← 2 to n do
F[i, j] ← max{F[i − 1, j]; F[i, j − 1]} + c[i, j]
return F[m, n]
Ta dễ dàng chứng minh được độ phức tạp của thuật toán thu thập đồng tiền ở trên
là O(m.n), trong đó m, n là kích thước của bảng.
Để tìm đường đi của Robot chọn được đồng tiền nhiều nhất, ta dựa bảng F[m, n],
nếu F[i, j] = F[i  1, j] + c[i, j] thì Robot đã đi qua ô (i  1, j) và tương tự F[i, j] = F[i, j
 1] + c[i, j] thì Robot đã qua ô (i, j  1). Chi tiết thuật toán tìm đường đi của Robot
như sau:
Algorithm FindCell(F[1..m, 1..n], c[1..m, 1..n])
//Đầu vào: Mảng F[m, n] chứa số đồng tiền nhiều nhất của từng ô,
mảng c[1..m, 1..n] chứa thông tin các đồng tiền trên bảng
//Đầu ra: hai mảng R, C chứa chỉ số dòng, cột của các ô đường đi.
i ← m; j ← n; k ← 0
while (i > 1) or (j > 1) do
k ← k +1
if (F[i, j] = F[i - 1, j] + c[i, j]) then
R[k] ← i – 1; C[k] ← j; i ← i – 1
else
R[k] ← i; C[k] ← j – 1; j ← j – 1
return R, C

7.2.4. Trò chơi chọn đồng tiền


Cho n (n chẵn) đồng tiền được xếp thành một dòng ngang, có thể có đồng tiền
giống nhau. Có hai người chơi, người đi trước gọi là A, người đi sau gọi là B. Tại mỗi
lượt chơi, người chơi chỉ được chọn một đồng tiền hoặc là bên trái nhất, hoặc là bên
phải nhất của dòng. Khi chọn hết các đồng tiền, trò chơi sẽ kết thúc và người chọn
được tổng số tiền nhiều hơn sẽ chiến thắng. Bài toán đặt ra là đưa ra cách chọn để
người chơi trước (người A) giành chiến thắng.
Có một cách đơn giản để chọn đồng tiền trong trò chơi này, đó là chọn đồng tiền
lớn nhất giữa hai đồng tiền bên trái nhất và bên phải nhất. Tuy nhiên cách này không
phải lúc nào cũng chiến thắng. Ví dụ cho dòng tiền với 6 đồng chi tiết các giá trị và
cách chọn như bảng sau:

A chọn trước Chỉ số 1 2 3 4 5 6 B chọn sau


Vị trí Tổng giá trị Giá trị 2 6 5 10 20 10 Vị trí Tổng giá trị
6 10 2 6 5 10 5 20
Lê Xuân Việt – Dương Hoàng Huyên
86 Phân tích và thiết kế thuật toán

A chọn trước Chỉ số 1 2 3 4 5 6 B chọn sau


Vị trí Tổng giá trị Giá trị 2 6 5 10 20 10 Vị trí Tổng giá trị
4 10 + 10 2 6 3 20 + 5
2 10+10+6=26 1 20+5+2=27
Để đưa ra cách chọn tối ưu, ta thiết kế thuật toán theo kĩ thuật quy hoạch động
như sau: gọi M(i, j) tổng giá trị các đồng tiền nhiều nhất mà người chơi trước chọn
được từ dòng tiền có chỉ số từ i đến j, ta cần tìm giá trị M(1, n). Giả sử các giá trị của
các đồng tiền lưu trong dãy v1, v2, …, vn. Để xác định công thức đệ quy cho M(i, j), ta
thấy rằng từ dòng tiền có chỉ số từ i đến j, người A chỉ có thể chọn hoặc là đồng tiền
thứ i hoặc là đồng tiền thứ j. Đến lượt người B sẽ tìm cách chọn sao cho người A sẽ
nhận được số tiền ít nhất có thể. Cụ thể người A chọn như sau:
Nếu j = i + 1, người A sẽ chọn giá trị lớn nhất giữa vi hoặc vj, và trò chơi kết thúc.
Ngoài ra, nếu người A chọn đồng tiền vi thì tổng giá trị đồng tiền nhận được sẽ là:
min{M(i + 1, j  1); M(i + 2, j)} + vi. Ngoài ra, nếu người A chọn đồng tiền vj thì tổng
giá trị đồng tiền nhận được là: min{M(i + 1, j  1), M(i, j  2)} + vj. Tóm lại, để người
A chọn được tổng số tiền lớn nhất có thể, ta chọn giá trị lớn nhất của hai giá trị trên,
tức là: M(i, j) = Max{min{M(i + 1, j  1); M(i + 2, j)} + vi; min{M(i + 1, j  1), M(i, j 
2)} + vj}, với M(i, i + 1) = Max{vi, vi + 1}, i = 1..n1.
ALGORITHM Coins-In-A-Line-Game(v[1..n])
//Đầu vào: Mảng V[1..n] giá trị của các đồng tiền
//Đầu ra: Tổng số tiền lớn nhất của người chơi trước
for i ← 1 to n - 1 do
M[i, i + 1] = Max(v[i]; v[i + 1])
k = 3
while(k <= n) do
i = 1; j = i + k
while (j <= n) do
M[i][j] = Max{Min{M[i + 1, j - 1]; M[i + 2, j]} + v[i];
Min{M[i, j - 2]; M[i + 1, j - 1]} + v[j]}
i = i + 1; j = j + 1
k = k +2
return M[1][n]
Thuật toán để tìm phương án tối ưu, tức là chỉ ra các đồng tiền người chơi trước
đã chọn giành chiến thắng, như sau:
ALGORITHM Find-Coins(M[1..n, 1..n], v[1..n])
//Đầu vào: mảng M[1..n, 1..n] chứa thông tin tổng số tiền lớn nhất
đã chọn, mảng v[1..n] chứa giá trị các đồng tiền
//Đầu ra: mảng S[1..k] chứa chỉ số của các đồng tiền đã chọn
i = 1; j = n; k = 0
while (i < j) do

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 7. Quy hoạch động 87

k = k +1
if (j = i + 1) then
if(v[i] > v[j]) then
S[k] = i; i = i + 1
else
S[k] = j; j = j - 1
else
if (M[i, j] = Min{M[i + 1, j - 1]; M[i + 2, j]} + v[i]) then
S[k] = i; i = i + 1
else
S[k] = j; j = j - 1
return S
Chú ý, thuật toán này chỉ áp dụng cho người chơi trước giành chiến thắng, vì vậy
nếu người chơi sau áp dụng thuật toán này vẫn không thắng người chơi trước. Quay lại
ví dụ ở phần trên, ta có dòng tiền như sau:
v[1] v[2] v[3] v[4] v[5] v[6]
2 6 5 10 20 10
Áp dụng thuật toán Coins-In-A-Line-Game ta tìm được ma trận M như sau:

j
1 2 3 4 5 6
i
1 6 16 27
2 6 26
3 10 25
4 20
5 20
6
Dựa vào ma trận M, ta xác định các đồng tiền đã chọn của người chơi trước trong
phương án tối ưu như sau:
Đầu tiên cho i = 1, j = 6, vì M[1, 6] = min{M[2, 5]; M[3, 6] + v[1]} = M[2, 5] +
V[1] = 27 = 25 + 2, nên người chơi trước chọn đồng tiền thứ nhất v[1] = 2. Đến lượt
người chơi sau chọn, giả sử họ chọn đồng tiền v[6] (có thể chọn v[2]). Khi đó dòng
tiền còn lại là từ v[2] đến v[5].
Tiếp theo ta có i = 2, j = 5, vì M[2, 5] = min{M[2, 3]; M[3, 4]} + v[5] = 26 = 6 +
20, nên người chơi trước sẽ chọn đồng tiền v[5] = 20. Đến lượt người chơi sau chọn
đồng tiền v[4] (có thể chọn v[3]). Khi đó dòng tiền còn lại là từ v[2] đến v[3].
Cuối cùng vì v[2] = 6 > v[3] = 5, nên người chơi trước sẽ chọn đồng tiền v[2] và
người chơi sau phải chọn đồng tiền cuối cùng v[3]. Vậy người chơi trước đã chọn
{v[1] = 2, v[5] = 20, v[2] = 6} với tổng giá trị là 28. Người chơi sau chọn {v[6] = 10,
v[4] = 10, v[3] = 5} với tổng giá trị là 25.
Lê Xuân Việt – Dương Hoàng Huyên
88 Phân tích và thiết kế thuật toán

7.3. Bài toán dãy con chung dài nhất


Cho dãy A = (a1, a2, …, an), dãy con của A có dạng (ai1 , ai2 , … , aik ) với ij < ij + 1,
j=1..k 1. Ví dụ dãy A = HELLOWORLD, khi đó HER là một dãy con của A. Chú ý,
các kí tự trong chuỗi con này không nhất thiết phải liên tục nhưng phải đảm bảo trật tự
ban đầu trong A.
Bài toán tìm dãy con chung dài nhất có thể phát biểu như sau: cho hai dãy X(x1,
x2, …, xm) và Y(y1, y2, …, yn) bất kì, tìm dãy con chung của X và Y sao cho dãy con này
có độ dài lớn nhất.
Kĩ thuật quy hoạch động để giải quyết bài toán này như sau: để thuận tiện trong
lúc trình bày ta gọi Xi = (x1, x2, …, xi) là dãy con của X với i phần tử đầu tiên, Yj = (y1,
y2, …, yj) là dãy con của Y với j phần tử đầu tiên. Gọi L(i, j) là độ dài của dãy con
chung dài nhất hai dãy Xi và Yj. Ta có L(0, j) = L(i, 0) = 0. Để xác định công thức đệ
quy tính L(i, j) ta xét hai trường hợp sau:
1. Nếu xi = yj, khi đó ta có nhận xét là phần tử cuối cùng của dãy Xi bằng phần tử
cuối cùng của dãy Yj do đó dãy con chung sẽ chứa phần tử này. Vậy L(i, j) = L(i  1, j
 1) + 1.
2. Nếu xi ≠ yj, khi đó xi và yj không thể đồng thời xuất hiện trong dãy con chung
dài nhất. Nếu xi không nằm trong dãy con cung dài nhất của Xi và Yj, thì dãy con chung
dài nhất của Xi và Yj là Xi-1 và Yj, khi đó L(i, j) = L(i  1, j). Nếu yj không nằm trong
dãy con chung dài nhất của Xi, Yj thì dãy con chung dài nhất của Xi, Yj sẽ là dãy con
chung dài nhất của Xi, Yj-1, khi đó L(i, j) = L(i, j  1). Kết hợp hai trường hợp trên ta
có: L(i, j) = Max{L(i  1, j); L(i, j  1)}. Chi tiết thuật toán như sau:
ALGORITHM MaxLenSubArray(X[1..m], Y[1..n])
//Đầu vào: Dãy X[1..m] dãy Y[1..n]
//Đầu ra: Độ dài của dãy con chung dài nhất
for i ← 0 to m do
L[i, 0] ← 0
for j ← 0 to n do
L[0, j] ← 0
for i ← 1 to m do
for j ← 1 to n do
if (X[i] = Y[j]) then
L[i, j] ← L[i - 1, j - 1] + 1
else
L[i, j] ← Max{L[i - 1, j]; L[i, j - 1]}
return L[n, m]
Để tìm dãy con chung dài nhất ta dựa vào các giá trị L[i, j]. Bắt đầu từ giá trị i =

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 7. Quy hoạch động 89

m, j = n. Nếu X[i] = Y[j] thì phần tử X[i] thuộc dãy con chung dài nhất, giảm i xuống 1,
giảm j xuống 1 và tiếp tục. Nếu L[i, j] = L[i  1, j] thì giảm i xuống 1. Nếu L[i, j] = L[i,
j  1] thì giảm j xuống và tiếp tục cho đến khi i hoặc j bằng 0. Chi tiết thuật toán như
sau:
ALGORITHM Find_MaxLenSubArray(L[1..m, 1..n], X[1..m], Y[1..n])
//Đầu vào: Mảng L[1..n, 1..m] được tạo ra từ thuật toán trên, dãy
X[m] và Y[n]
//Đầu ra: mảng Z[1..k] dãy con chung dài nhất.
i ← m; j ← n; k ← 0
while(i >= 1) and (j >= 1) do
if(X[i] = Y[j]) then
k ← k + 1; Z[k] ← X[i]; i ← i - 1; j ← j - 1;
else
if(L[i, j] = L[i - 1, j]) then
i ← i - 1;
else
j ← j - 1;
return Z;
Độ phức tạp của thuật toán tìm độ dài của dãy con chung dài nhất là O(m.n) và
độ phức tạp của thuật toán tìm dãy con chung dài nhất là O(Max{m, n}).

7.4. Bài toán Xếp ba lô và hàm bộ nhớ


7.4.1. Bài toán Xếp ba lô
Cho trước n đồ vật có trọng lượng lần lượt là w1, w2, …, wn và giá trị lần lượt là
v1, v2, …, vn, một ba lô với khả năng chứa là W. Yêu cầu chọn các đồ vật bỏ vào vừa
với ba lô mà có tổng giá trị là lớn nhất. Giả sử trọng lượng của các đồ vật và khả năng
chứa của ba lô là các số nguyên dương. Để giải quyết bài toán này bằng kĩ thuật quy
hoạch động ta cần phải tìm công thức đệ quy biểu diễn mối liên hệ của nghiệm bài
toán hiện tại với nghiệm của bài toán có kích thước nhỏ hơn. Đặt F(i, j) là giá trị lớn
nhất của cách chọn từ i đồ vật đầu tiên với ba lô có khả năng chứa là j, bài toán bây giờ
là tìm F(n, W). Ta chia tập con i phần tử đầu tiên thành 2 nhóm: một nhóm không chứa
đồ vật thứ i và nhóm thứ hai là chứa đồ vật thứ i. Ta có các nhận xét sau:
1. Các tập con i đồ vật đầu tiên mà không chứa đồ vật i, giá trị tối ưu cần tìm là
F(i  1, j).
2. Các tập con i đồ vật đầu tiên có chứa đồ vật i, giá trị tối ưu được tạo ra từ đồ
vật i và i  1 đồ vật trước đó với khả năng chứa của ba lô là j  wi. Giá trị tối ưu khi đó
là vi + F(i  1, j  wi).

Lê Xuân Việt – Dương Hoàng Huyên


90 Phân tích và thiết kế thuật toán

Giá trị tối ưu của cách chọn i đồ vật đầu tiên với sức chứa ba lô là j là giá trị lớn
nhất của một trong hai giá trị này. Tức là F(i, j) = Max{F(i  1, j); vi + F(i  1, j  wi)}
(với điều kiện j  wi ≥ 0). Chú ý, nếu đồ vật thứ i không vừa với ba lô có sức chứa j thì
ta không chọn đồ vật i, tức là F(i, j) = F(i  1, j) nếu j  wi < 0.
Tóm lại:
Max{F(i − 1, j) ; vi + F(i − 1, j − wi )} nếu j − wi ≥ 0
F(i, j) = { , với F(0, j) =
F(i − 1, j) nếu j − wi < 0
0 j ≥ 0, F(i, 0) = 0 i ≥ 0.
Chi tiết thuật toán như sau:
ALGORITHM Knapsack (w[1..n], v[1..n], W)
//Đầu vào: mảng w[1..n] chứa trọng lượng, v[1..n] giá trị của đồ vật,
W sức chứa của ba lô
//Đầu ra: F[n, W] giá trị lớn nhất của cách chọn n đồ vật vào ba lô
có khả năng W
for 1 ← 0 to n do
F[i, 0] ← 0;
for j ← 0 to W do
F[0, j] ← 0;
for 1 ← 1 to n do
for j ← 1 to W do
if (F[i - 1,j] > v[i] + F[i - 1, j - w[i]])
or (j - w[i] < 0) then
F[i, j] ← F[i - 1, j]
else
F[i, j] ← v[i] + F[i - 1, j - w[i]]
return F[n, W];
Xét ví dụ:
Đồ vật Trọng lượng (wi) Giá trị (vi)
Sức
1 2 12
chứa
2 1 10
ba lô
3 3 20
W=5
4 2 15
Chi tiết bảng F(i, j):

j
0 1 2 3 4 5
i
0 0 0 0 0 0 0
1 0 0 12 12 12 12
2 0 10 12 22 22 22
3 0 10 12 22 30 32
4 0 10 15 25 30 37

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 7. Quy hoạch động 91

Để tìm số đồ vật đã bỏ vào túi tạo thành phương án tối ưu ta tính ngược lại như
sau: do F(4, 5) > F(3, 5) nên đồ vật thứ 4 đã được thêm vào ba lô, tiếp theo ta tính F(3,
5  2) vì trọng lượng của đồ vật thứ 4 là 2. Ta thấy F(3, 3) = F(2, 3) nên đồ vật thứ 3
không bỏ vào ba lô. Tiếp tục, F(2, 3) > F(1, 3) nên đồ vật thứ 2 được bỏ vào ba lô, xét
tiếp F(1, 3  1) = F(1, 2) > F(0, 2) nên đồ vật thứ 1 đã thêm vào ba lô, 1 là phần tử
cuối cùng nên ba lô sẽ chứa các đồ vật lần lượt là 4, 2, 1. Chi tiết thuật toán như sau:
ALGORITHM FindItem(F[0..n,0..W], w[1..n], v[1..n])
//Đầu vào: F[0..n, 0..W] chứa các phương án tối ưu, w[1..n] trọng
lượng các đồ vật, v[1..n] giá trị các đồ vật.
//Đầu ra: mảng S[1..k] chứa phương án các đồ vật đã chọn
j ← W; i ← n
k ← 0
while (i >= 1) do
if(F[i, j] > F[i - 1, j]) then
k ← k + 1; S[k] ← i; j ← j - w[i];
i ← i - 1;
return S
Độ phức tạp của thuật toán tìm giá trị lớn nhất bỏ vào ba lô là O(n.W), độ phức
tạp của thuật toán tìm phương án tối ưu là O(n).

7.4.2. Hàm bộ nhớ


Như đã biết, quy hoạch động là bài toán tìm nghiệm liên quan đến công thức đệ
quy với các bài toán con trùng nhau. Phương pháp tiếp cận top-down tìm nghiệm bài
toán đệ quy dẫn đến tình huống giải bài toán con nhiều hơn một lần. Một phương pháp
quy hoạch động khác đó là bottom-up, điền tất cả các nghiệm của các bài toán con vào
một bảng và mỗi bài toán con chỉ giải một lần. Một nhược điểm của phương pháp
bottom-up đó là nghiệm của một số bài toán con không dùng để tổng hợp nghiệm ban
đầu. Một phương pháp có thể tổng hợp được ưu điểm của cả hai phương pháp trên đó
là hàm bộ nhớ. Hàm bộ nhớ là kĩ thuật chỉ giải các bài toán con cần thiết và chỉ giải
một lần. Phương pháp này tương tự như phương pháp top-down nhưng duy trì một
bảng lưu kết quả của bài toán con giống như kĩ thuật bottom-up.
Ban đầu, các phần tử của bảng được khởi tạo với kí hiệu “null” để chỉ ra rằng nó
chưa được tính toán. Sau đó, trước khi một giá trị mới được tính toán, phương pháp sẽ
kiểm tra các phần tử trong bảng, nếu phần tử đó không “null” thì lấy kết quả từ bảng.
Nếu phần tử cần tính là “null” nó sẽ được tính giá trị đó theo cách đệ quy và lưu vào
bảng. Thuật toán sau sẽ giải bài toán Xếp ba lô bằng kĩ thuật hàm bộ nhớ. Sau khi khởi
tạo bảng, hàm đệ quy sẽ gọi với i = n (số đồ vật) và j = W (khả năng của ba lô).

Lê Xuân Việt – Dương Hoàng Huyên


92 Phân tích và thiết kế thuật toán

ALGORITHM MFKnapsack(i, j)
//Đầu vào: một số nguyên dương i chỉ ra i đồ vật đầu tiên được xét
và số nguyên dương j chỉ ra khả năng chứa của ba lô
//Đầu ra: giá trị của tập con i phần tử đầu tiên cho phương án tối
ưu
//Chú ý: Sử dụng mảng toàn cục cho w[1..n],v[1..n], và F[0..n,0..W]
khởi tạo các phần tử mảng F[0..n, 0..W] bằng 0 cho các phần tử cột 0
và dòng 0, bằng -1 cho các phần tử còn lại
if (F[i, j] < 0) then
if (j < w[i]) then
value ← MFKnapsack(i − 1, j)
else
value ← max{ MFKnapsack(i − 1, j);
v[i] + MFKnapsack(i − 1, j − w[i])}
F[i, j] ← value
return F[i, j]
Áp dụng kĩ thuật hàm bộ nhớ cho ví dụ trên, ta có bảng như sau:

j
0 1 2 3 4 5
i
0 0 0 0 0 0 0
1 0 0 12 12 12 12
2 0 -1 12 22 -1 22
3 0 -1 -1 22 -1 32
4 0 -1 -1 -1 -1 37

7.5. Thuật toán Warshall và Floyd


7.5.1. Thuật toán Warshall
Ma trận kề của đồ thị có hướng A = {aij} là ma trận có giá trị 1 tại dòng i và cột j
nếu tồn tại cạnh có hướng từ đỉnh i đến đỉnh j và có giá trị 0 nếu không tồn tại cạnh có
hướng từ đỉnh i đến j. Tìm ma trận chứa thông tin về đường đi có hướng với độ dài bất
kì giữa các đỉnh trong đồ thị.
Ma trận này (còn gọi là bao đóng bắt cầu của đồ thị có hướng) có một vài ứng
dụng thực tế. Ví dụ khi giá trị của một ô trong bảng tính thay đổi, phần mềm bảng tính
phải biết tất cả các ô khác có bị tác động bởi sự thay đổi này hay không. Nếu bảng tính
được mô hình hóa bằng đồ thị có hướng, tất cả các đỉnh biểu diễn các ô và các cạnh
biểu diễn sự phụ thuộc giữa các ô.
Định nghĩa: bao đóng bắc cầu của đồ thị có hướng với n đỉnh là một ma trận cấp
n × n, T = {tij}. Trong đó phần tử tij ở dòng thứ i cột thứ j bằng 1 nếu tồn tại một đường
đi không tầm thường từ đỉnh i đến đỉnh j, ngược lại tij = 0. Một ví dụ về đồ thị có
hướng, ma trận kề và bao đóng bắt cầu của nó thể hiện ở Hình 7-3 sau:
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 7. Quy hoạch động 93

1 2 3 4 1 2 3 4
1 2
1 0 1 0 0 1 1 1 1 1
2 0 0 0 1 2 1 1 1 1
A= T=
3 0 0 0 0 3 0 0 0 0
3 4 4 4
1 0 1 0 1 1 1 1
(a) (b) (c)
Hình 7-3. (a) Đồ thị. (b) Ma trận kề. (c) Bao đóng bắt cầu của đồ thị.
Ý tưởng của thuật toán Warshall là xây dựng một chuỗi ma trận R(0), R(1), …, R(n).
Mỗi ma trận cung cấp thông tin nào đó về các đường đi có hướng trong đồ thị. Cụ thể
phần tử rij(k) trong dòng thứ i cột thứ j của ma trận R(k) bằng 1 nếu tồn tại một đường đi
có hướng với độ dài dương từ đỉnh i đến đỉnh j đi qua các đỉnh trung gian (nếu có) có
chỉ số không lớn hơn k. Chuỗi bắt đầu với ma trận R(0) chính là ma trận kề biểu diễn đồ
thị, R(1) chứa thông tin về các đường đi trong đồ thị có thể sử dụng đỉnh đầu tiên làm
trung gian, R(2) chứa thông tin về các đường đi trong đồ thị có thể sử dụng đỉnh 1 và
đỉnh 2 làm trung gian, …, R(n) chứa thông tin về các đường đi trong đồ thị có thể sử
dụng tất cả n đỉnh làm trung gian và đó chính là bao đóng bắc cầu.

Tâm điểm của thuật toán là ta có thể tính ma trận R(k) thông qua R(k  1). Ta có ijr(k)
bằng 1 nếu có đường đi từ đỉnh i đến đỉnh j qua các đỉnh trung gian được đánh số ≤ k.
Có hai tình huống xảy ra trong trường hợp này: một là danh sách các điểm trung gian
không chứa đỉnh k, khi đó đường đi từ i đến j có các đỉnh trung gian được đánh số ≤ k
 1 và do đó r(k–1)
ij
= 1. Tình huống thứ hai, danh sách đỉnh trung gian chứa đỉnh k, khi
đó ta có hai đường đi: một từ đỉnh i đến đỉnh k qua các đỉnh trung gian có chỉ số < k,
tức là r(k–1)
ik
= 1. Và một từ đỉnh k đến đỉnh j qua các đỉnh trung gian có chỉ số < k,
tức là r (k–1)
= 1. Vậy r(k) có thể được tính như sau:
kj ij

(k–1)
(k)
rij
rij = [ (k–1) , đây là công thức để tính các phần tử trong ma trận
rik and r(k–1)
kj

R(k). Ta có thể giải thích công thức này rõ ràng hơn như sau: nếu r(k–1) = 1, ij thì
r(k) = 1, nếu r(k–1) = 0 thì r(k) = 1 nếu và chỉ nếu r(k–1) = 1 và r(k–1) = 1.
ij ij ij ik kj

Thuật toán Warshall biểu diễn bằng giả mã như sau:


ALGORITHM Warshall(A[1..n, 1..n])
//Đầu vào: ma trận kề A của đồ thị có hướng với n đỉnh
//Đầu ra: ma trận R biểu diễn bao đóng bắc cầu của đồ thị
R(0) ← A
for k ← 1 to n do
Lê Xuân Việt – Dương Hoàng Huyên
94 Phân tích và thiết kế thuật toán

for i ← 1 to n do
for j ← 1 to n do
R(k)[i, j] ← (R(k-1)[i, j])
or (R(k - 1)[i, k] and R(k - 1)[k, j])
return R
Độ phức tạp của thuật toán Warshall là O(n3) với n là số đỉnh của đồ thị. Áp
dụng thuật toán Warshall với đồ thị đã cho ở Hình 7-3, ta có các ma trận R(k) như sau:
1 2 3 4
1 0 1 0 0 Số 1 cho biết tồn tại đường đi trực tiếp không qua
Ma trận R(0) = 2 0 0 0 1 đỉnh trung gian. R(0) chính là ma trận kề A.
3 0 0 0 0
4 1 0 1 0

1 2 3 4
1 0 1 0 0 Số 1 cho biết tồn tại đường đi có thể qua các đỉnh
Ma trận R (1)
= 2 0 0 0 1 trung gian đánh số  1. Có 1 đường đi mới thể hiện ở
3 0 0 0 0 số 1 in đậm.
4 1 1 1 0

1 2 3 4
1 0 1 0 1 Số 1 cho biết tồn tại đường đi có thể qua các đỉnh
Ma trận R(2) = 2 0 0 0 1 trung gian đánh số  2. Có 2 đường đi mới thể hiện ở
3 0 0 0 0 2 số 1 in đậm.
4 1 1 1 1

1 2 3 4
1 0 1 0 1 Số 1 cho biết tồn tại đường đi có thể qua các đỉnh
Ma trận R = 2
(3) 0 0 0 1 trung gian đánh số  3. Không có đường đi mới nào.
3 0 0 0 0
4 1 1 1 1

1 2 3 4
1 1 1 1 0 Số 1 cho biết tồn tại đường đi có thể qua các đỉnh
Ma trận R (4)
= 2 1 1 1 1 trung gian đánh số  4. Có 5 đường đi mới thể hiện ở
3 0 0 0 0 5 số 1 in đậm. R(4) = T chính là bao đóng bắt cầu.
4 1 1 1 1

7.5.2. Thuật toán Floyd


Cho đồ thị liên thông có trọng số (có thể có hướng hoặc vô hướng). Tìm độ dài
đường đi ngắn nhất từ mỗi đỉnh đến các đỉnh còn lại. Bài toán này có ứng dụng quan
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 7. Quy hoạch động 95

trọng trong thực tế liên quan đến các lĩnh vực truyền thông, mạng lưới giao thông.
Để thuận tiện, ta lưu độ dài các đường đi ngắn nhất trong một ma trận D = (dij)
kích thước n × n, gọi là ma trận khoảng cách: phần tử dij trong dòng i cột j chỉ ra độ dài
đường đi ngắn nhất từ đỉnh i đến đỉnh j. Ví dụ minh họa ở Hình 7-4.
Ta có thể sinh ra ma trận này bằng một thuật toán giống như thuật toán Warshall,
được gọi là thuật toán Floyd. Thuật toán này có thể áp dụng cho cả đồ thị trọng số có
hướng hoặc vô hướng. Thuật toán Floyd sẽ tính ma trận khoảng cách của đồ thị có
trọng số theo dãy các ma trận sau: D(0), D(1), …, D(n). Trong đó phần tử dij(k) là phần tử
tại dòng i, cột j của ma trận D(k) chính là độ dài đường đi ngắn nhất từ đỉnh i đến đỉnh j
có thể qua các đỉnh trung gian được đánh số ≤ k. Cụ thể D(0) là đường đi ngắn nhất
giữa các đỉnh không qua đỉnh trung gian nào, đó chính là ma trận trọng số W của đồ thị.
D(n) là ma trận khoảng cách chứa các độ dài đường đi ngắn nhất giữa các đỉnh, trong
các đường đi này có thể sử dụng n đỉnh làm trung gian và đó chính là ma trận khoảng
cách cần tạo ra.

2 1 2 3 4 1 2 3 4
1 2 1 0  3  1 0 10 3 4
3 6 7 2 2 0   2 2 0 5 6
W= 3  7 0 1 D= 3 7 7 0 1
3 1 4 4 6   0 4 6 16 9 0
(a) (b) (c)
Hình 7-4. (a) Đồ thị. (b) Ma trận trọng số. (c) Ma trận khoảng cách.
Giống như thuật toán Warshall, ta có thể tính ma trận khoảng cách D(k) thông qua
D(k  1). Đặt d(k) là phần tử dòng i cột j của ma trận D(k), điều này có nghĩa là d(k) là độ
ij ij
dài đường đi ngắn nhất từ đỉnh i đến đỉnh j qua các đỉnh trung gian (nếu có) có chỉ số
≤ k. Ta chia các đường đi này thành hai phần phân biệt: một là không sử dụng đỉnh k là
trung gian và hai là có sử dụng. Do đường đi trong phần thứ nhất không chứa đỉnh k
nên độ dài của nó chính là phần tử d(k–1)
ij , trong phần thứ hai ta lấy đỉnh k làm trung
gian để đi từ đỉnh i đến đỉnh j, khi đó khoảng cách d(k) = d(k–1) + d(k–1). Ta có công
ij ik kj

thức đệ quy để tính các phần tử d(k)


ij như sau:

d ( k) = Nin{d ( k–1) , d ( k–1) + d (k–1) }, với k ≥ 1 và d ( 0) = wij (ma trận kề biểu


ij ij ik kj ij
diễn đồ thị). Chi tiết thuật toán như sau:
ALGORITHM Floyd(W[1..n, 1..n])
//Đầu vào: ma trận trọng số W của đồ thị
Lê Xuân Việt – Dương Hoàng Huyên
96 Phân tích và thiết kế thuật toán

//Đầu ra: ma trận D khoảng các ngắn nhất của các đỉnh
D ← W
for k ← 1 to n do
for i ← 1 to n do
for j ← 1 to n do
D[i, j] ← min{D[i, j]; D[i, k] + D[k, j]}
return D
Độ phức tạp của thuật toán Floyd là O(n3). Áp dụng thuật toán Floyd cho ví dụ ở
Hình 7-4 như sau:
1 2 3 4
1 0  3  Độ dài đường đi ngắn nhất giữa các đỉnh không qua
Ma trận D (0)
= 2 2 0   đỉnh trung gian. D(0) chính là ma trận trọng số W.
3  7 0 1
4 6   0

1 2 3 4
1 0  3  Độ dài đường đi ngắn nhất giữa các đỉnh, có thể qua
Ma trận D (1)
= 2 2 0 5  đỉnh trung gian đánh số  1. Có 2 đường đi mới thể
3  7 0 1 hiện ở 2 giá trị in đậm.
4 6  9 0

1 2 3 4
1 0  3  Độ dài đường đi ngắn nhất giữa các đỉnh, có thể qua
Ma trận D (2)
= 2 2 0 5  đỉnh trung gian đánh số  2. Có 1 đường đi mới thể
3 9 7 0 1 hiện ở 1 giá trị in đậm.
4 6  9 0

1 2 3 4
1 0 10 3 4 Độ dài đường đi ngắn nhất giữa các đỉnh, có thể qua
Ma trận D (3)
= 2 2 0 5 6 đỉnh trung gian đánh số  3. Có 4 đường đi mới thể
3 9 7 0 1 hiện ở 4 giá trị in đậm.
4 6 16 9 0

1 2 3 4
1 0 10 3 4 Độ dài đường đi ngắn nhất giữa các đỉnh, có thể qua
Ma trận D = 2
(4) 2 0 5 6 đỉnh trung gian đánh số  4. Có 1 đường đi mới thể
3 7 7 0 1 hiện ở 1 giá trị in đậm. Đây chính là ma trận khoảng
4 6 16 9 0 cách cần tìm.

7.6. Bài tập


Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 7. Quy hoạch động 97

Bài 1. Kĩ thuật quy hoạch động có điểm gì giống và khác kĩ thuật chia để trị.
Bài 2. Đánh giá độ phức tạp của thuật toán chọn đồng tiền dựa vào công thức đệ quy.
Bài 3. Giải thuật toán chọn đồng tiền với dữ liệu cụ thể như sau: 5, 1, 2, 10, 6.
Bài 4. Áp dụng kĩ thuật quy hoạch động tìm các nghiệm của bài toán đổi tiền với các
mệnh giá 1, 3, 5 và số tiền là 9. Nếu có nhiều phương án giống nhau thì liệt kê tất cả.
Bài 5. Hãy sửa thuật toán thu thập đồng tiền trong trường hợp có một số ô không thể
truy cập được. Ví dụ như hình sau:

Bài 6. Một thanh kim loại có độ dài n (số nguyên) đơn vị. Mỗi đoạn với độ dài i có giá
trị là pi. Thiết kế thuật toán cắt thanh kim loại trên thành các đoạn sao cho tổng giá trị
là nhiều nhất.
Hướng dẫn: gọi L[n] là giá trị lớn nhất khi cắt thanh kim loại có chiều dài n đơn
vị thành các đoạn khác nhau. Ta thấy, giá trị L[n] nhận được khi cắt ra thanh kim loại
có chiều dài là n  i với giá trị pi lớn nhất, vì vậy: L[n] = MaxiSn {L[n − i] + ei }, L[0]
= 0.
Bài 7. Thiết kế thuật toán tính giá trị Ck = n!
mà không sử dụng phép nhân. Đánh
n (nk)!k!
giá độ phức tạp của thuật toán.
Hướng dẫn: sử dụng công thức đệ quy C k = C k + C k–1 , với C k = 1, C 1 = n
n n–1 n–1 k n

Bài 8. Áp dụng thuật toán quy hoạch động bottom-up giải bài toán Xếp ba lô với số
liệu cụ thể như sau:

Đồ vật Trọng lượng Giá trị Trọng lượng ba lô


1 3 25
2 2 20
3 1 15 W=6
4 4 40
5 5 50
Có bao nhiêu phương án tối ưu?

Lê Xuân Việt – Dương Hoàng Huyên


98 Phân tích và thiết kế thuật toán

Bài 9. Cho dãy số A = (a1, a2, …, an). Tìm dãy con của dãy A có tổng bằng S.
Hướng dẫn: gọi L[i, t] = 1 nếu có thể tạo ra tổng t từ dãy con i phần tử đầu tiên a1,
a2, …, ai, ngược lại L[i, t] = 0. Nếu L[n, S] = 1 tức là có dãy con n phần tử có tổng
bằng S. Công thức đệ quy tính L[i, t] như sau:

L[i, t] = 1 nếu L[i  1, t] = 1 hoặc L[i  1, t  ai] = 1

L[1, t] = 1 nếu t = a1 và L[1, t] = 0 nếu t  a1

L[i, 1] = 1 nếu a1 + a2 + … + ai = 1 và = 0 nếu a1 + a2 + … + ai  1.


Bài 10. Có n file video, file thứ i có kích thước là ai GB. Có hai đĩa USB có dung
lượng là như nhau. Hãy tìm cách chép n file video trên vào hai đĩa USB sao cho độ
chênh lệch kích thước các tập tin trên hai đĩa là nhỏ nhất.
Hướng dẫn: gọi T là tổng kích thước của n file video, ta cần tìm số S lớn nhất
thỏa S ≤ T / 2 và có 1 dãy con trong n file video có tổng bằng S. Khi đó ta sẽ có cách
chia với chênh lệch 2 phần là ít nhất và dãy con có tổng bằng S là phần thứ nhất, phần
thứ hai là phần còn lại.
Bài 11. Bài toán người bán cá Clement. Có người đánh cá bắt được n con cá, trọng
lượng mỗi con cá là ai đem bán ngoài chợ. Ở chợ cá, người mua không mua cá theo
đơn vị con mà mua theo đơn vị kg, chẳng hạn như 3kg, 5kg, … và người bán phải bán
nguyên con cá không được cắt nhỏ. Ví dụ: có 3 con cá có trọng lượng lần lượt là 3 2 4
kg, khi người mua 5 kg cá ta chọn 2 con thứ nhất và thứ hai với trọng lượng là 3 + 2 =
5 kg. Tuy nhiên, nếu người mua 8 kg cá, ta phải chọn phương án tốt nhất là con cá thứ
1 và 3 với trọng lượng là 3 + 4 = 7 < 8. Tóm lại, yêu cầu phải chọn số con cá để bán
sao cho tổng trọng lượng nhỏ hơn hoặc bằng trọng lượng cần mua.
Hướng dẫn: bài toán quy về việc tìm các phần tử trong mảng sao cho tổng các
phần tử đó lớn nhất và nhỏ hơn hoặc bằng số cho trước.
Bài 12. Cho n số nguyên dương, hãy chia n số này thành hai phần sao cho tích của
tổng hai phần này có giá trị lớn nhất.
Hướng dẫn: gọi T là tổng n số nguyên, giả sử ta đã chia thành hai nhóm, tổng
nhóm thứ nhất là S, tổng của nhóm còn lại là T  S, bài toán quy về tìm số S sao cho
tích S × (T  S) đạt giá trị lớn nhất.
Bài 13. Cho n số tự nhiên a1, a2, …, an. Cho trước số nguyên S, có hay không một
cách chèn dấu + hoặc – vào giữa các số để tổng các ai bằng S.
Hướng dẫn: đặt L[i, t] = 1 nếu có cách điền dấu vào i phần tử đầu tiên để cho
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 7. Quy hoạch động 99

tổng bằng t. Ta có L[1, t] = 1 nếu t = a1 và = 0 nếu t  a1. L[i, t] = 1 nếu L[i  1, t  ai]
= 1 hoặc L[i  1, t + ai] = 1. Nếu L[n, S] = 1 thì có phương án chèn dấu vào n phần tử
để kết quả bằng S.
Bài 14. Bài toán dãy con tăng nhiều phần tử nhất. Cho dãy a1, a2, …, an, tìm dãy con
tăng có nhiều phần tử nhất.
Hướng dẫn: gọi L[i] là độ dài của dãy con tăng nhiều phần tử nhất của dãy a1, a2,
…, ai. Ta có L[1] = 1, L[i] = Max{L[j]} + 1 với 0 < j < i, và aj < ai. Giải thích công
thức tính L[i] như sau: tìm giá trị L[j] lớn nhất mà thỏa điều kiện 0 < j < i và aj < ai sau
đó cộng 1.
Bài 15. Biến đổi chuỗi. Cho hai chuỗi S = s1s2…sn và D = d1d2…dm. Có 3 phép biến
đổi chuỗi như sau:
1. Chèn 1 kí tự vào sau kí tự thứ i.
2. Thay thế kí tự thứ i thành kí tự c.
3. Xóa kí tự thứ i.
Hãy tìm cách biến đổi chuỗi S thành chuỗi D với số phép biến đổi là ít nhất.
Hướng dẫn: cho L[i, j] là số phép biến đổi ít nhất để biến chuỗi S(i) = (s1s2…si)
thành chuỗi D(j) = (d1d2…dj). Ta cần tìm giá trị L[n, m]. Có hai trường hợp xảy ra:
Nếu si = dj (hai kí tự cuối cùng của hai chuỗi con S(i), D(j) bằng nhau), thì ta chỉ
cần biến đổi chuỗi S(i  1) thành chuỗi D(j  1) với số phép biến đổi ít nhất. Khi đó:
L[i, j] = L[i  1, j  1].

Nếu si  dj có 3 cách biến đổi phụ thuộc vào tình huống cụ thể như sau:

1. Nếu kí tự si 1 = dj thì xóa kí tự cuối của chuỗi S(i), tức là kí tự si, khi đó: L[i, j]
= L[i  1, j] + 1.

2. Nếu kí tự si 1 = dj  1 thì thay kí tự si thành kí tự dj, khi đó L[i, j] = L[i  1, j 


1] + 1

3. Nếu si = dj  1 thì chèn kí tự dj vào sau kí tự Si, khi đó L[i, j] = L[i, j  1] + 1.


Ta có: L[0, j] = j, tức là nếu chuỗi S bằng rỗng thì ta dùng j thao tác chèn kí tự
vào chuỗi S(j) để bằng chuỗi D(j). L[i, 0] = i, tức là nếu chuỗi D bằng rỗng, ta xóa i kí
tự trong S để chuỗi S bằng chuỗi D.

Lê Xuân Việt – Dương Hoàng Huyên


10 Phân tích và thiết kế thuật toán
0
L[i − 1, j − 1]nếu si = dj
Tóm lại: L[i, j] = {
Nin(L[i − 1, j], L[i − 1, j − 1], L[i, j − 1]) + 1 nếu si ≠ dj

Bài 16. Chuỗi đối xứng. Một chuỗi được gọi là đối xứng nếu chuỗi đó đọc ngược từ
trái qua phải và từ phải qua trái đều như nhau. Cho chuỗi S, tìm số kí tự ít nhất cần
thêm vào chuỗi S để chuỗi S trở thành chuỗi đối xứng.
Hướng dẫn: Gọi L[i, j] là số kí tự ít nhất cần thêm vào chuỗi con S[i..j] để chuỗi
con này trở thành chuỗi đối xứng. Ta cần tìm giá trị L[1, n]. Ta có:
L[i, i] = 0, chuỗi con S[i..i] có 1 kí tự nên nó là chuỗi đối xứng vì vậy ta không
cần thêm kí tự nào khác.

L[i, j] = L[i + 1, j  1] nếu kí tự si = sj.

L[i, j] = Max(L[i + 1, j]; L[i, j  1]) nếu si  sj.


Bài 17. Có n phòng học và k nhóm học sinh được đánh số từ nhỏ đến lớn (n ≥ k), cần
xếp k nhóm trên vào n phòng học sao cho nhóm có số hiệu nhỏ xếp phòng có số hiệu
nhỏ, nhóm có số hiệu lớn xếp vào phòng có số hiệu lớn. Trong mỗi phòng, nếu thừa
ghế phải chuyển các ghế thừa ra và nếu thiếu ghế phải chuyển ghế vào cho đủ. Biết
phòng i có ai ghế, nhóm j có bj học sinh. Hãy tìm phương án bố trí sao cho tổng số lần
chuyển ghế vào/ra là ít nhất.
Hướng dẫn: đặt L[i, j] là số lần chuyển ghế vào/ra ít nhất khi xếp nhóm j vào
phòng i.

Nếu i = j chỉ có một cách xếp nhóm i vào phòng i, do đó: L[i, i] = |a1  b1| + |a2 
b2| + … + |ai  bi|.
Nếu i < j không có cách xếp.

Nếu i > j có hai trường hợp xảy ra: xếp nhóm j vào phòng i, khi đó L[i, j] = L[i 
1, j  1] + |ai  bi|, không xếp nhóm j vào phòng i, khi đó L[i, j] = L[i  1, j].

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 8. Kĩ thuật tham lam 101

CHƯƠNG 8. KỸ THUẬT THAM LAM

8.1. Giới thiệu


Để bắt đầu, hãy xét bài toán đổi tiền như sau: Đổi một số lượng tiền n thành các
đồng tiền xu mệnh giá d1 > d2 > d3 > … > dm với số đồng xu ít nhất. Ví dụ, mệnh giá
tiền được sử dụng rộng rãi tại Hoa Kỳ là d1 = 25 (quarter), d2 = 10 (dime), d3 = 5
(nickel), d4 = 1 (penny). Ta sẽ đổi 48 cent bằng phương án sau: 1 quarter, 2 dime và 3
penny. “Tham lam” là suy nghĩ sẽ hướng đến việc chọn 1 quarter (25 cent) bởi vì nó
làm giảm nhiều nhất số lượng tiền còn lại, cụ thể là còn 23 cent. Bước thứ hai, theo ý
nghĩ bạn sẽ chọn một đồng tiền như trước, nhưng bạn không thể chọn 1 quarter vì nó
vi phạm vấn đề ràng buộc (lớn hơn số tiền còn lại mà bạn có). Nên lựa chọn tốt nhất
trong bước này là chọn 1 dime (10 cent). Lúc này số tiền còn lại là 13 cent, tiếp tục lựa
chọn 1 dime (10 cent) nữa và đương nhiên cuối cùng sẽ là 3 penny.
Tham lam là một kĩ thuật thiết kế thuật toán được áp dụng cho các bài toán tối ưu.
Kĩ thuật này đề xuất xây dựng nghiệm của bài toán thông qua các bước, mỗi bước là
mở rộng nghiệm bài toán đang xây dựng cho đến khi nhận được một nghiệm đầy đủ.
Tại mỗi bước (đây là tâm điểm của kĩ thuật này), lựa chọn phải thỏa các yêu cầu sau:
+ Khả thi: thỏa các ràng buộc của bài toán.
+ Tối ưu cục bộ: là lựa chọn tốt nhất trong tất cả các phương án có thể ở bước đó.
+ Không thể hủy bỏ: khi đã lựa chọn phương án đó thì không thể thay đổi trong
các bước tiếp theo.
Các yêu cầu trên cũng giải thích tên của kĩ thuật này là tham lam vì tại mỗi bước
chọn phương án tốt nhất có thể có.

8.2. Bài toán lịch phục vụ


Một người phục vụ phải phục vụ cho n khách hàng. Khách hàng thứ i cần phục
vụ trong thời gian ti , 1  i  n. Ta muốn tổng thời gian chờ đợi và thời gian được phục
vụ của tất cả các khách hàng (gọi là thời gian hệ thống) là nhỏ nhất. Hãy tìm một trình
tự phục vụ khách hàng (gọi là lịch phục vụ) để đạt được mục đích đặt ra.

Lê Xuân Việt – Dương Hoàng Huyên


102 Phân tích và thiết kế thuật toán

Giả sử có 3 khách hàng với thời gian phục vụ tương ứng là t1 = 5, t2 = 10, t3 = 3.
Khi đó có thể có 6 lịch phục vụ như sau:
Thứ tự Tổng thời gian hệ thống T
1 2 3 5 + (5 + 10) + (5 + 10 + 3) = 38
1 3 2 5 + (5 + 3) + (5 + 3 + 10) = 31
2 1 3 10 + (10 + 5) + (10 + 5 + 3) = 43
2 3 1 10 + (10 + 3) + (10 + 3 + 5) = 41

3 1 2 3 + (3 + 5) + (3 + 5 + 10) = 29  tối ưu
3 2 1 3 + (3 + 10) + (3 + 10 + 5) = 34.

Để đưa ra phương án tổng thời gian ít nhất dựa trên tư tưởng của kĩ thuật tham
lam ta thực hiện theo nguyên tắc sau: bàn nào có thời gian phục vụ ngắn nhất sẽ thực
hiện trước.

8.3. Bài toán lập lịch cho máy


Có tập T bao gồm n nhiệm vụ, mỗi nhiệm vụ i có khoảng thời gian bắt đầu si và
thời gian hoàn thành là fi (với si < fi). Nhiệm vụ i phải bắt đầu vào thời điểm si và hoàn
thành vào thời điểm fi. Mỗi nhiệm vụ chỉ có thể thực hiện trên một máy và mỗi máy
chỉ có thể thực hiện một nhiệm vụ tại một thời điểm. Hai nhiệm vụ i và j được gọi là
không xung đột nếu nó không bị chồng lấp thời gian, tức là fi < sj hoặc fj < si. Rõ ràng
hai nhiệm vụ có thể thực hiện trên một máy nếu nó không bị xung đột.
Bài toán lập lịch cho máy là lập lịch cho tất cả các nhiệm vụ trong T sao cho số
lượng máy cần sử dụng là ít nhất có thể.

Hình 8-1. Minh họa một cách lập lịch cho các nhiệm vụ với các cặp thời gian (si,
fi) tương ứng là (1, 3), (1, 4), (2, 5), (3, 7), (4, 7), (6, 9), (7, 8).
Có một vài cách để giải bài toán này bằng kĩ thuật tham lam, ví dụ như ưu tiên

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 8. Kĩ thuật tham lam 103

xét các nhiệm vụ có thời gian thực hiện là từ dài nhất đến ngắn nhất. Tuy nhiên, cách
này không cho kết quả tối ưu. Ví dụ như Hình 8-2.

Hình 8-2. Minh họa cách lập lịch không tối ưu. Các cặp thời gian (si, fi) tương
ứng là (1, 4), (5, 9), (3, 5), (4, 6); (a) cách chọn ưu tiên thời gian thực
hiện dài nhất đến ngắn nhất. (b) cách chọn tối ưu.
Một kĩ thuật tham lam khác để lập lịch cho các nhiệm vụ đó ưu tiên thời điểm bắt
đầu thực hiện từ sớm nhất đến trễ nhất. Cụ thể như sau: ta sắp xếp các nhiệm vụ tăng
dần theo thời gian bắt đầu thực hiện, gán công việc bắt đầu sớm nhất cho máy đầu tiên.
Tiếp tục xét các công việc tiếp theo, nếu không xung đột thì gán cho máy thứ nhất,
tương tự cho đến khi không còn việc nào có thể gán cho máy thứ nhất thì lập lịch các
công việc còn lại cho máy thứ hai, …

ALGORITHM TaskSchedule(T)
//Đầu vào: Tập T các nhiệm vụ với thời gian bắt đầu và kết thúc
(s[i], f[i])
//Đầu ra: lịch thực hiện các nhiệm vụ T với số máy ít nhất
m ← 1
while (T ≠ ) do
Chọn nhiệm vụ thứ i với thời gian bắt đầu sớm nhất: T = T \ {i}
if (máy j (1 ≤ j ≤ m) không xung đột với nhiệm vụ i) then
cho máy j thực hiện nhiệm vụ i
else
m ← m +1
cho máy m thực hiện nhiệm vụ i

Hình 8-3. Minh họa cách lập lịch tối ưu cho ví dụ ở Hình 8-1.

Lê Xuân Việt – Dương Hoàng Huyên


104 Phân tích và thiết kế thuật toán

8.4. Mã hóa Huffman


Giả sử ta cần mã hóa các kí hiệu từ bảng n chữ cái bằng cách gán cho mỗi kí hiệu
một chuỗi các bit nhị phân gọi là từ mã. Ví dụ ta có thể dùng từ mã có độ dài cố định
gán mỗi kí hiệu một chuỗi bit có cùng độ dài m (m ≥ log2 n). Đây cũng chính là cách
mã hóa của bảng mã ASCII. Có cách khác gán từ mã cho các kí hiệu này, đó là dùng
từ mã có độ dài khác nhau dựa trên ý tưởng là dùng từ mã ngắn hơn cho những kí hiệu
xuất hiện nhiều hơn. Ý tưởng này đã được sử dụng trong thực tế vào giữa thế kỉ 19 bởi
Samuel Morse.
Khi sử dụng từ mã có độ dài khác nhau có vấn đề phức tạp trong lúc giải mã là
làm thế nào để biết được kí tự bất kì có bao nhiêu bit. Để tránh vấn đề này, ta sử dụng
từ mã được gọi từ mã tiền tố. Trong từ mã gọi là tiền tố này, không có từ mã nào đứng
trước (tiền tố) từ mã khác. Cụ thể hơn, không có chuỗi bit nhị phân của từ mã nào xuất
hiện bên trái nhất trong chuỗi bit nhị phân của từ mã khác. Do đó, khi giải mã ta chỉ
cần quét chuỗi bit nhị phân cho đến khi nhận được một chuỗi con là từ mã của kí hiệu
nào đó, thay thế chuỗi con này bằng kí hiệu tương ứng và lặp lại cho đến hết chuỗi.
Để tạo từ mã tiền tố này, ta kết hợp mỗi kí hiệu trong bảng chữ cái một nút lá của
cây nhị phân. Cạnh bên trái gán nhãn 0, cạnh bên phải gán nhãn 1. Từ mã nhận được
bằng cách lưu lại nhãn trên đường đi từ gốc đến lá. Do không có đường đi nào mà đến
nút lá này rồi đến nút lá khác, nên không có từ mã nào là tiền tố của từ mã khác.
Thuật toán Huffman xây dựng từ mã cho các kí hiệu như sau:
Bước 1: Khởi tạo n cây, mỗi cây một nút và gán nhãn chính là kí hiệu của bảng
chữ cái. Ghi tần số xuất hiện của mỗi kí hiệu trên gốc của cây để chỉ trọng số. Tổng
quát hơn, trọng số của cây chính là trọng số của nút lá.
Bước 2: Tìm hai cây có trọng số nhỏ nhất, ghép hai cây này thành cây mới với
hai cây con trái và phải tương ứng. Ghi trọng số cây mới chính là tổng trọng của hai
cây. Lặp lại bước 2 cho đến khi chỉ còn lại một cây duy nhất.
Bước 3: Gán nhãn cho các cạnh của mỗi nút trên cây nhị phân vừa xây dựng như
sau: cạnh bên trái gán số 0, cạnh bên phải gán số 1.
Bước 4: Xây dựng từ mã cho các kí hiệu từ cây nhị phân như sau: xuất phát từ
nút gốc đi đến nút lá, trên đường đi lưu lại nhãn (gồm 0 hoặc 1) ở mỗi cạnh. Chuỗi nhị
phân hình thành trong quá trình di chuyển từ gốc đến kí hiệu nào chính là từ mã của kí
hiệu đó.
Ví dụ, xét bảng chữ cái gồm 5 kí hiệu: {A, B, C, D, _} với tần số xuất hiện trong

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 8. Kĩ thuật tham lam 105

văn bản như sau:

Kí hiệu A B C D _
Tần xuất 0.35 0.1 0.2 0.2 0.15
Chi tiết các bước xây dựng cây nhị phân Huffman để xác định từ mã tiền tố cho
các kí hiệu như Hình 8-4.

Hình 8-4. Minh họa cách xây dựng cây mã hóa nhị phân Huffman.
Kết quả từ mã như bảng sau:
Kí hiệu A B C D _
Tần xuất 0.35 0.1 0.2 0.2 0.15
Từ mã 11 100 00 01 101
Ta thấy độ dài trung bình của từ mã của mỗi kí hiệu là: 2 × 0.35 + 3 × 0.1 + 2 ×
0.2 + 2 × 0.2 + 3 × 0.15 = 2.25. Nếu dùng từ mã với độ dài cố định, ta cần ít nhất là 3

Lê Xuân Việt – Dương Hoàng Huyên


106 Phân tích và thiết kế thuật toán

bit để biểu diễn một kí hiệu, do đó tỉ lệ nén của mã hóa Huffman là (3  2.25) / 3 ×
100% = 25%. Tức là mã hóa Huffman giảm được khoảng 25% bộ nhớ sử dụng so với
mã hóa bằng độ dài cố định. Tuy nhiên, thực tế cho thấy rằng mã hóa Huffman giảm
được từ 20% đến 80% bộ nhớ tùy theo đặc điểm của từng dữ liệu văn bản.

8.5. Thuật toán Prim


Bài toán sau đây xuất hiện nhiều trong thực tế: cho n điểm nối chúng lại với nhau
bằng số đường nối ít nhất sao cho hai cặp điểm bất kì đều có đường nối với nhau. Ứng
dụng bài toán này ta có thể thiết kế tất cả các loại mạng bao gồm truyền thông, máy
tính, vận tải và điện tử bằng một phương án rẻ nhất mà vẫn đáp ứng tính liên thông.
Chúng ta có thể biểu diễn các điểm này bằng đỉnh của đồ thị, đường nối giữa các
đỉnh là cạnh của đồ thị và chi phí kết nối chính là trọng số của cạnh. Khi đó bài toán
chính là tìm cây khung nhỏ nhất của đồ thị.
Cây khung của một đồ thị vô hướng liên thông là đồ thị con liên thông không có
chu trình chứa tất cả các đỉnh của đồ thị. Nếu đồ thị có trọng số ở các cạnh, thì cây
khung nhỏ nhất là cây khung có trọng số ít nhất, ở đây trọng số của cây được định
nghĩa là tổng trọng số của các cạnh trong cây đó.
Nếu ta tìm cây khung dựa vào kĩ thuật vét cạn, ta sẽ gặp hai vấn đề. Thứ nhất, số
cây khung sẽ tăng hàm mũ theo kích thước của đồ thị. Thứ hai, việc sinh ra tất cả các
cây khung là không dễ dàng.
Thuật toán Prim xây dựng cây khung nhỏ nhất thông qua các bước, mỗi bước là
mở rộng cây con bằng cách bổ sung một đỉnh của đồ thị. Cây con khởi tạo chỉ có 1
đỉnh bất kì trong n đỉnh của đồ thị. Tại mỗi bước lặp, thuật toán sẽ mở rộng cây hiện
hành bằng cách bổ sung đỉnh gần nhất vào cây hiện tại. Đỉnh gần nhất chính là đỉnh
không phải của cây đó mà kết nối với một đỉnh bất kì trong cây bởi cạnh nhỏ nhất.
Thuật toán dừng khi tất cả các đỉnh đã được bổ sung vào cây. Do thuật toán mở rộng
cây bằng cách thêm chính xác một đỉnh tại mỗi bước lặp, nên tổng số bước lặp sẽ là n
 1, với n là số đỉnh của đồ thị. Thuật toán chi tiết như sau:
ALGORITHM Prim(G)
//Đầu vào: đồ thị có trọng số G = <V, E>
//Đầu ra: ET, tập cạnh tạo thành cây bao trùm nhỏ nhất của G
VT ← {v0} //Khởi tạo tập VT bằng một đỉnh bất kì
ET ← ∅
for i ← 1 to |V| − 1 do
tìm cạnh có trọng số nhỏ nhất e∗ = (v∗ , u∗ ) trong các cạnh (v, u)
sao cho v  VT, u  V-VT

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 8. Kĩ thuật tham lam 107

VT ← VT ∪ {u∗ }
ET ← ET ∪ {e∗ }
return ET
Xét ví dụ minh họa thuật toán Prim với đồ thị cho như Hình 8-5 sau:

Hình 8-5. Đồ thị 6 đỉnh với các trọng số tương ứng.


Các bước xây dựng cây khung bằng thuật toán Prim thể hiện trong bảng sau:

Tập đỉnh còn lại VVT cùng với


Tập đỉnh VT Tập cạnh ET của cây khung
khoảng cách ngắn nhất đến tập đỉnh
{v} (u, v)
đang xây dựng VT. u(v, trọng số)
{a} ∅ b(a, 3); c(, ); d(, ); e(a, 6); f(a, 5)
{a, b} (a, b) c(b, 1); d(, ); e(a, 6); f(b, 4)
{a, b, c} (a, b); (c, b) d(c, 6); e(a, 6); f(b, 4)
{a, b, c, f} (a, b); (c, b); (f, b) d(f, 5); e(f, 2)
{a, b, c, f, e} (a, b); (c, b); (f, b); (e, f) d(f, 5)
{a, b, c, f, e, d} (a, b); (c, b); (f, b); (e, f); (d, f) ∅
Chú ý, các kí hiệu chẳng hạn như: e(a, 6) có nghĩa là đỉnh e trong tập VVT gần
nhất với đỉnh a trong tập VT có trọng số là 6. Hoặc kí hiệu c(, ) có nghĩa là đỉnh c
trong tập VVT không có cạnh nối trực tiếp với bất kì đỉnh nào trong VT. Kí hiệu in
đậm là đỉnh được chọn trong bước tiếp theo. Cây khung tìm được thể hiện ở Hình 8-6.

Hình 8-6. Cây khung xây dựng theo thuật toán Prim của đồ thị ở Hình 8-5. Phần
in đậm là các cạnh của cây khung.

8.6. Thuật toán Kruskal

Lê Xuân Việt – Dương Hoàng Huyên


108 Phân tích và thiết kế thuật toán

Trong phần trước, ta đã xem xét thuật toán tham lam phát triển một cây khung
nhỏ nhất bằng cách chọn đỉnh gần nhất tới các đỉnh đã có trong cây. Có một thuật toán
tham lam khác cho bài toán cây khung nhỏ nhất cũng luôn cho nghiệm tối ưu. Đó là
thuật toán Kruskal do Joseph Kruskal phát minh ra khi ông còn học đại học năm thứ 2.
Thuật toán Kruskal tìm cây khung nhỏ nhất của một đồ thị có trọng số liên thông G =
(V, E) như một đồ thị con không có chu trình với |V|  1 cạnh mà tổng các trọng số
cạnh là nhỏ nhất.
Thuật toán bắt đầu bằng việc sắp xếp các cạnh theo thứ tự tăng của trọng số, và
khởi tạo đồ thị con trống. Thuật toán duyệt danh sách các cạnh đã sắp xếp này, thêm
cạnh trong danh sách vào đồ thị con hiện tại, nếu sau khi đưa vào nó không tạo chu ra
trình và bỏ qua nếu tạo ra chu trình.
ALGORITHM Kruskal(G)
//Đầu vào: đồ thị liên thông có trọng số G = <V, E>
//Đầu ra: ET, tập cạnh tạo thành cây bao trùm nhỏ nhất
sắp xếp E tăng dần theo trọng số w(e1) ≤ . . . ≤ w(e|E|)
ET ← ∅ ;
ecounter ← 0
k ← 0
while (ecounter < |V| − 1) do
k ← k +1
if (ET ∪ {ek} không tạo chu trình) then
ET ← ET ∪ {ek};
ecounter ← ecounter + 1
return ET
Lấy đồ thị ở Hình 8-5 để minh họa cho thuật toán Kruskal. Ta có các bước thực
hiện như sau:

Tập cạnh ET của Danh sách các cạnh sắp tăng dần theo trọng số.
cây khung (uv, trọng số)
(bc, 1); (ef, 2); (ab, 3); (bf, 4); (cf, 4); (af, 5); (df, 5); (ae, 6); (cd, 6);
 (de, 8)
{bc} (ef, 2); (ab, 3); (bf, 4); (cf, 4); (af, 5); (df, 5); (ae, 6); (cd, 6); (de, 8)
{bc, ef} (ab, 3); (bf, 4); (cf, 4); (af, 5); (df, 5); (ae, 6); (cd, 6); (de, 8)
{bc, ef, ab} (bf, 4); (cf, 4); (af, 5); (df, 5); (ae, 6); (cd, 6); (de, 8)
{bc, ef, ab, bf} (cf, 4); (af, 5); (df, 5); (ae, 6); (cd, 6); (de, 8)
{bc, ef, ab, bf, df} (cf, 4); (af, 5); (ae, 6); (cd, 6); (de, 8)
Chú ý, những cạnh in đậm là những cạnh sẽ chọn ở bước sau. Cây khung tìm
được giống như Hình 8-6.

8.7. Thuật toán Dijkstra


Bài toán cho một đỉnh bất kì trong đồ thị có trọng số, được gọi là đỉnh nguồn, tìm
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 8. Kĩ thuật tham lam 109

đường đi ngắn nhất đến tất cả các đỉnh khác trong đồ thị. Bài toán này tìm tập các
đường đi, mỗi đường đi sẽ dẫn đến một đỉnh trong đồ thị.
Ta có thể sử dụng thuật toán Floyd tìm đường đi ngắn nhất giữa các cặp đỉnh, tuy
nhiên có thuật toán hiệu quả hơn đó là thuật toán Dijkstra. Thuật toán này có thể áp
dụng trên đồ thị vô hướng hoặc có hướng với trọng số dương. Thuật toán như sau:
Đầu tiên tìm đường đi ngắn nhất từ đỉnh nguồn đến đỉnh gần với nó nhất, sau đó
đến đỉnh gần thứ 2, … Tổng quát, tại vòng lặp thứ i, thuật toán đã xác định đường đi
ngắn nhất đến i  1 đỉnh, các đỉnh (có thể gọi các đỉnh nguồn) và cạnh của đường đi
ngắn nhất này tạo thành cây Ti, đỉnh tiếp theo được chọn là đỉnh gần với nguồn nhất
trong các đỉnh kề với các đỉnh của Ti. Chi tiết thuật toán như sau:
ALGORITHM Dijkstra(G, s)
//Đầu vào: Đồ thị có trọng số dương G = <V, E> và đỉnh s
//Đầu ra: độ dài dv của đường đi ngắn nhất từ s đến v và đỉnh áp chót
pv của v trong đường đi đó.
for đỉnh v  V do
dv ← ∞; pv ← null
ds ← 0
VT ← ∅
for i ← 1 to |V| do
v ← đỉnh m  V - VT sao cho dm nhỏ nhất
VT ← VT ∪ {v}
for (đỉnh u  V − VT kề với v) do
if (dv + w(v, u) < du) then
du ← dv + w(v, u);
pu ← v
return dv, pv
Để hiểu rõ hơn thuật toán này, xét ví dụ minh họa tìm đường đi ngắn nhất từ đỉnh
a đến các đỉnh còn lại với đồ thị được cho ở Hình 8-7 như sau:

Hình 8-7. Đồ thị minh họa thuật toán Dijkstra tìm đường đi ngắn nhất từ đỉnh a
đến các đỉnh còn lại {b, c, d, e}.
Các bước thực hiện như sau:

Bước Các đỉnh đã xét VT Các đỉnh chứa xét V  VT


0  a( ,0), b(, ), c(, ), d(, ), e(, )
1 a(,0) b(a, 3), c(, ), d(a, 7), e(, )

Lê Xuân Việt – Dương Hoàng Huyên


110 Phân tích và thiết kế thuật toán

Bước Các đỉnh đã xét VT Các đỉnh chứa xét V  VT


2 a(,0), b(a, 3) c(b, 7), d(b, 5), e(, )
3 a(,0), b(a, 3), d(b, 5) c(b, 7), e(d, 9)
4 a(,0), b(a, 3), d(b, 5), c(b, 7) e(d, 9)
5 a(,0), b(a, 3), d(b, 5), c(b, 7), e(d, 9) 
Trong đó các kí hiệu ví dụ như e(d, 9) có ý nghĩa là đường đi ngắn nhất từ đỉnh a
đến đỉnh e với độ dài là 9 và đỉnh áp chót trong đường đi này là đỉnh d. Các giá trị in
đậm là các đỉnh sẽ được chọn ở bước sau.

8.8. Bài tập


Bài 1. Cài đặt thuật toán đổi tiền từ n mệnh giá cho trước.
Bài 2. Bài toán qua cầu. Có n người cùng ở một phía bờ sông cần đi qua cầu. Tốc độ
đi của n người tương ứng là s1,.., sn. Quy tắc qua cầu như sau:
Thời điểm qua cầu là ban đêm và cả nhóm chỉ có 1 cái đèn.
Mỗi lượt chỉ được tối đa là 2 người cùng qua cầu và phải mang theo đèn.
Nếu hai người cùng qua cầu thì tốc độ chung bằng tốc độ của người đi chậm hơn.
Những người này muốn qua cầu càng nhanh càng tốt. Hãy tìm một thuật toán để
đưa toàn bộ nhóm người qua cầu trong thời gian ít nhất.
Bài 3.
Giả sử có n người có chiều cao tương ứng là h1, …, hn đi trượt tuyết và có n đôi
giày trượt có chiều cao tương ứng là s1, …, sn. Cần tìm cách gán cho mỗi người một
đôi giày sao cho sự sai khác trung bình giữa chiều cao của mỗi người và chiều cao của
đôi giày trượt gán cho người đó là nhỏ nhất. Tức là nếu như người thứ i có chiều cao hi
∑n |hi–ci|
được gán đôi giày cao si thì ta muốn cực tiểu giá trị: i=1
.
n

a. Xét thuật toán tham lam sau: Tìm người và đôi giày có sự sai khác về chiều
cao nhỏ nhất, gán cho người đó đôi giày đó. Lặp lại như vậy cho tới khi mọi người đều
được gán giày. Thuật toán này có cho lời giải tối ưu? Chứng minh hoặc bác bỏ.
b. Xét thuật toán tham lam sau: Gán cho người thấp nhất đôi giày thấp nhất. Lặp
lại như vậy với những người còn lại và các đôi giày còn lại cho tới khi mọi người đều
được gán giày. Thuật toán này có cho lời giải tối ưu? Chứng minh hoặc bác bỏ.
Bài 4. Cho n khoảng mở (a1, b1), (a2, b2), …(an, bn) trên đường thẳng số thực. Mỗi
khoảng biểu diễn thời điểm bắt đầu và thời điểm kết thúc. Hãy tìm số khoảng lớn nhất
sao cho các khoảng này không giao nhau lần lượt theo các tiêu chí sau:
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 8. Kĩ thuật tham lam 111

a. Thời gian bắt đầu sớm nhất.

b. Độ lớn khoảng (bi  ai) là nhỏ nhất


c. Thời gian hoàn thành sớm nhất.
Bài 5. Có n thùng giống nhau, một trong n thùng đó chứa W lít nước và các thùng còn
lại là rỗng. Bạn chỉ được thực hiện 1 thao tác sau: lấy hai thùng bất kì và chia tổng
lượng nước trong hai thùng bằng nhau (tức là, nếu thùng nào nhiều nước hơn thì rót
qua thùng ít hơn để hai thùng bằng nhau). Yêu cầu thực hiện các thao tác trên để lượng
nước ở thùng ban đầu là ít nhất. Cách tốt nhất để thực hiện yêu cầu trên là gì?
Bài 6. Có 1 nhóm n người, mỗi người biết ngày sinh nhật của người khác. Họ muốn
chia sẻ tất cả các thông tin này đến mỗi người trong nhóm bằng thư điện tử. Giả sử
người gởi chèn tất cả thông tin đã biết vào thư điện tử tại thời điểm gởi và một thông
điệp chỉ có thể có một địa chỉ. Thiết kế thuật toán tham lam để mỗi người đều biết
ngày sinh của mọi người trong nhóm sao cho số thư cần gởi là ít nhất.
Bài 7. Cho n quả cân có trọng lượng tương ứng là w1, w2, …, wn. Tìm tập con các quả
cân sao cho nó có thể cân được đồ vật có trọng lượng W. Xét hai trường hợp:
a. Các quả cân chỉ có thể đặt ở 1 phía của cân.
b. Các quả cân có thể đặt ở hai phía của cân.
Bài 8. Bảo vệ nhà bảo tàng. Giả sử L là độ dài hành lang của nhà bảo tàng. Các vị trí
treo tranh dọc theo hành lang là các số thực x1, .., xn. Giả sử mỗi người bảo vệ có thể
bảo vệ tất cả các bức tranh ở khoảng cách 1 từ vị trí của người bảo vệ. Mô tả thuật toán
xác định các vị trí đứng gác sao cho số người bảo vệ là nhỏ nhất.
Bài 9. Có một văn bản gồm n từ. Từ thứ i có độ dài wi, tức là chứa wi ký tự. (Để đơn
giản ta giả sử không có khoảng trống giữa các từ và các dấu câu). Mục đích là ngắt văn
bản này thành các dòng (lưu ý là không được thay đổi thứ tự các từ). Độ dài của dòng
lý tưởng là L. Không có dòng nào được dài hơn L. Dòng có độ dài K có mức độ lãng
phí là L  K. Cần tìm một cách ngắt dòng có mức độ lãng phí tổng thể là tối thiểu. Xét
thuật toán sau: với mỗi i (1  i  n) nếu dòng đang xét còn chứa được từ thứ i thì đặt
nó trên dòng này, ngược lại thì đặt từ thứ i trên dòng mới. Thuật toán này có cho lời
giải tối ưu hay không trong các trường hợp sau:
a. Mức độ lãng phí tổng thể được định nghĩa là tổng mức độ lãng phí của các
dòng.
b. Mức độ lãng phí tổng thể được định nghĩa là mức độ lãng phí lớn nhất của các

Lê Xuân Việt – Dương Hoàng Huyên


112 Phân tích và thiết kế thuật toán

dòng.
Bài 10. Áp dụng thuật toán mã hóa Huffman cho dữ liệu như bảng sau:

Kí hiệu A B C D _
Tần xuất 0.4 0.1 0.2 0.15 0.15
Mã hóa chuỗi văn bản ABACABAD bằng từ mã vừa xây dựng ở trên.
Giải mã chuỗi nhị phân 100010111001010 dựa vào bảng mã trên.
Bài 11. Dự đoán thẻ bài. Thiết kế thuật toán cực tiểu hóa số câu hỏi trong trò chơi sau:
có 45 lá bài bao gồm một lá xì bích, hai lá hai bích, ba lá ba bích, …, chín lá chín bích,
một người chơi rút một lá bài bất kì từ các lá bài đã cho (đã được xáo trộn ngẫu nhiên).
Bạn có thể xác định lá bài này bằng cách hỏi các câu hỏi dạng yes/no.
Bài 12. Viết chương trình xây dựng bảng mã Huffman cho một văn bản tiếng Anh và
mã hóa nó. Viết chương trình giải mã văn bản đã mã hóa ở trên.
Bài 13. Một thổ dân muốn vượt qua sa mạc mà chỉ mang theo một chai nước. Anh ta
có một bản đồ đánh dấu tất cả các vị trí có giếng nước trên đường đi. Giả sử với một
chai nước anh ta có thể đi được k km. Mô tả thuật toán xác định các vị trí người thổ
dân nên dừng lại đổ nước đầy chai sao cho số lần dừng lại là ít nhất.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 9. Giải quyết giới hạn của thuật toán 113

CHƯƠNG 9. GIẢI QUYẾT CÁC GIỚI HẠN CỦA THUẬT


TOÁN

9.1. Giới thiệu


Hai thuật toán quay lui và nhánh-cận đều dựa trên việc xây dựng một cây không
gian trạng thái mà mỗi nút được lựa chọn đại diện cho một phần của nghiệm nhiều
thành phần. Gốc của cây biểu diễn trạng thái ban đầu trước khi tìm kiếm nghiệm. Các
nút ở mức đầu tiên biểu diễn cách lựa chọn thành phần đầu tiên của nghiệm, các nút
mức thứ hai biểu diễn cách chọn thành phần thứ hai, vân vân. Một nút trong cây trạng
thái được gọi là có triển vọng nếu tiếp tục phát triển cây trạng thái tại nút này sẽ dẫn
đến nghiệm đầy đủ, ngược lại ta nói nút đó là không có triển vọng. Các nút lá của cây
biểu diễn hoặc là nút không có triển vọng hoặc là nghiệm đầy đủ.
Cả hai kỹ thuật đều chấm dứt ngay ở một nút nào đó nếu chắc chắn rằng không
thu được nghiệm cho bài toán khi xem xét các lựa chọn tương ứng với các nút con
cháu của nó. Hai kĩ thuật này khác nhau ở bản chất những bài toán mà chúng được áp
dụng. Nhánh-cận được áp dụng chỉ để giải quyết bài toán tối ưu hóa vì nó dựa trên
việc tính toán một giá trị biên (còn gọi là cận) cho các giá trị có thể của hàm mục tiêu
của bài toán. Quay lui không bị ràng buộc bởi nhu cầu này, thông thường nó được áp
dụng cho các vấn đề không cần tối ưu hóa.
Một khác biệt nữa giữa quay lui và nhánh-cận là thứ tự các nút của cây không
gian trạng thái được tạo ra. Đối với quay lui, cây này thường phát triển theo chiều sâu
(tương tự như DFS) còn nhánh-cận có thể tạo ra các nút theo một số quy tắc, chẳng
hạn như nguyên tắc tốt nhất (best-first).

9.2. Kỹ thuật Quay lui


9.2.1. Phương pháp chung
Giả sử nghiệm của bài toán là một vector X gồm n thành phần, mỗi thành phần xi
 Si, tùy thuộc vào bài toán mà vector X có thể có cùng hoặc khác độ dài. Kĩ thuật
quay lui sẽ sinh ra một cây không gian trạng thái mà các nút của nó biểu diễn vector X

Lê Xuân Việt – Dương Hoàng Huyên


114 Phân tích và thiết kế thuật toán

đã xây dựng được i thành phần đầu tiên. Nếu (x1, x2, …, xi) vẫn chưa phải là nghiệm
của bài toán, thuật toán sẽ tìm thành phần tiếp theo xi + 1 trong Si + 1 phù hợp với (x1, x2,
…, xi) và thỏa ràng buộc để bổ sung vào X tạo nên bộ (x1, x2, …, xi, xi + 1). Nếu thành
phần xi + 1 không tồn tại, thuật toán quay lại xét giá trị tiếp theo của xi, vân vân. Phương
pháp chung của kĩ thuật quay lui có thể biểu diễn bằng giả mã như sau:
ALGORITHM Backtracking(X[1..i])
//Đầu vào: X[1..i] i phần tử đầu tiên có thể là nghiệm
//Đầu ra: Tất cả nghiệm của bài toán
if (X[1..i] là nghiệm) then
write X[1..i]
else
for (x ∈ Si + 1 phù hợp với X[1..i] và thỏa ràng buộc) do
X[i + 1] ← x
Backtracking(X[1..i + 1])

9.2.2. Bài toán N quân hậu


Trên bàn cờ vua kích thước n × n, hãy tìm cách đặt n quân hậu sao cho không
quân nào chiếu quân nào. Để đơn giản, ta sẽ đặt quân hậu thứ i vào hàng i, do đó bài
toán sẽ tìm cột thỏa điều kiện cho quân hậu thứ i. Cụ thể như Hình 9-1.

Hình 9-1. Bàn cờ 4 × 4 cho bài toán 4 quân hậu.


Dễ dàng giải bài toán trên với n = 1, với n = 2 hoặc n = 3 sẽ không có nghiệm. Ta
bắt đầu giải bài toán với n = 4. Bắt đầu đặt quân hậu 1 ở vị trí có vị trí đầu tiên chấp
nhận được là ô (1, 1) (ô ở cột 1 hàng 1). Sau đó, đặt quân hậu 2, vị trí chấp nhận được
đó là ô (2, 3). Tiếp theo sẽ không có vị trí chấp nhận được cho quân hậu 3. Vì vậy, ta
phải quay lui để đặt quân hậu 2 ở vị trí ô (2, 4). Sau đó, quân hậu 3 được đặt ở ô (3, 2).
Rõ ràng ta không thể tìm được vị trí cho quân hậu 4. Lần lượt lùi về quân hậu 3 rồi
quân hậu 2 vẫn không có khả năng để đặt. Cuối cùng ta quay về quân hậu 1 và di
chuyển nó đến ô (1, 2). Tiếp tục đặt quân hậu 2 vào ô (2, 4), quân hậu 3 đặt ở ô (3, 1)
và quân hậu 4 đặt ở ô (4, 3), đây là một nghiệm của bài toán. Cây không gian trạng
thái này được thể hiện trong Hình 9-2.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 9. Giải quyết giới hạn của thuật toán 115

Hình 9-2. Cây không gian trạng thái giải quyết bài toán 4-quân hậu bằng kỹ
thuật quay lui.
Để đặt nhiều hơn 4 quân hậu, ta tiếp tục hoạt động tương tự như 4-quân hậu cho
đến khi lùi về quân hậu 1 và thử hết khả năng, nghĩa là đã thử đặt ở cột cuối cùng. Với
bài toán này chúng ta có thể lợi dụng tính đối xứng của bàn cờ để xác định thêm một
số nghiệm khác. Thuật toán đặt n quân hậu biểu diễn bằng giả mã như sau:
Algorithm N-Queen(X[1..i])
//Đầu vào: số quân hậu n
//Đầu ra: bộ X[1..n] trong đó X[i] là chỉ số cột của quân hậu đặt ở
dòng i
if (i = n) then
write X[1..i]
else
for k ← 1 to n do
if (X[1..i + 1] không chiếu nhau) then
X[i + 1] ← k
Lê Xuân Việt – Dương Hoàng Huyên
116 Phân tích và thiết kế thuật toán

N-Queen(X[1..i + 1]

9.2.3. Bài toán Tổng tập con


Cho tập A = {a1, ..., an} với ai là các số nguyên dương và số nguyên dương d.
Tìm tất cả các tập con của A có tổng bằng d. Ví dụ tập A = {3, 5, 6, 7} và d = 15, khi
đó tập con {3, 5, 7} có tổng bằng 15.
Để đơn giản trong quá trình xây dựng cây không gian trạng thái, ta giả sử a1 < a2
< … < an. Cây không gian trạng thái là một cây nhị phân giống như trong Hình 9-3 với
tập hợp A = {3, 5, 6, 7} và d = 15. Gốc của cây là điểm khởi đầu. Nhánh trái và nhánh
phải tại mỗi nút tương ứng với bao gồm hoặc loại trừ phần tử đang xét. Một đường đi
từ gốc tới mức thứ i của cây chỉ ra một tập con i phần tử đầu tiên. Ta ghi lại giá trị s là
tổng các số trên đường đi này. Nếu s = d, đó là nghiệm của bài toán. Trường hợp s  d,
ta kết thúc việc tìm kiếm (không phát triển cây tại nút này) nếu gặp một trong trong hai
điều kiện sau:
s + ai+1 >d (tổng s quá lớn).

s ∑
+ nj=i+1 aj < d (tổng s quá nhỏ).

Hình 9-3. Cây không gian trạng thái của thuật toán quay lui áp dụng cho trường
hợp A = {3, 5, 6, 7} và d = 15.
Algorithm Sum-Subset(X[1..i], d)
//Đầu vào: Mảng a[1..n] với a[1] < a[2] < … < a[n], số nguyên d
//Đầu ra: mảng X[1..i] với X[i] = {0, 1} và ∑k=1 i
X[i]. a[i] = d
i
if(∑k=1 X[i]. a[i] = d) then
write X[1..i]
else
if {(∑i k=1 X[i]. a[i]) + a[i + 1] ≤ d và (∑i k=1 X[i]. a[i]) + ∑ j=i+1 a[j] > d} then
n

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 9. Giải quyết giới hạn của thuật toán 117

for k ← 0 to 1 do
X[i + 1] ← k
Sum-Subset(X[1..i + 1], d)

9.2.4. Bài toán tìm chu trình Hamilton


Cho đồ thị vô hướng G = <V, E>, tìm đường đi xuất phát từ đỉnh bất kì, qua tất
cả các đỉnh còn lại, mỗi đỉnh qua duy một lần và trở về đỉnh xuất phát. Ví dụ tìm chu
trình Hamilton trong đồ thị cho như Hình 9-4.

Hình 9-4. Đồ thị vô hướng với 6 đỉnh.


Không mất tính tổng quát, ta giả sử rằng nếu tồn tại chu trình Hamilton, nó bắt
đầu từ đỉnh a. Theo đó, ta có nút gốc của cây không gian trạng thái tại đỉnh a, xem
Hình 9-5.

Hình 9-5. Cây không gian trạng thái của bài toán tìm chu trình Hamilton của đồ
thị đã cho ở Hình 9-4. Các số trên các nút của cây chỉ trật tự các nút
được sinh ra.
Thành phần đầu tiên của nghiệm nếu có sẽ là đỉnh a. Có ba cách để chọn đỉnh kề
với a để phát triển cây, ở đây ta chọn đỉnh b. Từ b thuật toán sẽ tiếp tục chọn c, sau đó
đến d, tiếp tục đến e và cuối cùng đến f. Tại đỉnh f, ta không thể quay về đỉnh a nên

Lê Xuân Việt – Dương Hoàng Huyên


118 Phân tích và thiết kế thuật toán

đường đi a, b, c, d, e, f không phải là nghiệm của bài toán. Thuật toán sẽ quay lui về
đỉnh e để tiếp tục lựa chọn đường đi khác. Tuy nhiên, tại đỉnh e không thể lựa chọn
đỉnh khác để đi tiếp nên thuật toán tiếp tục quay lui về đỉnh d. Tương tự, tại đỉnh d
cũng không chọn được đỉnh khác để đi tiếp nên thuật toán lại tiếp tục lui về đỉnh c.
Tại đỉnh c, ta có thể đi tiếp đến đỉnh e, sau đó đến đỉnh d. Tuy nhiên tại đỉnh d ta
không thể đi đến đỉnh f được nên thuật toán tiếp tục quay lui về đỉnh e để từ e đi đến f.
Tại đỉnh f lại không có đường đi đến đỉnh d nên thuật toán quay lui về e, tiếp tục quay
lui về c tiếp tục quay lui về b.
Tại đỉnh b, ta có thể đi tiếp đến đỉnh f, sau đó đến e, tiếp tục đến c, tiếp tục đến d
và cuỗi cùng quay về a. Vậy ta có một chu trình Hamilton như sau: a, b, f, e, c, d, a.

9.3. Nhánh và cận


9.3.1. Phương pháp chung
Trong kỹ thuật quay lui ở phần trước, ta xem xét để cắt những nhánh trên cây
trạng thái càng sớm càng tốt nếu như đi theo các nhánh đó sẽ không tìm ra nghiệm.
Chúng ta thấy rằng bài toán tìm kiếm tối ưu là làm tối thiểu hoặc tối đa hàm mục tiêu
dựa trên các ràng buộc. Khác với quay lui, kỹ thuật nhánh-cận yêu cầu thêm hai yếu
tố: cách chọn nút kế tiếp và giá trị cận tốt nhất theo hàm mục tiêu.
Giả sử ta cần giải quyết bài toán tối ưu tổ hợp với mô hình tổng quát như sau:
min{f(x): x ∈ D}, trong đó D là tập hữu hạn phần tử, chẳng hạn D = {x = (x1, x2, ..., xn)
∈ A1 × A2 × ... × An}, với A1, A2, ..., An là các tập hữu hạn.
Ta có thể sử dụng thuật toán quay lui để liệt kê các phương án của bài toán.
Trong quá trình liệt kê theo thuật toán quay lui, ta sẽ xây dựng dần các thành phần của
nó. Một bộ phận gồm k thành phần (a1, a2, ..., ak) xuất hiện trong quá trình thực hiện
thuật toán sẽ được gọi là phương án bộ phận cấp k.
Thuật toán nhánh-cận có thể được áp dụng giải bài toán đặt ra nếu như có thể tìm
được một hàm g xác định trên tập tất cả các phương án bộ phận của bài toán thoả mãn
bất đẳng thức sau: g(a1, a2,..., ak) ≤ min{f(x1, x2,…xn), xi = ai, i = 1…k} với mọi lời giải
bộ phận (a1, a2,.., ak), và với mọi k = 1, 2, ...
Bất đẳng thức vừa nêu có nghĩa là giá trị của hàm tại phương án bộ phận (a1,
a2, ..., ak) không vượt quá giá trị nhỏ nhất của hàm mục tiêu bài toán trên tập con các
phương án.
Giả sử ta đã có được hàm g. Xét cách sử dụng hàm này để hạn chế khối lượng

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 9. Giải quyết giới hạn của thuật toán 119

duyệt trong quá trình duyệt tất cả các phương án theo thuật toán quay lui. Trong quá
trình liệt kê các phương án có thể đã thu được một số phương án (là nghiệm, có thể
chưa tối ưu) của bài toán. Gọi x̅ là phương án làm giá trị hàm mục tiêu nhỏ nhất trong
số các phương án đã duyệt, ký hiệu ƒ̅ = ƒ(x̅ ). Khi đó nếu g(a1, a2, ..., ak) > ƒ thì tập
con các phương án của bài toán D(a1, a2, …, ak) chắc chắn không chứa phương án tối
ưu. Trong trường hợp này ta không cần phải phát triển phương án bộ phận (a1, a2, ...,
ak), nói cách khác là ta có thể loại bỏ các phương án trong tập D(a1, a2, …, an) khỏi
quá trình tìm kiếm.
Algorithm Branch_and_bound(k)
// Phát triển phương án bộ phận (a1, a2, ..., ak - 1)
for (ak ∈ Ak) do
if (chấp nhận ak) then
xk  ak;
if (k = n) then <cập nhật ƒ>;
else if (g(a1, a2,..., ak) ≤ ƒ) Branch_and_bound(k + 1);
9.3.2. Bài toán Xếp ba lô
Chúng ta sẽ dùng thuật toán nhánh-cận để giải quyết bài toán xếp ba lô được
phát biểu ở phần trước.
Để thuận tiện, giả sử các vật được sắp theo tỷ lệ: v1 / w1 ≥ v2 / w2 ≥ … ≥ vn / wn. Ta
có cây không gian trạng thái cho bài toán này là cây nhị phân được xây dựng như sau:
Mỗi nút ở mức i (0 ≤ i ≤ n) của cây biểu thị cách chọn từ i đồ vật đầu tiên. Tại mỗi nút
sẽ có hai nhánh, nhánh bên trái biểu diễn tập con có chứa đồ vật và nhánh bên phải
biểu diễn tập con không chứa đồ vật tương ứng. Ta lưu tổng trọng lượng, tổng giá trị
và cận trên của cách chọn trong nút.
Cách tính cận trên ub (upper bound) để thêm vào v (tổng giá trị của các vật được
chọn) là phần còn trống của ba lô W  w với giá trị tốt nhất thu được là vi + 1 / wi + 1. Ta
có: ub = v + (W − w)(vi + 1 / wi + 1) (*). Trong đó, w là tổng trọng lượng các vật đã chọn.
Xét ví dụ cụ thể như sau:

Chỉ số Trọng lượng Giá trị Giá trị v


( ) Khả năng chứa
i w v Trọng lượng w
1 4 40 10
2 7 42 6
W=10
3 5 25 5
4 3 12 4
Chi tiết cây không gian trạng thái của ví dụ này thể hiện ở Hình 9-6. Tại nút gốc
của cây không gian trạng thái, không có đồ vật nào được chọn do đó tổng trọng lượng

Lê Xuân Việt – Dương Hoàng Huyên


120 Phân tích và thiết kế thuật toán

và tổng giá trị bằng 0, giá trị cận trên được tính bằng công thức (*) là 100. Nút 1 (bên
trái nút gốc) biểu diễn tập con có chứa đồ vật 1, tổng trọng lượng và giá trị đã được
thêm vào là w = 4 và v = 40 tương ứng, giá trị của cận trên ub = 40 + (10  4) × 6 = 76,
nút 2 (bên phải nút gốc) biểu diễn tập con không chứa đồ vật 1, do đó w = 0, v = 0 và
ub = 0 + (10  0) × 6 = 60. Do nút 1 có cận trên lớn hơn nút 2, nên nó hứa hẹn đến
nghiệm tối ưu hơn và do đó ta phát triển cây không gian trạng thái từ nút 1.

Hình 9-6. Cây không gian trạng thái cho bài toán Xếp ba lô.
Con của nút 1 là nút 3 và nút 4, biểu diễn tập con với đồ vật thứ 1 và có và không
có đồ vật thứ 2 tương ứng. Do tổng trọng lượng biểu diễn tập con ở nút 3 đã vượt quá
khả năng chứa của ba lô nên ta không phát triển cây không gian trạng thái ở nút 3. Nút
4 có w và v bằng giá trị của nút cha và cận trên ub = 40 + (10  4) × 5 = 70, ta chọn
nút 4 để phát triển nhánh của cây không gian trạng thái vì nút 4 có cận trên lớn hơn nút
2. Tương tự, khi phát triển nút 4, ta nhận được nút 5 và 6 tương ứng với trường hợp có
và không có đồ vật 3. Tiếp tục tính toán các giá trị w, v, ub cho các nút mới, ta quyết
định phát triển nhánh mới từ nút 5 (bởi vì có cận trên lớn hơn nút 2 và nút 6) và nhận
được nút 7 và 8. Ta thấy nút 7 có giá trị w vượt quá khả năng chứa của ba lô nên nút 7
không phải là nghiệm. Nút 8 có giá trị ub lớn hơn các giá trị ub của các nút 2 và 6 nên
kết thúc xây dựng cây không gian trạng thái và chọn nút 8 là biểu diễn của nghiệm.
Chú ý, ta vẫn còn hai nút là nút 2 và nút 6 có thể phát triển tiếp cây không gian trạng
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 9. Giải quyết giới hạn của thuật toán 121

thái. Tuy nhiên vì cận trên trên của nó nhỏ hơn nút 8, do đó nếu tiếp tục phát triển cây
không gian trạng thái cũng không cho kết quả tốt hơn nút 8. Đây cũng là ý tưởng chính
của kĩ thuật nhánh-cận.

9.3.3. Bài toán Người đi du lịch


Chúng ta có thể áp dụng kĩ thuật nhánh-cận để giải bài toán người đi du lịch nếu
như ta đưa ra được cận dưới của các độ dài hành trình. Một trong những cận dưới đơn
giản đó là lấy khoảng cách ngắn nhất của hai thành phố rồi nhân nó với số thành phố n.
Ta cũng có thể tính cận dưới theo cách khác như sau: với mỗi thành phố i, tìm
tổng si khoảng cách hai thành phố gần với thành phố i nhất, tính tổng s của n thành phố
sau đó chia 2 sau đó làm tròn số: lb = s/2 (**). Xét ví dụ cụ thể như Hình 9-7 sau:

Hình 9-7. Đồ thị biểu diễn bài toán Người đi du lịch với số thành phố n=5.
Trong ví dụ ở Hình 9-7, cận dưới tính theo công thức (**) như sau: lb =
(1+5)+(3+6)+(1+2)+(3+4)+(2+3)
] ] = 14.
2

Trước khi xây dựng cây không gian trạng thái cho ví dụ trên, ta xét 2 tính chất
quan trọng của bài toán này như sau: (i). Không mất tính tổng quát, ta chỉ xét những
hành trình bắt đầu từ a, (ii). Bởi vì đây là đồ thị vô hướng, nên ta có thể sinh ra hành
trình mà luôn đến thành phố b trước thành phố c. Cây không gian trạng thái của bài
toán người đi du lịch với n = 5 thành phố đã cho như Hình 9-8.

Lê Xuân Việt – Dương Hoàng Huyên


122 Phân tích và thiết kế thuật toán

Hình 9-8. Cây không gian trạng thái cho ví dụ ở Hình 9-7.
Xuất phát từ thành phố a, cận dưới tính được từ công thức (**) là lb = 14. Từ
thành phố a, ta có thể đến thành phố b, d, e với giá trị cận dưới tính theo công thức
(**) lần lượt là lb = 14, 16, 19. Từ thành phố a đến thành phố c không thực hiện vì dựa
vào tính chất (ii). Vì hành trình từ a đến b có cận dưới mới cập nhật là lb = 14 là nhỏ
nhất, nên ta phát triển cây không gian trạng thái từ nút này (nút 1). Tại nút 1 này, ta có
thể có 3 cách đi, đó là (a  b  c) hoặc (a  b  d) hoặc (a  b  e) do đó ta có
thể tạo ra 3 nút con 5, 6, 7 từ nút 1 này và lần lượt giá trị cận dưới là 16, 16, 19. Tại
thời điểm này ta có 5 nút có thể phát triển cây không gian trạng thái, đó là nút: 3, 4, 5,
6, 7. Ta chọn nút thứ 5 để phát triển cây vì nút này có cận dưới lb = 16 nhỏ nhất. Sau
khi phát triển cây không gian trạng thái từ nút thứ 5, ta tạo ra hai nút con (đây cũng là
nút lá) mới là nút 8, 9 với độ dài đường đi lần lượt là 24, 19.
Sau khi tạo ra hai lá 8 và 9, ta vẫn còn các nút khác có thể phát triển cây không
gian trạng thái đó là nút 3, 4, 6, 7. Ta tiếp tục chọn nút thứ 6 để phát triển cây vì cận
dưới tại nút này lb = 16 nhỏ nhất. Tại nút thứ 6, ta tạo ra hai nút con 10, 11 (đây cũng
là nút lá) với độ dài đường đi lần lượt là 24, 16.
Tại thời điểm này, ta vẫn còn 3 nút để phát triển cây không gian trạng thái là nút
3, 4, 7. Tuy nhiên vì cận dưới của ba nút này lần lượt là lb = 16, 19, 19 đều lớn hơn

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 9. Giải quyết giới hạn của thuật toán 123

hoặc bằng độ dài của nút lá 11, do đó nếu tiếp tục phát triển cây không gian trạng thái
tại 3 nút này, chắc chắn sẽ sinh ra đường đi có độ dài lớn hơn 16. Đây cũng chính là ý
tưởng của kĩ thuật nhánh-cận. Vậy nghiệm tối ưu là nút lá 11 vì có giá trị đường đi
nhỏ nhất so với các nút lá khác.

9.3.4. Bài toán Phân công công việc


Tiếp tục minh họa kĩ thuật nhánh-cận bằng cách giải bài toán phân công n người
với n việc sao cho tổng giá trị là nhỏ nhất. Dữ liệu của bài toán được cho trong ma trận
C = {cij} là giá trị của cách phân công người thứ i với công việc j. Bài toán có thể phát
biểu lại như sau: chọn 1 phần tử trong mỗi dòng của ma trận sao cho không có 2 phần
tử nào cùng cột và tổng của các phần tử nhỏ nhất có thể. Ví dụ cho 4 người a, b, c, d
và 4 công việc 1, 2, 3, 4 với giá trị phân công chi tiết như Hình 9-9 sau:

Hình 9-9. Ví dụ bài toán phân công 4 người với 4 công việc.
Ta có thể tìm cận dưới của bài toán dựa vào tính chất: nghiệm tối ưu chắc chắn
phải lớn hoặc bằng tổng các số nhỏ nhất ở mỗi dòng, trong ví dụ trên ta có tổng này là
2 + 3 + 1 + 4 = 10. Các bước xây dựng cây không gian trạng thái cho ví dụ cụ thể của
bài toán phân công như sau:
Bắt đầu với nút gốc không có phần tử nào được chọn từ ma trận C, giá trị cận
dưới của nút gốc là lb = 10. Các nút cho mức đầu tiên của cây là cách chọn phần tử
cho dòng thứ nhất (hay là phân công việc cho người a, xem Hình 9-10).

Hình 9-10. Mức 1 và mức 2 của cây không gian trạng thái.
Tại mức đầu tiên ta có 4 nút lá (đánh số 1  4) có thể chứa nghiệm tối ưu, trong
đó nút thứ 2 là hứa hẹn nhất vì nó có giá trị cận dưới lb thấp nhất, ta tiếp tục phát triển

Lê Xuân Việt – Dương Hoàng Huyên


124 Phân tích và thiết kế thuật toán

nhánh tại nút số 2 này bằng cách xét 3 cách khác nhau để chọn phần tử dòng số 2
không nằm trong cột số 2 (đây là 3 công việc khác có thể phân công cho người b, xem
Hình 9-11).
Trong 6 nút lá của cây không gian trạng thái ở Hình 9-11, nút 1, 3, 4, 5, 6 và 7 có
thể chứa nghiệm tối ưu, ta lại chọn nút thứ 5 để phát triển cây lên mức 4 vì nút này có
giá trị lb = 13 nhỏ nhất. Để phát triển cây không gian trạng thái từ nút số 5 ta chỉ có 2
lựa chọn: hoặc chọn phần tử cột thứ 3 dòng c và phần tử thứ 4 dòng d, hoặc chọn phần
tử cột thứ 4 dòng c và phần tử cột thứ 3 dòng d. Từ đó dẫn đến cây không gian trạng
thái ở Hình 9-12.

Hình 9-11. Xây dựng mức thứ 3 cho cây không gian trạng thái.

Hình 9-12. Cây không gian trạng thái ở mức thứ 4.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 9. Giải quyết giới hạn của thuật toán 125

Ta thấy nút thứ 8 có giá trị 13 và nút thứ 9 có giá trị là 25 do đó ta chọn nút thứ 8
là nghiệm. Vậy nghiệm của bài toán là a  2, b  1, c  3, d  4 và tổng giá trị
phân công là 13. Chú ý, ta vẫn có thể phát triển cây không gian trạng thái ở các nút
còn lại như: 1, 3, 4, 6, 7. Tuy nhiên cận dưới của các nút này lớn hơn giá trị của nút
thứ 8 nên tiếp tục phát triển cây tại các nút này cũng không dẫn đến nghiệm tối ưu.

9.4. Bài tập


Bài 1.
a. Tiếp tục thuật toán Quay lui tìm kiếm một nghiệm cho bài toán 4-quân hậu, đề
xuất nghiệm thứ hai cho bài toán n-quân hậu.
b. Giải thích làm thế nào có thể sử dụng tính đối xứng của bàn cờ để giải quyết
bài toán 4-quân hậu.
Bài 2.
a. Tìm nghiệm cho bài toán 5-quân hậu bằng thuật toán Quay lui.
b. Sử dụng tính đối xứng của bàn cờ, tìm ít nhất bốn nghiệm cho bài toán trên.
Bài 3. Cài đặt thuật toán Quay lui cho n-quân hậu với ngôn ngữ lập trình tùy chọn.
Chạy chương trình của bạn với một mẫu các giá trị n để có được số lượng các nút
trong cây không gian trạng thái của thuật toán. So sánh các số này với các số giải pháp
được tạo ra bởi thuật toán tìm kiếm toàn bộ.
Bài 4. Thiết kế một thuật toán thời gian tuyến tính để tìm một nghiệm pháp cho bài
toán n-quân hậu với mọi n ≥ 4.
Bài 5. Áp dụng kĩ thuật Quay lui để tìm một chu trình Hamilton trong đồ thị sau đây.
a b

c d e

Bài 6. Áp dụng kĩ thuật Quay lui để giải quyết bài toán 3-màu cho đồ thị trong ở Bài 5.
Bài 7. Tạo ra tất cả các hoán vị của {1, 2, 3, 4} bằng thuật toán Quay lui.

Bài 8.
a. Áp dụng thuật toán Quay lui để giải quyết trường hợp sau đây của bài toán
tổng tập con: A = {1, 3, 4, 5} và d = 11.

Lê Xuân Việt – Dương Hoàng Huyên


126 Phân tích và thiết kế thuật toán

b. Thuật toán Quay lui có làm việc một cách đúng đắn hay không nếu chúng ta sử
dụng chỉ một trong hai bất đẳng thức để chấm dứt một nút không khả thi?
Bài 9. Viết một chương trình cài đặt thuật toán Quay lui cho bài toán chu trình
Hamilton.
Bài 10. Trò chơi ghép hình các chốt. Trò chơi này được chơi trên một bảng với 15 lỗ
nhỏ được sắp xếp trong một tam giác đều. Ở một vị trí ban đầu, 14 lỗ được chiếm đóng
bởi chốt, như hình bên dưới. Một nước đi hợp lệ là một bước nhảy của một chốt qua
chốt liền kề với nó vào một lỗ trống, chốt bị nhảy qua sẽ bị loại khỏi bảng;

Thiết kế và cài đặt thuật toán Quay lui để giải quyết phiên bản sau của câu đố
này:
a. Bắt đầu với một vị trí nhất định của lỗ trống, tìm một chuỗi ngắn nhất để di
chuyển mà loại bỏ 14 chốt không có giới hạn về vị trí ngoài của chốt còn lại.
b. Bắt đầu với một vị trí xác định của lỗ trống, tìm một chuỗi ngắn nhất để di
chuyển mà loại bỏ 14 chốt, với các chốt còn lại tại các lỗ trống ban đầu của bảng.

Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Tài liệu tham khảo 127

TÀI LIỆU THAM KHẢO

Tiếng Việt
[1]. Đinh Mạnh Tường, Cấu trúc dữ liệu và thuật toán, Nhà xuất bản khoa học
kĩ thuật (2000).
[2]. Hồ Anh Minh, Bài giảng nhập môn thuật toán, Khoa Công nghệ thông tin
– trường Đại học Quy Nhơn.
[3]. Hồ Thuần, Hồ Cẩm Hà, Trần Thiên Thành, Cấu trúc dữ liệu, phân tích
thuật toán và phát triển phần mềm, Nhà xuất bản giáo dục (2008).

Tiếng Anh
[1]. Anany Levitin, Introduction to the Design and Analysis of Algorithms, 3rd
Edition, PearsonMcGraw-Hill/ Osborne (2011).
[2]. Harsh Bhasin, ALGORITHMS: Design and Analysis, Oxford University Press
(2015).
[3]. Michael T. Goodrich, Roberto Tamassia, Algorithm Design and Applications,
Wiley (2015).
[4]. Thomas H. Cormen, Charles E. Leiserson, Ronald R. Rivest, Clifford Stein,
Introduction to Algorithm, 3rd Edition, MIT Press (2009).

Lê Xuân Việt – Dương Hoàng Huyên

You might also like