You are on page 1of 173

TRƯỜNG ĐẠI HỌC BÁCH KHOA HÀ NỘI

VIỆN CÔNG NGHỆ THÔNG TIN VÀ TRUYỀN THÔNG

Cấu trúc dữ liệu và thuật toán

Nguyễn Khánh Phương

Computer Science department


School of Information and Communication technology
E-mail: phuongnk@soict.hust.edu.vn
Nội dung khóa học
Chương 1. Các khái niệm cơ bản
Chương 2. Các sơ đồ thuật toán
Chương 3. Các cấu trúc dữ liệu cơ bản
Chương 4. Cây
Chương 5. Sắp xếp
Chương 6. Tìm kiếm
Chương 7. Đồ thị
2
TRƯỜNG ĐẠI HỌC BÁCH KHOA HÀ NỘI
VIỆN CÔNG NGHỆ THÔNG TIN VÀ TRUYỀN THÔNG

Chương 2. Các sơ đồ thuật toán

Nguyễn Khánh Phương

Computer Science department


School of Information and Communication technology
E-mail: phuongnk@soict.hust.edu.vn
N i dung
1. Duyệt toàn bộ
2. Thuật toán đệ qui
3. Thuật toán quay lui
4. Chia để trị
5. Quy hoạch động

4
N i dung
1. Duyệt toàn bộ
2. Thuật toán đệ qui
3. Thuật toán quay lui
4. Chia để trị
5. Quy hoạch động

5
1. Duyệt toàn bộ
Duyệt toàn bộ (Brute force – Exhaustive search):
• Khi bài toán yêu cầu tìm đối tượng thỏa mãn tính chất nào đó trong tập các đối
tượng đã cho, ta có thể áp dụng phương pháp duyệt toàn bộ:
– Duyệt qua tất cả các đối tượng, với mỗi đối tượng, tiến hành kiểm tra xem
nó có thỏa mãn tính chất yêu cầu hay không, nếu có thì đối tượng đó là lời
giải cần tìm, nếu không thì tiếp tục tìm.
Ví dụ: Bài toán người du lịch (Traveling Salesman Problem): Một người du lịch
muốn đi tham quan n thành phố T1, T2, ..., Tn. Hành trình là cách đi xuất phát từ
một thành phố nào đó đi qua tất cả các thành phố còn lại, mỗi thành phố đúng một
lần, rồi quay trở lại thành phố xuất phát. Biết dij là chi phí đi từ thành phố Ti đến
thành phố Tj (i, j = 1, 2,..., n). Tìm hành trình với tổng chi phí là nhỏ nhất.
Giải: duyệt toàn bộ cần tính toán tổng cộng n! hành trình có thể có, mỗi hành trình
tính chi phí đường đi tương ứng, và so sánh chi phí n! hành trình này với nhau để
đưa ra được hành trình có chi phí nhỏ nhất.
• Duyệt toàn bộ: đơn giản, nhưng thời gian tính không hiệu quả.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ: bài toán stock
Bài toán thường được hỏi trong các cuộc phỏng vấn của Google và Amazon:

Ví dụ:
Giá stock trong 6 ngày là {100, 60, 70, 65, 80, 85}, khi đó span = {0,0,1,0,3,4}.
Giải bằng phương pháp duyệt toàn bộ:
Duyệt lần lượt từng ngày i (từ trái sang phải for i = 0,..,5):
• span[i] = 0;
• quét lần lượt các ngày j trước nó (for j = i-1,..,0):
– if price[j] <= price[i] then span[i]++;
– else break;

Độ phức tạp: O(n2) trong đó n là số ngày.


7
Bài tập: đưa ra thuật toán với thời gian tính O(n) và bộ nhớ O(n)
Ví dụ: Một dạng phát biểu khác của bài toán stock

NGUYỄN KHÁNH PHƯƠNG 8


Bộ môn KHMT – ĐHBK HN
Bài tập: Vito’s family

NGUYỄN KHÁNH PHƯƠNG 9


Bộ môn KHMT – ĐHBK HN
N i dung
1. Duyệt toàn bộ
2. Thuật toán đệ qui
3. Thuật toán quay lui
4. Chia để trị
5. Quy hoạch động

NGUYỄN KHÁNH PHƯƠNG 10


Bộ môn KHMT – ĐHBK HN
2. Thuật toán đệ qui
2.1. Khái niệm đệ qui
2.2. Sơ đồ thuật toán đệ qui
2.3. Một số ví dụ minh hoạ
2.4. Phân tích thuật toán đệ qui
2.5. Đệ qui có nhớ

NGUYỄN KHÁNH PHƯƠNG 11


Bộ môn KHMT – ĐHBK HN
2.1. Khái niệm đệ qui
• Trong thực tế ta thường gặp những đối tượng bao gồm chính nó hoặc
được định nghĩa dưới dạng của chính nó. Ta nói các đối tượng đó
được xác định một cách đệ qui.
• Ví dụ:
– Điểm quân số
– Fractal
– Các hàm được định nghĩa đệ qui
– Tập hợp được định nghĩa đệ qui
– Định nghĩa đệ qui của cây
– ...

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ Đệ qui: Điểm quân

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ Đệ qui: Điểm quân

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ Đệ qui: Điểm quân

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ Đệ qui: Điểm quân

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ Đệ qui: Điểm quân

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ Đệ qui: Điểm quân

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ Đệ qui: Điểm quân

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ Đệ qui: Điểm quân

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ Đệ qui: Điểm quân

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ Đệ qui: Điểm quân

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ Đệ qui: Điểm quân

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ: The Handshake Problem
Có n người trong phòng. Nếu mỗi người đều bắt tay với n-1
người còn lại, mỗi người đúng 1 lần. Tính h(n) là tổng số lần
bắt tay có thể có.
h(n) = h(n-1) + n-1 h(4) = h(3) + 3 h(3) = h(2) + 2 h(2) = 1

h(n): Tổng các số nguyên từ 1 đến n-1 = n(n-1) / 2


Ví dụ Đệ qui: Fractals

fractals là ví dụ về hình ảnh được xây dựng một


cách đệ qui (đối tượng lặp lại một cách đệ qui).
Hàm đệ qui (Recursive Functions)
Các hàm đệ qui được xác định phụ thuộc vào biến nguyên không âm n theo sơ đồ sau:
Bước cơ sở (Basic Step): Xác định giá trị của hàm tại n=0: f(0).
Bước đệ qui (Recursive Step): Cho giá trị của f(k), k ≤ n, đưa ra qui tắc tính giá trị
của f(n+1).

Ví dụ 1:
f(0) = 3, n=0
f(n+1) = 2f(n) + 3, n>0
Khi đó ta có: f(1) = 2 × 3 + 3 = 9, f(2) = 2 × 9 + 3 = 21, ...
Hàm đệ qui (Recursive Functions)
Ví dụ 2: Định nghĩa đệ qui của n!
f(0) = 1
f(n) = n * f(n-1)

Để tính giá trị của hàm đệ qui ta thay thế dần theo định nghĩa đệ qui để thu được biểu
thức với đối số càng ngày càng nhỏ cho đến tận điều kiện đầu.
Chẳng hạn:

đệ qui

5! = 5 · 4! = 5 · 4 · 3! = 5 · 4 · 3 · 2! = 5 · 4 · 3 · 2 · 1!
= 5 · 4 · 3 · 2 · 1 · 0! = 5 · 4 · 3 · 2 · 1 · 1 = 120
int factorial(int n){
điều kiện đầu if (n==0)
return 1;
else
return n*factorial(n-1);
}
factorial(4);
int factorial(int n){
if (n==0)
factorial(4) else
return 1;

return n*factorial(n-1);
}
factorial(4);
int factorial(int n){
if (n==0)
factorial(4) else
return 1;

return n*factorial(n-1);
}

n=4
Returns 4*factorial(3)
factorial(4);
int factorial(int n){
if (n==0)
factorial(4) else
return 1;

return n*factorial(n-1);
}

n=4
Returns 4*factorial(3)
n=3
Returns 3*factorial(2)
factorial(4);
int factorial(int n){
if (n==0)
factorial(4) else
return 1;

return n*factorial(n-1);
}

n=4
Returns 4*factorial(3)
n=3
Returns 3*factorial(2)
n=2
Returns 2*factorial(1)
factorial(4);
int factorial(int n){
if (n==0)
factorial(4) else
return 1;

return n*factorial(n-1);
}

n=4
Returns 4*factorial(3)
n=3
Returns 3*factorial(2)
n=2
Returns 2*factorial(1)
n=1
Returns 1*factorial(0)
factorial(4);
int factorial(int n){
if (n==0)
factorial(4) else
return 1;

return n*factorial(n-1);
}

n=4
Returns 4*factorial(3)
n=3
Returns 3*factorial(2)
n=2
Returns 2*factorial(1)
n=1
Returns 1*factorial(0)

n=0
Returns 1
factorial(4);
int factorial(int n){
if (n==0)
factorial(4) else
return 1;

return n*factorial(n-1);
}

n=4
Returns 4*factorial(3)
n=3
Returns 3*factorial(2)
n=2
Returns 2*factorial(1)
n=1
Returns 1*factorial(0)

1
factorial(4);
int factorial(int n){
if (n==0)
factorial(4) else
return 1;

return n*factorial(n-1);
}

n=4
Returns 4*factorial(3)
n=3
Returns 3*factorial(2)
n=2
Returns 2*factorial(1)

1
factorial(4);
int factorial(int n){
if (n==0)
factorial(4) else
return 1;

return n*factorial(n-1);
}

n=4
Returns 4*factorial(3)
n=3
Returns 3*factorial(2)

2
factorial(4);
int factorial(int n){
if (n==0)
factorial(4) else
return 1;

return n*factorial(n-1);
}

n=4
Returns 4*factorial(3)

6
factorial(4);
int factorial(int n){
if (n==0)
factorial(4) else
return 1;

24
return n*factorial(n-1);
}
int factorial(int 3)
{
int fact;
if (n > 1)
fact = factorial(2) * 3;
else
fact = 1;
return fact;
}
int factorial(int 3)
{
int fact;
if (n > 1)
fact = factorial(2) * 3;
else
fact = 1;
return fact;
}

int factorial(int 2)
{
int fact;
if (n > 1)
fact = factorial(1) * 2;
else
fact = 1;
return fact;
}
int factorial(int 3)
{
int fact;
if (n > 1)
fact = factorial(2) * 3;
else
fact = 1;
return fact;
}

int factorial(int 2)
{
int fact;
if (n > 1)
fact = factorial(1) * 2;
else
fact = 1;
return fact;
}

int factorial(int 1)
{
int fact;
if (n > 1)
fact = factorial(n - 1) * n;
else
fact = 1;
return fact;
}
int factorial(int 3)
{
int fact;
if (n > 1)
fact = factorial(2) * 3;
else
fact = 1;
return fact;
}

int factorial(int 2)
{
int fact;
if (n > 1)
fact = factorial(1) * 2;
else
fact = 1;
return fact;
}

int factorial(int 1)
{
int fact;
if (n > 1)
fact = factorial(n - 1) * n;
else
fact = 1;
return 1;
}
int factorial(int 3)
{
int fact;
if (n > 1)
fact = factorial(2) * 3;
else
fact = 1;
return fact;
}

int factorial(int 2)
{
int fact;
if (n > 1)
fact = 1 * 2;
else
fact = 1;
return fact;
}

int factorial(int 1)
{
int fact;
if (n > 1)
fact = factorial(n - 1) * n;
else
fact = 1;
return 1;
}
int factorial(int 3)
{
int fact;
if (n > 1)
fact = factorial(2) * 3;
else
fact = 1;
return fact;
}

int factorial(int 2)
{
int fact;
if (n > 1)
fact = 1 * 2;
else
fact = 1;
return 2;
}
int factorial(int 3)
{
int fact;
if (n > 1)
fact = 2 * 3;
else
fact = 1;
return fact;
}

int factorial(int 2)
{
int fact;
if (n > 1)
fact = 1 * 2;
else
fact = 1;
return 2;
}
int factorial(int 3)
{
int fact;
if (n > 1)
fact = 2 * 3;
else
fact = 1;
return 6;
}
Chương trình chạy theo thứ tự thế nào?

Thực thi hàm factorial(4) sẽ dừng cho n=4


đến khi hàm factorial(3) trả về kết quả Returns 4*factorial(3)
n=3
Khi hàm factorial(3) trả về kết quả, hàm Returns 3*factorial(2)
factorial(4) tiếp tục được thực hiện
Các hàm đệ quy đều có thể cài đặt lại bằng cách sử dụng vòng lặp while/for

• Chúng ta cũng có thể cài đặt tính n! bằng cách sử dụng vòng lặp while

int factorial (int n)


{
i=n; fact = 1;
int factorial(int n){
while(i >= 1) if (n==0)
{ return 1;
else
fact=fact*i; return n*factorial(n-1);
i--; }

}
return fact;
}
Chú ý: Việc thay thế hàm đệ qui bởi hàm không đệ qui thường được gọi
là việc khử đệ qui. Khử đệ qui không phải bao giờ cũng là dễ thực hiện
như trong tình huống bài toán tính giai thừa.
Ví dụ 3
Viết hàm prod(list) trả về tích các số có trong danh sách list
Ví dụ: prod({1,3,3,4}) = 36

Gợi ý: Xây dựng hàm thức đệ qui:


helperProd(list, k) = list[k]*list[k+1]*…list[listSize-1]

list[0] * list[1] * list[2] * list[3]


helperProd(list, 3)

helperProd(list, 2)

helperProd(list, 1)

helperProd(list, 0)
Ví dụ 3
Viết hàm prod(list) trả về tích các số có trong danh sách list
Ví dụ:
prod({1,3,3,4}) = 36
Hàm đệ qui (Recursive Functions)
Bài tập: Viết hàm đệ quy tính tổng dãy số a0, a1, …, an-1
n 1
Định nghĩa đệ qui của tổng sn   ak
k 0
s1 = a0
sn = sn-1 + an-1

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ 4. Fibonacci - Sự phát triển của bày thỏ
Người nông dân nuôi một cặp thỏ mới sinh. Khi được 2 tháng tuổi, cứ mỗi cặp thỏ lại sinh ra
một cặp thỏ khác sau mỗi tháng. Hỏi người nông dân đó có bao nhiêu con thỏ sau n tháng ?
 n = 1: f1 = 1
 n = 2: f2 = 1 Dãy Fibonacci: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,...
 n > 2: fn = fn-1 + fn-2
Công thức đúng cho n>2 vì mỗi cặp thỏ mới được sinh ra từ một cặp thỏ ít nhất 2 tháng tuổi

New-born Month 1-month rabbits >=2-month rabbits Tổng fn


rabbits
1 1 0 1
2 0 1 1
3 1 1 2
4 1 2 3
5 2 3 5
6 3 5 8
7 5 8 13
8 8 13 21
Ví dụ 4. Fibonacci
Hàm đệ quy F(n) :
• Giá trị đầu : F(n=0) =0; F(n=1) = 1
• Công thức đệ quy: F(n) = F(n-1) + F(n-2)
Cài đặt đệ quy Cài đặt dùng vòng lặp
int F(int n) int F(int n)
if n < 2 then if n < 2 then return n;
return n; else
{
else x= 0; F(n-2)
return F(n-1) + F(n-2); y= 1; F(n-1)
for k = 1 to n-1
{ z = x+y; F(n)
x = y;
y = z;
}
} NGUYỄN KHÁNH PHƯƠNG
return y; //y isBộF(n)
môn KHMT – ĐHBK HN
53
2. Thuật toán đệ qui
2.1. Khái niệm đệ qui
2.2. Sơ đồ thuật toán đệ qui
2.3. Một số ví dụ minh hoạ
2.4. Phân tích thuật toán đệ qui
2.5. Đệ qui có nhớ

NGUYỄN KHÁNH PHƯƠNG 54


Bộ môn KHMT – ĐHBK HN
Định nghĩa
• Thuật toán đệ qui là thuật toán tự gọi đến chính mình với đầu vào kích
thước nhỏ hơn.
• Việc phát triển thuật toán đệ qui là thuận tiện khi cần xử lý với các đối tượng
được định nghĩa đệ qui (chẳng hạn: tập hợp, hàm, cây, ...trong các ví dụ nêu
trong mục trước).
• Các thuật toán được phát triển dựa trên phương pháp chia để trị thông
thường được mô tả dưới dạng các thuật toán đệ qui (xem ví dụ mở đầu)
• Các ngôn ngữ lập trình cấp cao thường cho phép xây dựng các hàm (thủ tục)
đệ qui, nghĩa là trong thân của hàm (thủ tục) có chứa những lệnh gọi đến
chính nó. Vì thế, khi cài đặt các thuật toán đệ qui người ta thường xây dựng
các hàm (thủ tục) đệ qui.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Cấu trúc của thuật toán đệ qui
Thuật toán RecAlg(input)
{
if (kích thước của input là nhỏ nhất) then
Thực hiện Bước cơ sở; //giải bài toán kích thước đầu vào nhỏ nhất
else
{
RecAlg(input với kích thước nhỏ hơn); // bước đệ qui
// có thể có thêm những lệnh gọi đệ qui
Tổng hợp lời giải của các bài toán con để thu được lời_giải;
return (lời_giải) ;
}
}
Thời gian tính:
T(n) = if (base case) then chi phí hằng số
else ( thời gian giải các bài toán con +
thời gian tổng hợp lời giải)
Kết quả thời gian tính phụ thuộc vào:
– Số lượng bài toán con
– Kích thước bài toán con
– Thời gian tổng hợp lời giải các bài toán con

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
2. Thuật toán đệ qui
2.1. Khái niệm đệ qui
2.2. Sơ đồ thuật toán đệ qui
2.3. Một số ví dụ minh hoạ
2.4. Phân tích thuật toán đệ qui
2.5. Đệ qui có nhớ

NGUYỄN KHÁNH PHƯƠNG 57


Bộ môn KHMT – ĐHBK HN
Cài đặt các thuật toán đệ qui
• Để cài đặt các thuật toán đệ qui trên các ngôn ngữ lập trình,
ta thường xây dựng các hàm (thủ tục) đệ qui.
• Trong mục này ta xét một số ví dụ minh hoạ cài đặt thuật
toán đệ qui:
– Hàm đệ qui tính n!
– Hàm đệ qui tính số Fibonacci
– Hàm đệ qui tính hệ số nhị thức
– Tìm kiếm nhị phân
– Bài toán tháp Hà nội

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ 1: Tính hệ số nhị thức
• Hệ số nhị thức C(n,k) được định nghĩa đệ qui như sau:
C(n,0) = 1, C(n,n) =1; với mọi n >=0,
C(n,k) = C(n-1,k-1)+C(n-1,k), 0 < k < n

• Cài đặt hàm đệ qui trên C:


int C(int n, int k){
if ((k==0)||(k==n)) return 1;
else return C(n-1,k-1)+C(n-1,k);
}

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ 2: Tìm kiếm nhị phân
Bài toán: Cho mảng số A[1..n] được sắp xếp theo thứ tự không giảm và số K. Cần tìm
chỉ số i (1  i  n) sao cho A[i] = K.
• Để đơn giản ta giả thiết rằng chỉ số như vậy là tồn tại. Thuật toán để giải bài toán
được xây dựng dựa trên lập luận sau: Số K cho trước
– hoặc là bằng phần tử nằm ở vị trí ở giữa mảng A
– hoặc là nằm ở nửa bên trái (L) của mảng A
– hoặc là nằm ở nửa bên phải (R) của mảng A.
(Tình huống L (R) xảy ra chỉ khi K nhỏ hơn (lớn hơn) phần tử ở giữa của mảng A)

K = 95

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ 2: Tìm kiếm nhị phân
int binsearch(int low, int high, int A[], int K)
{
middle = (low + high)/2;
if (A[middle] == K) return middle;
else {
if (A[middle] > K)
return binsearch(low, middle-1, A, K);
else // A[middle] < K:
return binsearch(middle+1, high, A, K)
}
}

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ 3. Bài toán tháp Hà nội
Trò chơi Tháp Hà nội được trình bày như sau: “Có 3 cọc a, b, c. Trên cọc a có một
chồng gồm n cái đĩa, đường kính giảm dần từ dưới lên trên. Cần phải chuyển chồng
đĩa từ cọc a sang cọc c tuân thủ quy tắc:
1. Mỗi lần chỉ chuyển 1 đĩa
2. Chỉ được xếp đĩa có đường kính nhỏ hơn lên trên đĩa có đường kính lớn hơn.
Trong qu¸ trình chuyÓn ®ưîc phÐp dïng cäc b lµm cäc trung gian.
Bài toán đặt ra là: Tìm số lần di chuyển đĩa ít nhất cần thực hiện để hoàn thành nhiệm
vụ đặt ra trong trò chơi Tháp Hà nội.

62
Cọc a Cọc c Cọc b
Tower of Hanoi: n=5
1. Mỗi lần chỉ chuyển 1 đĩa
2. Chỉ được xếp đĩa có đường kính nhỏ hơn lên trên đĩa có đường kính lớn hơn

Cọc a Cọc c Cọc b 63


Bài toán: chuyển n đĩa từ cọc a sang cọc c sử dụng cọc b làm trung gian
Cần tìm hn là sè lÇn di chuyÓn ®Üa Ýt nhÊt cÇn thùc hiÖn

hn = 2hn-1 + 1, n ≥ 2.
Việc di chuyển đĩa gồm 3 giai đoạn:
(1) ChuyÓn n-1 ®Üa tõ cäc a ®Õn cäc b sö dông cäc c lµm trung gian.
Bài toán kích thước n-1  Số lần di chuyển = hn-1
(2) ChuyÓn 1 ®Üa (®Üa víi ®ưêng kÝnh lín nhÊt) tõ cäc a ®Õn cäc c.
 Số lần di chuyển = 1
(3) ChuyÓn n-1 ®Üa tõ cäc b ®Õn cäc c (sö dông cäc a lµm trung gian).
Bài toán kích thước n-1  Số lần di chuyển = hn-1

Cọc a Cọc c Cọc b 64


Tower of Hanoi: n=5

(1) ChuyÓn n-1 ®Üa tõ cäc a ®Õn cäc b sö dông cäc c lµm trung gian.
Bài toán kích thước n-1  Số lần di chuyển = hn-1
(2) ChuyÓn 1 ®Üa (®Üa víi ®ưêng kÝnh lín nhÊt) tõ cäc a ®Õn cäc c.
 Số lần di chuyển = 1
(3) ChuyÓn n-1 ®Üa tõ cäc b ®Õn cäc c (sö dông cäc a lµm trung gian).
Bài toán kích thước n-1  Số lần di chuyển = hn-1

Cọc a Cọc c Cọc b 65


Thuật toán tháp Hà nội
Thuật toán có thể mô tả trong thủ tục đệ qui sau đây:
//chuyển n đĩa từ cọc a sang cọc c sử dụng cọc b làm trung gian:
HanoiTower(n, a, c, b);
{
if (n==1) then <chuyển đĩa từ cọc a sang cọc c>
else
{
HanoiTower(n-1,a,b,c);
HanoiTower(1,a,c,b);
HanoiTower(n-1,b,c,a);
}
}
Cài đặt
Ví dụ 4: Palindrome
• Định nghĩa. Ta gọi palindrome là xâu mà đọc nó từ trái qua phải cũng
giống như đọc nó từ phải qua trái.
Ví dụ: NOON, DEED, RADAR, MADAM
Able was I ere I saw Elba
• Để nhận biết một xâu cho trước có phải là palindrome hay không: ta tiến
hành
– So sánh kí tự đầu và kí tự cuối của xâu.
• Nếu bằng nhau: xâu mới = xâu cũ bỏ đi kí tự đầu và kí tự cuối. Lặp
lại bước so sánh.
• Nếu khác nhau: xâu đã cho không phải là palindrome
Ví dụ 4: Palindrome: xau[start….end]
• Bước cơ sở (Base case) : xâu chỉ có <=1 kí tự (end – start <=1)
return true
• Bước đệ quy:
return true if (xau[start]==xau[stop] &&
palindrome(xau, start+1, end-1))
2. Thuật toán đệ qui
2.1. Khái niệm đệ qui
2.2. Sơ đồ thuật toán đệ qui
2.3. Một số ví dụ minh hoạ
2.4. Phân tích thuật toán đệ qui
2.5. Đệ qui có nhớ

70
2.4. Phân tích thuật toán đệ qui
• Để phân tích thuật toán đệ qui ta thường tiến hành như sau:

– Gọi T(n) là thời gian tính của thuật toán

– Xây dựng công thức đệ qui cho T(n).

– Giải công thức đệ qui thu được để đưa ra đánh giá cho T(n)

• Nói chung ta chỉ cần một đánh giá sát cho tốc độ tăng của T(n) nên việc giải
công thức đệ qui đối với T(n) là đưa ra đánh giá tốc độ tăng của T(n) trong
ký hiệu tiệm cận
Ví dụ: Tìm kiếm nhị phân
int binsearch(int low, int high, int A[],int K)
{
mid = (low+high)/2;
if (A[m] == K) return mid;
else if (A[mid] > K)
return binsearch(low,mid-1, A, K);
else //A[mid] < K
return binsearch(mid+1,high, A, K);
}

• Gọi T(n) là thời gian tính của việc thực hiện binsearch(0, n-1, A, K), ta có
T(1) = c
T(n) = T(n/2) + d
trong đó c và d là các hằng số.
Giải công thức đệ quy:
T(n) = T(n/2) + d, T(1) = c
Bước lặp Chi phí
0 T(n) 0

1 T(n/2) d

2 T(n/4) d

………

log2n T(1) d

• Giá trị hàm T(n) bằng tổng các giá trị tại tất cả các mức:
= c + dlog2n

 Ta có T(n) = O(log2n)
Giải công thức đệ quy:
T(n) = T(n/2) + d, T(1) = c
T(n) = T(n/2) + d
= T(n/4) + d + d
= T(n/8) + d + d + d
= ….
= T(n/2k) + kd
= T(1) + d log2n với k = log2n
= c + d log2n = O(log2n)

 Ta có T(n) = O(log2n)
2. Thuật toán đệ qui
2.1. Khái niệm đệ qui
2.2. Sơ đồ thuật toán đệ qui
2.3. Một số ví dụ minh hoạ
2.4. Phân tích thuật toán đệ qui
2.5. Đệ qui có nhớ

75
2.5. Đệ qui có nhớ
• Trong phần trước ta đã thấy các thuật toán đệ qui để tính số
Fibonacci và tính hệ số nhị thức là kém hiệu quả.
• Để tăng hiệu quả của các thuật toán đệ qui mà không cần
tiến hành xây dựng các thủ tục lặp hay khử đệ qui, ta có thể
sử dụng kỹ thuật đệ qui có nhớ.
• Sử dụng kỹ thuật này, trong nhiều trường hợp, ta giữ
nguyên được cấu trúc đệ qui của thuật toán và đồng thời lại
đảm bảo được hiệu quả của nó. Nhược điểm lớn nhất của
cách làm này là đòi hỏi về bộ nhớ.
Bài toán con trùng lặp
• Nhận thấy là trong các thuật toán đệ qui là mỗi khi cần đến
lời giải của một bài toán con ta lại phải trị nó một cách đệ
qui. Do đó, có những bài toán con bị giải đi giải lại nhiều
lần. Điều đó dẫn đến tính kém hiệu quả của thuật toán. Hiện
tượng này gọi là hiện tượng bài toán con trùng lặp.
Ví dụ: Thuật toán tính hệ số nhị thức C(5,3). Cây đệ qui thực
hiện lệnh gọi hàm C(5,3) được chỉ ra trong hình sau đây
Ví dụ: Bài toán con trùng lặp khi tính Fibonaci F(4)
int F (int n) {
if n <2 then return n;
else return F (n-1)+F(n-2);
}

F (4)

F (3) F (2)

F (2) F (1) F (1) F(0)

F (1) F (0)
Ví dụ: Bài toán con trùng lặp trong việc tính C(5,3)

C(5,3)

C(4,2) C(4,3)

C(3,1) C(3,2) C(3,2) C(3,3)

C(2,0) C(2,1) C(2,1) C(2,2) C(2,1) C(2,2)

C(1,0) C(1,1) C(1,0) C(1,1) C(1,0) C(1,1)


Đệ qui có nhớ
• Để khắc phục hiện tượng này, ý tưởng của đệ qui có nhớ là: Ta sẽ dùng
biến ghi nhớ lại thông tin về lời giải của các bài toán con ngay sau lần
đầu tiên nó được giải. Điều đó cho phép rút ngắn thời gian tính của thuật
toán, bởi vì, mỗi khi cần đến có thể tra cứu mà không phải giải lại những
bài toán con đã được giải trước đó.
Ví dụ: Thuật toán đệ qui tính hệ số nhị thức, ta đưa vào biến
• D[n][k] để ghi nhận những giá trị đã tính.
• Đầu tiên D[n][k]=0, mỗi khi tính được C(n, k) giá trị này sẽ được ghi
nhận vào D[n][k]. Như vậy, nếu D[n][k]>0 thì điều đó có nghĩa là không
cần gọi đệ qui hàm C(n, k)
Ví dụ: Hàm tính C(n,k) có nhớ
int C(int n,int k){
if (D[n][k]>0) return D[n][k];
else{
D[n][k] = C(n-1,k-1)+C(n-1,k);
return D[n][k];
}
}
Trước khi gọi hàm C(n, k) cần khởi tạo mảng D[ ][ ] như sau:
• D[i][0] = 1, D[i][n]=1, với i = 0,1,..., n;
• D[i][j] = 0, với những i, j còn lại.
N i dung
1. Duyệt toàn bộ
2. Thuật toán đệ qui
3. Thuật toán quay lui
4. Chia để trị
5. Quy hoạch động

82
Sơ đồ thuật toán quay lui
• Bài toán liệt kê (Q): Cho A1, A2,..., An là các tập hữu hạn. Ký hiệu
A = A1 A2  ... An = { (a1, a2, ..., an): ai  Ai , i=1, 2, ..., n}.
Giả sử P là tính chất cho trên A. Vấn đề đặt ra là liệt kê tất cả các
phần tử của A thoả mãn tính chất P:
D = { a = (a1, a2, ..., an)  A: a thoả mãn tính chất P }.
• Các phần tử của tập D được gọi là các lời giải chấp nhận được.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Sơ đồ thuật toán quay lui
Tất cả các bài toán liệt kê tổ hợp cơ bản đều có thể phát biểu dưới dạng
bài toán liệt kê (Q).
Ví dụ:
• Bài toán liệt kê xâu nhị phân độ dài n dẫn về việc liệt kê các phần tử
của tập
Bn = {(a1, ..., an): ai  {0, 1}, i=1, 2, ..., n}.
• Bài toán liệt kê các tập con m phần tử của tập N = {1, 2, ..., n} đòi hỏi
liệt kê các phần tử của tập:
S(m,n) = {(a1,..., am)Nm: 1 ≤ a1 < ... < am ≤ n }.
• TËp c¸c ho¸n vÞ cña c¸c sè tù nhiªn 1, 2, ..., n lµ tËp
n = {(a1,..., an)  Nn: ai ≠ aj ; i ≠ j }.
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
Lời giải bộ phận
Lời giải của bài toán là bộ có thứ tự gồm n thành phần (a1, a2,
..., an), trong đó ai  Ai , i = 1, 2, ..., n.
Định nghĩa. Ta gọi lời giải bộ phận cấp k (0≤k≤ n) là bộ có
thứ tự gồm k thành phần
(a1, a2, ..., ak),
trong đó ai  Ai , i = 1, 2, ..., k.
• Khi k = 0, lời giải bộ phận cấp 0 được ký hiệu là ( ) và còn
được gọi là lời giải rỗng.
• Nếu k = n, ta có lời giải đầy đủ hay đơn giản là một lời giải
của bài toán.
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
Sơ đồ thuật toán quay lui
Thuật toán quay lui được xây dựng dựa trên việc xây dựng dần
từng thành phần của lời giải.
• Thuật toán bắt đầu từ lời giải rỗng ( ).
• Trên cơ sở tính chất P ta xác định được những phần tử nào
của tập A1 có thể chọn vào vị trí thứ nhất của lời giải.
Những phần tử như vậy ta sẽ gọi là những ứng cử viên (viết
tắt là UCV) vào vị trí thứ nhất của lời giải. Ký hiệu tập các
UCV vào vị trí thứ nhất của lời giải là S1. Lấy a1  S1, bổ
sung nó vào lời giải rỗng đang có ta thu được lời giải bộ
phận cấp 1: (a1).

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Sơ đồ thuật toán quay lui
• Bước tổng quát: Giả sử ta đang có lời giải bộ phận cấp k-1:
(a1, a2, ..., ak-1),
cần xây dựng lời giải bộ phận cấp k:
(a1, a2, ..., ak-1, ak)
– Trên cơ sở tính chất P, ta xác định được những phần tử nào của tập Ak có
thể chọn vào vị trí thứ k của lời giải.
– Những phần tử như vậy ta sẽ gọi là những ứng cử viên (viết tắt là UCV)
vào vị trí thứ k của lời giải khi k-1 thành phần đầu của nó đã được chọn
là (a1, a2, ..., ak-1). Ký hiệu tập các ứng cử viên này là Sk.
– Xét 2 tình huống:
• Sk ≠ 
NGUYỄN KHÁNH PHƯƠNG
• Sk =  Bộ môn KHMT – ĐHBK HN
Sơ đồ thuật toán quay lui
• Sk ≠ : Lấy ak  Sk bổ sung nó vào lời giải bộ phận cấp k-1 đang có (a1, a2, ...,
ak-1) ta thu được lời giải bộ phận cấp k (a1, a2, ..., ak-1, ak). Khi đó
– Nếu k = n thì ta thu được một lời giải,
– Nếu k < n, ta tiếp tục đi xây dựng thành phần thứ k+1 của lời giải.
• Sk=: Điều đó có nghĩa là lời giải bộ phận (a1, a2, ..., ak-1) không thể tiếp tục
phát triển thành lời giải đầy đủ. Trong tình huống này ta quay trở lại tìm ứng cử
viên mới vào vị trí thứ k-1 của lời giải (chú ý: ứng cử viên mới này nằm trong tập
Sk-1)
– Nếu tìm thấy UCV như vậy, thì bổ sung nó vào vị trí thứ k-1 rồi lại tiếp tục
đi xây dựng thành phần thứ k.
– Nếu không tìm được thì ta lại quay trở lại thêm một bước nữa tìm UCV
mới vào vị trí thứ k-2, ... Nếu quay lại tận lời giải rỗng mà vẫn không tìm
được UCV mới vào vị trí thứ 1, thì thuật toán kết thúc.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Cây liệt kê lời giải theo thuật toán quay lui

Gốc (lời giải rỗng)


Tập UCV S1
a1

(a1)
Tập UCV S2 khi đã có (a1)
a2

(a1, a2)

Tập UCV S3
khi đã có (a1, a2)
(a1, a2, a3)
a3
Tập UCV S4
khi đã có (a1, a2 , a3)

a4 a’4
Thuật toán quay lui (đệ qui) Thuật toán quay lui (không đệ qui)

void Try(int k) void Bactraking ( )


{ {
<Xây dựng Sk là tập chứa các ứng cử viên cho vị trí k=1;
thứ k của lời giải>; <Xây dựng Sk>;
for y  Sk //Với mỗi UCV y từ Sk while (k > 0) {
{ while (Sk   ) {
ak = y; ak  Sk; // Lấy ak từ Sk
if (k == n) then<Ghi nhận lời giải (a1, a2, ..., ak) >; if <(k == n) > then <Ghi nhận (a1,a2,...,ak) >;
else Try(k+1);
else {
Trả các biến về trạng thái cũ;
k = k+1;
}
} <Xây dựng Sk>;
}
Lệnh gọi để thực hiện thuật toán quay lui là: }
Try(1); k = k - 1; // Quay lui
• Nếu chỉ cần tìm một lời giải thì cần tìm cách }
}
chấm dứt các thủ tục gọi đệ qui lồng nhau
sinh bởi lệnh gọi Try(1) sau khi ghi nhận Lệnh gọi để thực hiện thuật toán quay lui là:
được lời giải đầu tiên. Bactraking ( );
• Nếu kết thúc thuật toán mà ta không thu
được một lời giải nào thì điều đó có nghĩa là NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
bài toán không có lời giải.
Hai vấn đề mấu chốt
• Để cài đặt thuật toán quay lui giải các bài toán tổ hợp cụ thể
ta cần giải quyết hai vấn đề cơ bản sau:
– Tìm thuật toán xây dựng các tập UCV Sk
– Tìm cách mô tả các tập này để có thể cài đặt thao tác liệt kê
các phần tử của chúng (cài đặt vòng lặp qui ước for y  Sk ).
• Hiệu quả của thuật toán liệt kê phụ thuộc vào việc ta có xác
định được chính xác các tập UCV này hay không.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Chú ý
• Nếu độ dài của lời giải là không biết trước và các lời giải cũng không nhất thiết phải
có cùng độ dài.

• Khi đó chỉ cần sửa lại câu lệnh


if (k == n) then <Ghi nhận lời giải (a1, a2, ..., ak) >;
else Try(k+1);
thành
if <(a1, a2, ..., ak) là lời giải> then <Ghi nhận (a1, a2, ..., ak) >;
else Try(k+1);
 Cần xây dựng hàm nhận biết (a1, a2, ..., ak) đã là lời giải hay chưa.
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
3. Thuật toán quay lui
3.1. Sơ đồ thuật toán quay lui
3.2. Một số ví dụ minh họa
– Ví dụ 1. Liệt kê xâu nhị phân độ dài n
– Ví dụ 2. Liệt kê các m-tập con của n-tập
– Ví dụ 3. Liệt kê hoán vị
– Ví dụ 4. Bài toán xếp hậu

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
93
Ví dụ 1: LiÖt kª x©u nhÞ ph©n ®é dµi n
• Bài toán liệt kê xâu nhị phân độ dài n dẫn về việc liệt kê các phần tử
của tập
Bn = {(b1, ..., bn): bi  {0, 1}, i=1, 2, ..., n}.
• Ta xét cách giải quyết hai vấn đề cơ bản để cài đặt thuật toán quay lui:
– Xây dựng tập ứng cử viên Sk: Rõ ràng ta có S1 = {0, 1}. Giả sử đã
có xâu nhị phân cấp k-1 (b1, ..., bk-1), khi đó rõ ràng Sk = {0,1}.
Như vậy, tập các UCV vào các vị trí của lời giải đã được xác định.
– Cài đặt vòng lặp liệt kê các phần tử của Sk: ta có thể sử dụng vòng
lặp for
for (y=0;y<=1;y++)

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
C©y liÖt kª d·y nhÞ ph©n ®é dµi 3

()
Sk = {0,1}
0 1
(0)
(1)
0 1 0 1
(00) (01) (10) (11)
0 1 0 1 0 1 0 1

(000) (001) (010) (011) (100) (101) (110) (111)

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Chương trình trên C++ (Đệ qui)
#include <iostream> void Try(int k){
using namespace std; int j;
int n, count; for (j = 0; j<=1; j++) {
int b[100]; b[k] = j;
if (k == n) Ghinhan();
void Ghinhan() { else Try(k+1);
int i, j; }
count++; }
cout<<"Xau thu " << count<<": ";
for (i=1 ; i<= n ;i++) { int main() {
j=b[i]; cout<<"Nhap n = ";cin>>n;
cout<<j<<" "; count = 0; Try(1);
} cout<<"So luong xau = "<<count<<endl;
cout<<endl; }
}

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Chương trình trên C++ (không đệ qui)

#include <iostream> void Xau( ) {


using namespace std; k=1; s[k]=0;
int n, count,k; while (k > 0) {
int b[100], s[100]; while (s[k] <= 1) {
b[k]=s[k];
void Ghinhan() { s[k]=s[k]+1;
int i, j; if (k==n) Ghinhan();
count++; else {
cout<<"Xau thu " << count<<": k++;
"; s[k]=0;
for (i=1 ; i<= n ;i++) { }
j=b[i]; }
cout<<j<<" "; k--; // Quay lui
} }
cout<<endl; }
}
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
Chương trình trên C++ (không đệ qui)

int main() {
cout<<"Nhap n = ";cin>>n;
count = 0; Xau();
cout<<"So luong xau = "<<count<<endl;

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
3. Thuật toán quay lui
3.1. Sơ đồ thuật toán quay lui
3.2. Một số ví dụ minh họa
– Ví dụ 1. Liệt kê xâu nhị phân độ dài n
– Ví dụ 2. Liệt kê các m-tập con của n-tập
– Ví dụ 3. Liệt kê hoán vị
– Ví dụ 4. Bài toán xếp hậu

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
99
Ví dụ 2. Liệt kê các m-tập con của n-tập
Bài toán: Liệt kê các tập con m phần tử của tập N = {1, 2, ..., n}.

Ví dụ: Liệt kê các tập con 3 phần tử của tập N = {1, 2, 3, 4, 5}


Giải: (1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5), (2, 3, 4), (2, 3, 5),
(2, 4, 5), (3, 4, 5)

 Bài toán dẫn về: Liệt kê các phần tử của tập:


S(m,n)={(a1,..., am)Nm: 1 ≤ a1<...<am≤ n}

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ 2. Liệt kê các m-tập con của n-tập
Ta xét cách giải quyết 2 vấn đề cơ bản để cài đặt thuật toán quay lui:
• Xây dựng tập ứng cử viên Sk:
– Từ điều kiện: 1  a1 < a2 < ... < am  n
suy ra S1 = {1, 2, ..., n-(m-1) }.
– Giả sử có tập con (a1, ..., ak-1). Từ điều kiện ak-1 < ak < . . . < am
≤ n, ta suy ra:
Sk = {ak-1+1, ak-1+2, ..., n-(m-k)}.
• Cài đặt vòng lặp liệt kê các phần tử của Sk:
for (y=a[k-1]+1;y<=n-m+k;y++) ...

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Chương trình trên C++ (Đệ qui)

#include <iostream> void Try(int k){


using namespace std; int j;
for (j = a[k-1] +1; j<= n-m+k; j++) {
int n, m, count; a[k] = j;
if (k==m) Ghinhan();
int a[100];
else Try(k+1);
void Ghinhan() { }
int i; }
count++; int main() {
cout<<"Tap con thu " <<count<<": "; cout<<"Nhap n, m = "; cin>>n; cin>>m;
for (i=1 ; i<= m ;i++) a[0]=0; count = 0; Try(1);
cout<<a[i]<<" "; cout<<"So tap con "<<m<<" phan tu cua tap
"<<n<<" phan tu = "<<count<<endl;
cout<<endl;
}
}

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Chương trình trên C++ (không đệ qui)

#include <iostream> void MSet(){


using namespace std; k=1; s[k]=1;
while(k>0){
int n, m, count,k; while (s[k]<= n-m+k) {
int a[100], s[100]; a[k]=s[k]; s[k]=s[k]+1;
void Ghinhan() { if (k==m) Ghinhan();
int i; else { k++; s[k]=a[k-1]+1; }
count++; } k--;
cout<<"Tap con thu " <<count<<": }
"; }
for (i=1 ; i<= m ;i++) int main() {
cout<<a[i]<<" "; cout<<"Nhap n, m = "; cin>>n; cin>>m;
cout<<endl; a[0]=0; count = 0; MSet();
} cout<<"So tap con "<<m<<" phan tu cua
tap "<<n<<" phan tu = "<<count<<endl;
}
Cây liệt kê S(5,3)

MSet(1); ()

1 3
2

(1) (2) (3)


2 4 4
3 3 4
(1,2) (1,3) (1,4) (2,3) (2,4) (3,4)
3 4 5 5 5 4 5 5 5
4

(1,2,3) (1,2,4) (1,2,5) (1,3,4) (1,3,5) (1,4,5) (2,3,4) (2,3,5) (2,4,5) (3,4,5)

Sk = {ak-1+1, ak-1+2, ..., n-(m-k)} NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
3. Thuật toán quay lui
3.1. Sơ đồ thuật toán quay lui
3.2. Một số ví dụ minh họa
– Ví dụ 1. Liệt kê xâu nhị phân độ dài n
– Ví dụ 2. Liệt kê các m-tập con của n-tập
– Ví dụ 3. Liệt kê hoán vị
– Ví dụ 4. Bài toán xếp hậu

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
105
Ví dụ 3. Liệt kê hoán vị
TËp c¸c ho¸n vÞ cña c¸c sè tù nhiªn 1, 2, ..., n lµ tËp:
n = {(x1,..., xn)  Nn: xi ≠ xj , i ≠ j }.

Bài toán: Liệt kê tất cả các phần tử của n

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Ví dụ 3. Liệt kê hoán vị
• Xây dựng tập ứng cử viên Sk:
– Râ rµng S1 = N. Gi¶ sö ta ®ang cã ho¸n vÞ bé phËn (a1, a2, ..., ak-1),
tõ ®iÒu kiÖn ai ≠ aj, víi mäi i ≠ j ta suy ra
Sk = N \ { a1, a2, ..., ak-1}.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Mô tả Sk

Xây dựng hàm nhận biết UCV:

bool UCV(int j, int k)


{
//UCV nhận giá trị true khi và chỉ khi j  Sk
int i;
for (i=1;i++;i<=k-1)
if (j == a[i]) return false;
return true;
}

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Liệt kê hoán vị
• Cài đặt vòng lặp liệt kê các phần tử của Sk:
for (y=1; y++; y <= n)
if (UCV(y, k) )
{
// y là UCV vào vị trí k
...
}

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Chương trình trên C++

#include <iostream> bool UCV(int j, int k)


using namespace std; {
int i;
int n, m, count; for (i=1; i<=k-1; i++)
int a[100]; if (j == a[i]) return false;
return true;
int Ghinhan() {
}
int i, j;
count++;
cout<<"Hoan vi thu "<<count<<": ";
for (i=1 ; i<= n ;i++)
cout<<a[i]<<" ";
cout<<endl;
}
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
void Try(int k) int main() {
{ cout<<("Nhap n = "); cin>>n;
int j; count = 0; Try(1);
for (j = 1; j<=n; j++) cout<<"So hoan vi = " << count;
if (UCV(j,k)) }
{ a[k] = j;
if (k==n) Ghinhan( );
else Try(k+1);
}
}
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
Cây liệt kê hoán vị của 1, 2, 3
()
Hoanvi(1);
S1 = ?
1 2 3

(1) (2) (3)


S2 = ?
2 3 1 3 1 2
(1,2) (1,3) (2,1) (2,3) (3,1) (3,2)
S3 = ? 3 1
3 2 2 1

(1,2,3) (1,3,2) (2,1,3) (2,3,1) (3,1,2) (3,2,1)

Sk = N \ { a1, a2, ..., ak-1}


3. Thuật toán quay lui
3.1. Sơ đồ thuật toán quay lui
3.2. Một số ví dụ minh họa
– Ví dụ 1. Liệt kê xâu nhị phân độ dài n
– Ví dụ 2. Liệt kê các m-tập con của n-tập
– Ví dụ 3. Liệt kê hoán vị
– Ví dụ 4. Bài toán xếp hậu

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
113
Ví dụ 4. Bài toán xếp hậu
• Liệt kê tất cả các cách xếp n quân Hậu trên bàn cờ nn sao
cho chúng không ăn được lẫn nhau, nghĩa là sao cho không
có hai con nào trong số chúng nằm trên cùng một dòng hay
một cột hay một đường chéo của bàn cờ.

n cột
The n-Queens Problem

Hai con hậu bất kỳ không được xếp


trên cùng một dòng ...
The n-Queens Problem

Hai con hậu bất kỳ không được xếp


trên cùng một cột ...
The n-Queens Problem

Hai con hậu bất kỳ không được


xếp trên cùng một dòng, một cột
hay một đường chéo!
Biểu diễn lời giải
• Đánh số các cột và dòng của bàn cờ từ 1 đến n.
• Một cách xếp hậu có thể biểu diễn bởi bộ có n thành phần
(a1, a2 ,..., an), trong đó ai là toạ độ cột của con hậu ở dòng i.
• Các điều kiện đặt ra đối với bộ (a1, a2 ,..., an):
– ai  aj , với mọi i  j (nghĩa là hai con hậu ở hai dòng i và j không
được nằm trên cùng một cột);
– | ai – aj |  | i – j |, với mọi i  j (nghĩa là hai con hậu ở hai ô (i, ai)
và (j, aj) không được nằm trên cùng một đường chéo).

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Phát biểu bài toán
• Như vậy bài toán xếp Hậu dẫn về bài toán liệt kê các phần
tử của tập:
D={(a1, a2, ..., an)Nn: ai ≠ aj và |ai – aj| ≠ |i – j|, i ≠ j }

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Bài toán xếp hậu: Duyệt toàn bộ
• Cần xếp N quân hậu lên bàn cờ có tất cả NxN ô, hỏi có tất cả bao nhiêu cách
xếp ?
– Quân thứ 1 có thể đặt vào 1 trong số N*N ô  có N2 cách
– Sau khi đặt quân thứ 1, tiến hành đặt quân thứ 2: bỏ qua ô đã đặt quân
thứ 1  chỉ còn N2 – 1 ô có thể đặt quân thứ 2  có N2 – 1 cách
– …
==> tổng cộng có tất cả (N2)! cách đặt. Với mỗi cách ta kiểm tra xem có
thỏa mãn điều kiện không có quân nào cùng dòng/cột/đường chéo; nếu
thỏa mãn thì thu được lời giải tương ứng.
• Có một cách khác: giảm không gian xét duyệt từ (N2)! xuống còn N!

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Bài toán xếp hậu: Duyệt toàn bộ
• Có một cách khác: giảm không gian xét duyệt từ (N2)! xuống còn N!
– Mỗi cách xếp N quân hậu lên bàn cờ sao cho không có quân nào cùng dòng, cùng cột ~
một hoán vị N phần tử

Mảng a gồm 4 phần tử


a[1] =3; a[2] = 1; a[2] = 4; a[4] = 2
Dòng 1: xếp quân hậu ở cột 3
Dòng 2: xếp quân hậu ở cột 1
Dòng 3: xếp quân hậu ở cột 4
Dòng 4: xếp quân hậu ở cột 2
Hoán vị: (3, 1, 4, 2)

– Chú ý: không phải hoán vị nào cũng là lời giải của bài toán xếp hậu:

Thuật toán: Liệt kê tất cả N! hoán vị;


kiểm tra điều kiện không cùng đường
chéo tại mỗi hoán vị. Nếu hoán vị nào
thỏa mãn điều kiện  cho ta 1 lời giải

? Làm thế nào để kiểm tra được điều kiện


Hoán vị: (3, 1, 4, 2) Hoán vị: (3, 2, 1, 4) Không cùng đường chéo
=> lời giải => Không phải là lời giải
Vì còn phải kiểm tra điều kiện: không có quân hậu nào cùng đường chéo
Bài toán xếp hậu: Duyệt toàn bộ
• Có một cách khác: giảm không gian xét duyệt từ (N2)! xuống còn N!
– Kiểm tra hai quân hậu đặt ở ô (i1, j1) và ô (i2, j2) không thuộc cùng một đường chéo

7
|i1-i2| ≠ |j1-j2|
6

1 2 3 4 5 6 7 8
Bài toán xếp hậu: Thuật toán quay lui
Thuật toán: Xếp từng quân hậu lên lần lượt từng dòng của bàn cờ: tại bước lặp thứ
k (k=1,..,n): ta cần tìm tọa độ cột ak để đặt quân cờ lên dòng k [tức là đặt lên ô
(dòng k, cột ak)]

• Giả sử đã xây dựng được lời giải bộ phận (a1, a2, …, ak-1): tức là đã xếp được
(k-1) quân hậu lần lượt vào các ô (1, a1), (2, a2), …(k-1, ak-1) thỏa mãn điều kiện
đề bài.
• Giờ tiếp tục xây dựng thành phần thứ k của lời giải: tức là cần tìm tọa độ cột để
xếp quân hậu lên dòng thứ k của bàn cờ. Ta tiến hành như sau:
– Duyệt lần lượt từng cột j =1,2,..,n:
• Kiểm tra xem có thể xếp quân hậu lên ô (k, j) - dòng k cột j hay không:
bằng cách dùng hàm nhận biết ứng cử viên
UCVh(int j, int k) trả về giá trị 1 nếu xếp được; ngược lại hàm trả
về giá trị 0
 UCVh nhận giá trị 1 khi và chỉ khi jSk
Thuật toán quay lui: Hàm nhận biết ứng cử viên
int UCVh(int j, int k) //check xếp quân hậu vào ô (k, j)
{ // UCVh nhận giá trị 1 khi và chỉ khi j Sk
for (i=1; i<k; i++) //duyệt qua lần lượt từ dòng 1..(k-1) là những dòng đã xếp quân hậu
if ((j == a[i]) || (fabs(j-a[i])== k-i)) return 0;

return 1;
}
void Try(int k) //tìm a[k]: cột xếp quân hậu thứ k lên dòng k
{
for (int j=1; j<=n; j++) //duyệt qua lần lượt từ cột 1..n: xem có xếp được lên ô (k, j) không
if (UCVh(j,k)) //nếu xếp được quân hậu lên ô (k, j)
{
a[k]=j;
if (k==n) Ghinhan(); //nếu đã xếp được đủ cả n quân hậu thì in ra lời giải
else Try(k+1); //nếu ko thì tiếp tục tìm vị trí xếp quân hậu thứ k+1 lên dòng thứ k+1
}
}
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
125
Chú ý
• Rõ ràng là bài toán xếp hậu không phải là luôn có lời giải,
chẳng hạn bài toán không có lời giải khi n = 2, 3. Do đó
điều này cần được thông báo khi kết thúc thuật toán.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
Mét lêi gi¶i cña bµi to¸n xÕp hËu khi n = 8
N i dung
1. Duyệt toàn bộ
2. Thuật toán đệ qui
3. Thuật toán quay lui
4. Chia để trị
5. Quy hoạch động

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
128
4. Chia để trị
4.1. Sơ đồ chung của thuật toán
4.2. Một số ví dụ minh họa

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
129
4.1. Sơ đồ thuật toán chia để trị
• Chia để trị (Divide and Conquer): thủ tục gồm 3 thao tác:
– Divide: Phân rã bài toán đã cho thành bài toán cùng dạng với
kích thước nhỏ hơn (gọi là bài toán con) S1, S2, …
– Conquer: Giải các bài toán con một cách đệ quy
– Combine: Tổng hợp lời giải của các bài toán con S1, S2, … để
thu được lời giải của bài toán ban đầu S

• Nếu bài toán con là đủ nhỏ có thể dễ dàng giải được, thì ta tiến
hành giải trực tiếp, nếu không: bài toán con lại được giải bằng
cách áp dụng đệ quy thủ tục trên (tức là lại tiếp tục chia nó thành
các bài toán nhỏ hơn). Do đó, các thuật toán chia để trị là các thuật
toán đệ quy => để phân tích độ phức tạp có thể sử dụng công thức
đệ quy
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
130
4.1. Sơ đồ thuật toán chia để trị
Để có được mô tả chi tiết của thuật toán chia để trị chúng ta cần phải xác định:
• Kích thước tới hạn n0 (bài toán với kích thước nhỏ hơn n0 sẽ không cần chia nhỏ)

• Kích thước của mỗi bài toán con trong cách chia

• Số lượng các bài toán con như vậy

• Thuật toán tổng hợp lời giải của các bài toán con.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
131
Ví dụ.
procedure D-and-C(int n)
begin
if (n == 0) return;
D-and-C(n/2);
D-and-C(n/2);
for (int i =0 ; i < n; i++)
begin
//Thực hiện các thao tác thời gian cỡ hằng số
end;
end;

Độ phức tạp tính toán của thuật toán chia để trị này là gì ?
Trả lời: T(n) = 2T(n/2) + n
Giải ta được T(n) = O(nlog2n) (hoặc có thể sử dụng định lý thợ)
132
4. Chia để trị
4.1. Sơ đồ chung của thuật toán
4.2. Một số ví dụ minh họa
– Ví dụ 1. Tìm kiếm nhị phân
– Ví dụ 2. Nhân số nguyên
– Ví dụ 3. Lũy thừa

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
133
Ví dụ 1. Tìm kiếm nhị phân (Binary search)
Bài toán: Cho mảng số A[1..n] được sắp xếp theo thứ tự tăng dần và số K.
Cần tìm chỉ số i (1  i  n) sao cho A[i] = K.
• Để đơn giản ta giả thiết rằng chỉ số như vậy là tồn tại. Thuật toán chia để trị
để giải bài toán được xây dựng dựa trên lập luận sau: Số K cho trước
– hoặc là bằng phần tử nằm ở vị trí ở giữa mảng A
– hoặc là nằm ở nửa bên trái (L) của mảng A
– hoặc là nằm ở nửa bên phải (R) của mảng A.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
134
4. Chia để trị
4.1. Sơ đồ chung của thuật toán
4.2. Một số ví dụ minh họa
– Ví dụ 1. Tìm kiếm nhị phân
– Ví dụ 2. Nhân số nguyên
– Ví dụ 3. Lũy thừa

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
135
Ví dụ 2. Nhân số nguyên

9 8 1
• Nếu các nhân tử có n chữ số, thì
1 2 3 4 thời gian cần thiết là (n2)
3 9 2 4 • Có cách làm nào tốt hơn hay
2 9 4 3 không?

1 9 6 2
9 8 1
1 2 1 0 5 5 4

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
136
Ví dụ 2. Nhân số nguyên
• Bài toán: Cho
x = xn-1 xn-2 ... x1 x0 và
y = yn-1 yn-2 ... y1 y0
là 2 số nguyên không âm có n chữ số thập phân. Cần tính
z = z2n-1 z2n-2 ... z1 z0
là biểu diễn với 2n chữ số thập phân của tích xy.

13
7
Ví dụ 2. Nhân số nguyên: Thuật toán Karatsuba (1962)
• Ta có: x = xn-1 xn-2 ... x1 x0 và y = yn-1 yn-2 ... y1 y0

• Vì thế: z = z2n-1 z2n-2 ... z1 z0 = x * y

13
8
Ví dụ 2. Nhân số nguyên: Thuật toán Karatsuba (1962)
• Đặt: Khi đó:

• Vì thế:

Để tính ac, ad, bc, bd ta phải thực hiện 4 phép nhân
các số có n/2 chữ số.
 Bài toán đã cho yêu cầu thực hiện phép nhân của 2 số x và
y có n chữ số : được quy về bài toán tính 4 phép nhân của số
có n/2 chữ số 13
9
Ví dụ 2. Nhân số nguyên: Thuật toán Karatsuba (1962)
• Neo đệ qui: Việc nhân hai số nguyên có 1 chữ số có thể thực hiện một
cách trực tiếp;
• Chia: Nếu n>1 thì tích của 2 số nguyên có n chữ số có thể biểu diễn
qua 4 tích của 4 số nguyên có n/2 chữ số: a*c, a*d, b*c, b*d
• Tổng hợp: Để tính kết quả z = xy khi đã biết 4 tích nói trên chỉ cần
thực hiện các phép cộng (có thể thực hiện với thời gian O(n)) và phép
nhân với luỹ thừa của 10 (có thể thực hiện với thời gian O(n), bằng
việc điền một số lượng số 0 thích hợp vào bên phải).

Có thể tăng tốc??


14
0
Ví dụ 2. Nhân số nguyên: Thuật toán Karatsuba (1962)
Karatsuba đã phát hiện cách thực hiện việc nhân 2 số nguyên có n
chữ số đòi hỏi 3 phép nhân các số có n/2 chữ số sau đây:

• Đặt: U = ac, V = bd, W =(a+b)(c+d)


Khi đó: ad + bc = W – U – V,
và do đó có thể tính:
z = x*y = (a10n/2 +b)  (c10n/2 +d)
= (ac) 10n + (ad + bc) 10n/2 + bd
= U10n + (W-U-V)10n/2 + V.

14
1
Thuật toán Karatsuba
function Karatsuba(x, y, n)
begin
if n=1 then return x[0]*y[0]
else
begin
a:= x[n-1] ... x[n/2];
b:= x[n/2 -1] ... x[0];
c:= y[n-1] ... y[n/2];
d:= y[n/2-1] ... y[0];
U:= Karatsuba(a, c, n/2);
V:= Karatsuba(b, d, n/2);
W:= Karatsuba(a+b, c+d, n/2);
return U*10n + (W-U-V)*10n/2 + V;
end;
end;

14
2
4. Chia để trị
4.1. Sơ đồ chung của thuật toán
4.2. Một số ví dụ minh họa
– Ví dụ 1. Tìm kiếm nhị phân
– Ví dụ 2. Nhân số nguyên
– Ví dụ 3. Lũy thừa

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
143
Ví dụ 3. Lũy thừa
Bài toán: Tính xn , với x, n là những số nguyên
Giả sử chúng ta không có phương thức dựng sẵn pow
Cách cài đặt đơn giản: xn = x*x*x*….*x
n lần
int pow(int x, int n)
{
int res = 1;
for (int i = 0; i < n; i++) res = res * x;
return res;
}
Độ phức tạp: O(n)
 Cách tăng tốc ??
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
144
Ví dụ 3. Lũy thừa: chia để trị
Nhận xét (1):
• x0 = 1
• xn = x × xn−1
 Cần viết hàm pow:
• pow(x,0) = 1
• pow(x, n) = x × pow(x, n − 1)

 Thời gian tính: T(n) = 1 + T(n-1)  T(n) = O(n)


145
Ví dụ 3. Lũy thừa: chia để trị
Nhận xét (2): xn = xn/2 × xn/2
 pow(x, n) = pow(x, n/2) × pow(x, n/2)
pow(x, n/2) được sử dụng 2 lần, nhưng chúng ta chỉ cần tính nó một lần:
pow(x, n) = pow(x, n/2)2
 Ta có thể sử dụng để tăng tốc trong trường hợp n là số chẵn vì khi đó n/2 sẽ là số nguyên

int pow(int x, int n)


{
if (n == 0) return 1;
if (n%2 !=0)
return x * pow(x, n - 1);
int tmp = pow(x, n/2);
return tmp*tmp;
}
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
146
Ví dụ 3. Lũy thừa

Thuật toán trên có thể áp dụng với:


• Tính xn khi x là một số thực, khi đó * là phép nhân số thực
• Tính An khi A là một ma trận, khi đó * là phép nhân ma trận
• Tính xn (mod m) khi x là một ma trận, và * là phép nhân số nguyên đồng dư
m
• Tính x*x*…*x khi x là phần tử bất kỳ và * là toán tử kết hợp bất kỳ.
Thời gian tính: O(f * logn) với f là chi phí để thực hiện toán tử *

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
147
N i dung
1. Duyệt toàn bộ
2. Thuật toán đệ qui
3. Thuật toán quay lui
4. Chia để trị
5. Quy hoạch động

148
5. Quy hoạch động
5.1. Sơ đồ chung của thuật toán
5.2. Một số ví dụ minh họa

149
5. Quy hoạch động
5.1. Sơ đồ chung của thuật toán
5.2. Một số ví dụ minh họa

150
5.1. Sơ đồ chung của thuật toán quy hoạch động
Việc phát triển thuật toán dựa trên quy hoạch động (Dynamic Programming) bao gồm 3 giai đoạn:
• Phân rã:
– Chia bài toán cần giải thành những bài toán con nhỏ hơn có cùng dạng với bài toán ban
đầu sao cho bài toán con kích thước nhỏ nhất có thể giải một cách trực tiếp.
– Bản thân bài toán xuất phát có thể coi là bài toán con có kích thước lớn nhất trong họ các
bài toán con này.
• Ghi nhận lời giải:
– Lưu trữ lời giải của các bài toán con vào một bảng. Việc làm này là cần thiết vì lời giải
của những bài toán con thường được sử dụng lại rất nhiều lần, và điều đó nâng cao hiệu
quả của giải thuật do không phải giải lặp lại cùng một bài toán nhiều lần.

• Tổng hợp lời giải:


– Lần lượt từ lời giải của các bài toán con kích thước nhỏ hơn tìm cách xây dựng lời giải
của bài toán kích thước lớn hơn, cho đến khi thu được lời giải của bài toán xuất phát (là
bài toán con có kích thước lớn nhất).
– Kỹ thuật giải các bài toán con của quy hoach động là quá trình đi từ dưới lên (bottom-
up); còn với phương pháp chia để trị: các bài toán con được trị một cách đệ quy (top-
down).
151
5.1. Sơ đồ chung của thuật toán quy hoạch động
Để việc áp dụng quy hoạch động dẫn đến thuật toán hiệu quả, bài toán cần có 2 tính chất:
• Cấu trúc con tối ưu: (còn được gọi là tiêu chuẩn tối ưu) Để giải được bài toán đặt ra một
cách tối ưu, mỗi bài toán con cũng phải được giải một cách tối ưu. Mặc dù sự kiện này có vẻ
là hiển nhiên, nhưng nó thường không được thoả mãn do các bài toán con là giao nhau.
• Số lượng các bài toán con phải không quá lớn. Chính xác hơn là tổng số các bài toán con
cần giải cùng lắm phải bị chặn bởi một đa thức của kích thước dữ liệu vào.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
152
5.1. Sơ đồ của thuật toán quy hoạch động
5.1. Sơ đồ chung của thuật toán
5.2. Một số ví dụ minh họa
– Ví dụ 1. Dãy con lớn nhất
– Ví dụ 2. Dãy con chung dài nhất
– Ví dụ 3. Bài toán người du lịch

153
Ví dụ 1. Dãy con lớn nhất (The maximum subarray problem)

• Cho dãy số gồm n số:

a1, a2, … , an
Dãy gồm liên tiếp các số ai, ai+1 , …, aj với 1 ≤ i ≤ j ≤ n được gọi là dãy
con của dãy đã cho và được gọi là trọng lượng của dãy con này
Bài toán đặt ra là: Hãy tìm trọng lượng lớn nhất của các dãy con, tức là
tìm cực đại giá trị . Ta gọi dãy con có trọng lượng lớn nhất là dãy
con lớn nhất.
Ví dụ: Cho dãy số -2, 11, -4, 13, -5, 2 thì cần đưa ra câu trả lời là 20 (dãy con
lớn nhất là 11, -4, 13 với giá trị = 11+ (-4)+13 =20 )

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
154
Dãy con lớn nhất (The maximum subarray problem)
1. Duyệt toàn bộ (Brute force)

2. Duyệt toàn bộ có cải tiến

3. Thuật toán đệ quy (Recursive algorithm)


n log n
4. Thuật toán Quy hoạch động (Dynamic programming)

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
155
Thuật toán quy hoạch động giải bài toán dãy con lớn nhất
Thuật toán quy hoạch động được chia làm 3 giai đoạn:
1. Phân rã:
• Gọi si là trọng lượng của dãy con lớn nhất của dãy a1, a2, ..., ai , i = 1, 2, ..., n.
• Rõ ràng, sn là giá trị cần tìm (lời giải của bài toán).

3. Tổng hợp lời giải:


• s1 = a1
• Giả sử i > 1 và ta đã biết giá trị sk với k = 1, 2, ..., i-1. Ta cần tính giá trị si là trọng lượng của dãy con lớn nhất
của dãy:
a1, a2, ..., ai-1, ai .
• Nhận thấy rằng: dãy con lớn nhất của dãy a1, a2, ..., ai-1, ai có thể hoặc bao gồm phần tử ai hoặc không bao gồm
phần tử ai  do đó, dãy con lớn nhất của dãy a1, a2, ..., ai-1, ai chỉ có thể là một trọng 2 dãy sau:
– Dãy con lớn nhất của dãy a1, a2, ..., ai-1
– Dãy con lớn nhất của dãy a1, a2, ..., ai , và dãy con này kết thúc tại phần tử ai.
 Do đó, ta có si = max {si-1, ei}, i = 2, …, n.
với ei là trọng lượng của dãy con lớn nhất a1, a2, ..., ai và dãy con này kết thúc tại ai.
Để tính ei, ta xây dựng công thức đệ quy:
– e1 = a1;
– ei = max {ai, ei-1 + ai}, i = 2, ..., n. 156
Thuật toán quy hoạch động giải bài toán dãy con lớn nhất
Thuật toán quy hoạch động được chia làm 3 giai đoạn:
1. Phân rã:
• Gọi si là trọng lượng của dãy con lớn nhất của dãy a1, a2, ..., ai , i = 1, 2, ..., n.
• Rõ ràng, sn là giá trị cần tìm (lời giải của bài toán).

3. Tổng hợp lời giải:


• s1 = a1
• Giả sử i > 1 và ta đã biết giá trị sk với k = 1, 2, ..., i-1. Ta cần tính giá trị si là trọng lượng của dãy con lớn nhất
của dãy:
a1, a2, ..., ai-1, ai .
• Nhận thấy rằng: dãy con lớn nhất của dãy a1, a2, ..., ai-1, ai có thể hoặc bao gồm phần tử ai hoặc không bao gồm
phần tử ai  do đó, dãy con lớn nhất của dãy a1, a2, ..., ai-1, ai chỉ có thể là một trọng 2 dãy sau:
– Dãy con lớn nhất của dãy a1, a2, ..., ai-1
– Dãy con lớn nhất của dãy a1, a2, ..., ai , và dãy con này kết thúc tại phần tử ai.
 Do đó, ta có si = max {si-1, ei}, i = 2, …, n.
với ei là trọng lượng của dãy con lớn nhất a1, a2, ..., ai và dãy con này kết thúc tại ai.
Để tính ei, ta xây dựng công thức đệ quy:
– e1 = a1;
– ei = max {ai, ei-1 + ai}, i = 2, ..., n. 157
Thuật toán quy hoạch động giải bài toán dãy con lớn nhất
MaxSub(a)
{
smax = a[1]; // smax : trọng lượng của dãy con lớn nhất
ei = a[1]; // ei: trọng lượng của dãy con lớn nhất kết thúc tại phần tử a[i]
imax = 1; // imax : chỉ số của phần tử cuối cùng thuộc dãy con lớn nhất
for i = 2 to n {
u = ei + a[i];
v = a[i];
if (u > v) ei = u
else ei = v;
if (ei > smax) {
smax := ei;
imax := i;
}
}
}

Phân tích thuật toán:


Số phép cộng phải thực hiện trong thuật toán
= Số lần thực hiện câu lệnh u = ei + a[i]
=n
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
158
5.1. Sơ đồ của thuật toán quy hoạch động
5.1. Sơ đồ chung của thuật toán
5.2. Một số ví dụ minh họa
– Ví dụ 1. Dãy con lớn nhất
– Ví dụ 2. Dãy con chung dài nhất
– Ví dụ 3. Bài toán người du lịch

159
Dãy con chung dài nhất (Longest common subsequence - LCS)
• Cho dãy gồm n phần tử X = <x1, x2, ..., xn>
Dãy con của X là dãy thu được từ dãy X bằng việc loại bỏ một số phần tử:
– Dãy Z = <z1, z2, ..., zk> được gọi là dãy con của dãy X nếu tìm được dãy các chỉ
số 1  i(1) < i(2) < ... < i(k)  n sao cho zj = xi(j), j = 1, 2, ..., k.
Ví dụ: Dãy
Z = <B, C, D, B>
là dãy con của dãy
X = <A, A, B, C, B, C, D, A, B, D, A, B>
với dãy chỉ số là <3, 4, 7, 9>.
• Cho hai dãy X và Y, ta nói dãy Z là dãy con chung của hai dãy X và Y nếu Z là dãy
con của cả hai dãy này.
Bài toán LCS: Cho hai dãy
X = <x1, x2, …, xm>
Y = <y1, y2,…,yn>
Hãy tìm dãy con chung dài nhất của X và Y.
NGUYỄN KHÁNH PHƯƠNG
Ứng dụng: Trong sinh học, khi khảo sát gien Bộ môn KHMT – ĐHBK HN
160
Dãy con chung dài nhất (Longest common subsequence - LCS)

Ví dụ:
X = <A, B, C, D, E, F, G>
Y = <C, C, E, D, E, G, F>
• Dãy Z = <C, D, F> là dãy con chung.
• Dãy <B, F, G> không là dãy con chung.
• Dãy <C, D, E, G> là dãy con chung dài nhất vì không tìm được dãy con
chung có độ dài 5.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
161
Dãy con chung dài nhất (Longest common subsequence - LCS)

Bài toán LCS: Cho hai dãy


X = <x1, x2, …, xm> ,
Y = <y1, y2,…,yn> .
Hãy tìm dãy con chung dài nhất của X và Y.

• Thuật toán trực tiếp (duyệt toàn bộ):


– Duyệt tất cả các dãy con có thể của X  số dãy con = (2m dãy con)
– Với mỗi dãy con cuả X ta kiểm tra xem nó có là dãy con cuả Y. Thời gian: O(n)
• Thời gian của thuật toán trực tiếp: O(n.2m)

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
162
LCS: Phân rã
Bài toán LCS: Cho hai dãy
X = <x1, x2, …, xm>
Y = <y1, y2,…,yn>
Hãy tìm dãy con chung dài nhất của X và Y.

• Tính c[i,j] là độ dài của dãy con chung dài nhất của hai dãy
Xi = <x1, x2, ..., xi>

Yj = <y1, y2, ..., yj>.
với mọi 0  i  m và 0  j  n
 Bài toán được phân ra thành (m+1)(n+1) bài toán con.
 Giá trị mục tiêu tối ưu là c[m,n]
NGUYỄN KHÁNH PHƯƠNG
Bộ môn KHMT – ĐHBK HN
163
LCS: Tổng hợp lời giải
• Rõ ràng
c[0, j] = 0, j = 0, 1, ..., n
c[i, 0] = 0, i = 0, 1, ..., m.
• Giả sử i > 0, j > 0, ta cần tính c[i, j] là độ dài của LCS của hai dãy Xi = <x1, x2,
..., xi> và Yj <y1, y2, ..., yj>. Có hai tình huống:
– Nếu xi = yj :
• LCS của Xi và Yj sẽ thu được bằng việc bổ sung xi vào LCS của hai dãy
Xi-1 và Yj-1  c[i, j] = c[i-1, j-1] + 1
– Nếu xi  yj :
• LCS của Xi và Yj sẽ là dãy con dài nhất trong hai LCS của (Xi và Yj-1) và
của (Xi-1 và Yj)  c[i, j] = max {c[i, j-1], c[i-1, j]}
• Từ đó ta có công thức sau để tính c[i, j]:
0, nÕu i  0 hoÆc j  0,

c[i, j ]  c[i  1, j  1]  1, nÕu i, j  0 vµ xi  y j

max{c[i, j  1], c[i  1, j ]}, nÕu i, j  0 vµ xi  y j . 164
LCS: Cài đặt
0, nÕu i  0 hoÆc j  0,

c[i, j ]  c[i  1, j  1]  1, nÕu i, j  0 vµ xi  y j

procedure LCS(X,Y) max{c[i, j  1], c[i  1, j ]}, nÕu i, j  0 vµ xi  y j .
begin
for i=0 to m do c[i,0]=0;
for j=0 to n do c[0,j]=0;
for i=1 to m do
for j=1 to n do
if (xi == yj) then
c[i,j] = c[i-1,j-1]+1;
else if (c[i-1,j] >= c[i,j-1])
c[i,j] = c[i-1,j];
else c[i,j] = c[i,j-1];
end;
Thời gian tính: O(mn)

Câu hỏi: làm thế nào để đưa ra được dãy con chung lớn nhất gồm những phần tử nào ?
165
LCS: Cài đặt
Câu hỏi: làm thế nào để đưa ra được dãy con chung lớn nhất gồm những phần tử nào ?
Trả lời: sử dụng biến phụ để truy dấu vết: Biến b[i,j] ghi nhận tình huống tối ưu khi tính
giá trị c[i,j]

procedure LCS(X,Y)
begin
for i=0 to m do c[i,0]=0;
for j=0 to n do c[0,j]=0;
for i=1 to m do
for j=1 to n do
if (xi == yj) then
{c[i,j] = c[i-1,j-1]+1; b[i,j]:= ‘ ’;}
else if (c[i-1,j] >= c[i,j-1])
{c[i,j] = c[i-1,j]; b[i,j]:= ‘’;}
else {c[i,j] = c[i,j-1]; b[i,j]:= ‘’;}
end;

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
166
LCS: Đưa ra dãy con chung dài nhất
• Biến b[i,j] ghi nhận tình huống tối ưu khi tính giá trị c[i,j]. Sử dụng
biến này ta có thể đưa ra LCS của hai dãy X và Y nhờ thủ tục:

procedure PrintLCS(b, X, i, j)
begin
if (i=0) or (j=0) then return;
if (b[i,j]== ’ ’) then
{
PrintLCS(b, X, i-1, j-1);
print xi; //đưa ra phần tử xi
}
else if (b[i,j]== ’’) then
PrintLCS(b, X, i-1, j);
else //b[i,j]=’←’
PrintLCS(b, X, i, j-1);
end;

167
5.1. Sơ đồ của thuật toán quy hoạch động
5.1. Sơ đồ chung của thuật toán
5.2. Một số ví dụ minh họa
– Ví dụ 1. Dãy con lớn nhất
– Ví dụ 2. Dãy con chung dài nhất
– Ví dụ 3. Bài toán người du lịch

168
Bài toán người du lịch (Traveling Salesman Problem – TSP)

• Có n thành phố 1, 2, ..., n. Biết ma trận chi phí đi lại giữa


các thành phố là
D = (dij: i, j = 1,2,..., n).
• Người du lịch xuất phát từ một thành phố bất kỳ muốn đi
qua tất cả các thành phố, mỗi thành phố đúng một lần rồi lại
quay về thành phố xuất phát. Cách đi như vậy được gọi là
một hành trình. Chi phí của hành trình được tính như là
tổng các chi phí của các đoạn đường của nó.
• Cần tìm hành trình với chi phí nhỏ nhất.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
169
TSP: Phân rã
• Cố định thành phố xuất phát là 1.
• Kí hiệu k là tập tất cả các tập con k phần tử của {2, 3, ..., n}.
• Với mỗi k = 1, 2, ..., n-1, với mỗi tập con S  k và với mỗi j  S gọi:
Ck(S, j) là chi phí của đường đi ngắn nhất bắt đầu từ đỉnh 1 đi qua mỗi đỉnh
trong S đúng một lần và kết thúc ở đỉnh j.
• Ví dụ: S = {2, 3, 4} và j = 3, ta sẽ phải tìm đường đi ngắn nhất trong số 2
đường đi
1243

1  4  2  3.

NGUYỄN KHÁNH PHƯƠNG


Bộ môn KHMT – ĐHBK HN
170
TSP: Tổng hợp lời giải

• Ta có
C1({i}, i) = d(1, i), i = 2, ..., n.
• Giả sử đã tính Ck-1(S, j) cho mỗi tập S  k-1 và mỗi j = 2,..., n,
• Ta sẽ phải tính Ck(S, j) cho mỗi S  k và j = 2, ..., n.
Ta có thể tính Ck(S, j) nhờ lập luận sau đây: Đường đi với chi phí nhỏ nhất cần tìm sẽ
phải qua một thành phố i  S \ {j} và từ i đến j (nghĩa là i là thành phố đi ngay trước j
trong đường đi đó).
Do đó: Ck ( S , j )  min{Ck 1 ( S  { j}, i )  d (i, j )},
iS \{ j}

j  2,..., n; S   k , k  2,..., n  1

• Để tìm hành trình tối ưu cần ghi nhận Pred(S, j) là chỉ số i đạt min trong biểu thức trên.
• Chi phí tối ưu sẽ là min{C({2, 3,..., n}, i )  d (i,1)}
i

Để tính mỗi Ck(S, j) ta mất thời gian O(n). Có tất cả O(n 2n) giá trị Ck(S, j) cần tính. Do
đó thuật toán mô tả có thời gian tính là O(n2 2n). 171
/*********************** int tsp(int i, int S, int n) {
DP Algorithm for TSP if (S == ((1 << n) - 1)) {
return d[i][0];
***********************/
}
#include <stdio.h>
if (C[i][S] != -1) {
#include <iostream> return C[i][S];
#include <string.h> }
int res = INF;
using namespace std; for (int j = 0; j < n; j++) {
if (S & (1 << j))
const int N = 20; continue;
res = min(res, d[i][j] + tsp(j, S | (1 << j), n));
const int INF = 100000000;
}
int d[N][N];
C[i][S] = res;
int C[N][1<<N]; return res;
}
int main() {
int n, i, j;
memset(mem, -1, sizeof(mem));
freopen("tsp.inp", "r", stdin);
freopen("tsp.out", "w", stdout);
// printf(" Input Data \n");
cin >> n;
for (int i=0; i<n; i++)
for (int j=0; j<n; j++)
scanf("%d", &d[i][j]);
printf("**** Cost Matrix n = %d \n", n);
for (int i=0; i<n; i++) {
for (int j=0; j<n; j++)
printf("%5d ", d[i][j]);
printf("\n");
}
printf("\n\n**** Optimal value: %d ****\n", tsp(0, 1<<0, n));
}

You might also like