Professional Documents
Culture Documents
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ả.
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;
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?
• 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
}
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
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
K = 95
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
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
(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
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:
– 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 (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)
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.
(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)
()
Sk = {0,1}
0 1
(0)
(1)
0 1 0 1
(00) (01) (10) (11)
0 1 0 1 0 1 0 1
int main() {
cout<<"Nhap n = ";cin>>n;
count = 0; Xau();
cout<<"So luong xau = "<<count<<endl;
MSet(1); ()
1 3
2
(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)
n cột
The n-Queens Problem
– 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:
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 jSk
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.
• 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
• Thuật toán tổng hợp lời giải của các bài toán con.
Độ 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
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
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
13
8
Ví dụ 2. Nhân số nguyên: Thuật toán Karatsuba (1962)
• Đặt: Khi đó:
• Vì thế:
Để tính ac, ad, bc, bd 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).
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
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.
153
Ví dụ 1. Dãy con lớn nhất (The maximum subarray problem)
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 )
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.
• 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>
và
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;
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)
• 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 )},
iS \{ 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));
}