Professional Documents
Culture Documents
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
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 1. Giới thiệu 5
Begin
n0 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
Begin
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.
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 1. Giới thiệu 9
Begin
us ← 1; i ← 1; j ← 1
i ≤ u
và Sai
return us End
j ≤ v
Đúng
Đúng Sai
pi = qj
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ố.
Thiết kế thuật
toán.
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.
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 đó:
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.
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.
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)
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.
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.
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).
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
…
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⌋
k1
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)
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.
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
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
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
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
return −1
Ta dễ dàng đánh giá thuật toán này có độ phức tạp là O(n).
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:
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
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
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).
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ố.
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).
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!).
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:
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 đó.
Bài kích
toán thước n
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.
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
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
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.
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).
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
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:
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.
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.
//Đầ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.
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
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.
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 4. Giảm để trị 53
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.
Bài kích
toán thước 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ố.
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 =
Hình 5-2. Minh họa thuật toán sắp xếp trộn với 8 phần tử.
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:
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ử.
Đá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
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
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 5. Chia để trị 61
= 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.
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.
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
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
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:
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
return A
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
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).
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ì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).
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.
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 6. Biến đổi để trị 73
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.
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:
= 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:
Hệ số ai 2 1 3 1 5
x=3 2 3×21=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à:
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 6. Biến đổi để trị 75
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.
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:
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.
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.
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.
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ộ.
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).
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
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
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}).
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).
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
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
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
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
//Đầ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.
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 (nk)!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:
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:
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.
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.
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
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.
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.
Khoa Công nghệ thông tin – Trường Đại học Quy Nhơn
Chương 8. Kĩ thuật tham lam 105
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
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.
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-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.
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.
đườ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:
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
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
đã 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])
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]
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)
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
đườ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.
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:
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.
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.
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.
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
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.
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.
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.
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
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).