Thuật toán chia để trị hoạt động bằng cách chia bài toán thành nhiều bài toán nhỏ hơn thuộc cùng thể loại, cứ như vậy lặp lại nhiều lần, cho đến khi bài toán thu được đủ đơn giản để có t
Trang 1MỤC LỤC
Chương 1: 2
THUẬT TOÁN CHIA ĐỂ TRỊ 2
(Divide to Conquer) 2
1.1/ Đặt vấn đề: 2
1.1.1) Khái niệm: 2
1.1.2) Sơ đồ chung: 2
1.2/ Thuật toán: 3
1.2.1) Ý tưởng: 3
1.2.2) Các bước giải: 3
1.2.3 Sơ đồ thuật toán chia để trị: 3
Chương 2: 4
ỨNG DỤNG CỦA THUẬT TOÁN CHIA ĐỂ TRỊ GIẢI BÀI TOÁN SẮP XẾP 4
2.1/ Phát biểu bài toán: 4
2.2/ Phương pháp thiết kế: 4
2.3/ Ứng dụng: 4
2.3 1 Merge sort 4
a Ý tưởng: 4
b Giải thuật: 5
c Đánh giá thuật toán: 6
d Chương trình sắp xếp dãy bằng Mergesort: 7
e Ví dụ bài toán sắp xếp thực tế: 9
2.3.2 Quicksort 9
a Ý tưởng: 9
b. Giải thuật: 10
c Đánh giá thuật toán: 11
d Chương trình sắp xếp dãy bằng Quicksort 14
e Ví dụ bài toán sắp xếp thực tế: 15
Trang 2CHƯƠNG 1:
THUẬT TOÁN CHIA ĐỂ TRỊ
(Divide to Conquer) 1.1/ Đặt vấn đề:
Trong khoa học máy tính, chia để trị là một mô hình thiết kế thuật toán quan trọng
dựa trên đệ quy với nhiều phân nhánh Thuật toán chia để trị hoạt động bằng cách chia bài toán thành nhiều bài toán nhỏ hơn thuộc cùng thể loại, cứ như vậy lặp lại nhiều lần, cho đến khi bài toán thu được đủ đơn giản để có thể giải quyết trực tiếp Sau đó lời giải của các bài toán nhỏ được tổng hợp lại thành lời giải cho bài toán ban đầu
a
Hình 1: Mô hình phương pháp chia để trị
Tên gọi "chia để trị" đôi khi cũng được áp dụng cho các thuật toán quy bài toán ban đầu về đúng một bài toán nhỏ hơn, chẳng hạn như sắp xếp trong một danh sách theo thứ tự không tăng hoặc không giảm Những thuật toán này có thể được lập trình hiệu quả hơn thuật toán chia để trị thông thường: đặc biệt, nếu các thuật toán này dùng đệ quy đuôi thì
có thể chuyển chúng thành một vòng lặp thay vì đệ quy
1.1.1) Khái niệm:
Chia để trị là một trong những phương pháp thiết kế giải thuật cơ bản bao gồm các thao tác:
Chia: Chia bài toán cần giải thành một loạt các bài toán con độc lập
Trị: Đòi hỏi việc giải các bài toán con thu được
Tổng hợp: Thực hiện việc xây dựng lời giải của bài toán đặt ra từ các lời giải của bài toán con
1.1.2) Sơ đồ chung:
Sơ đồ chung của thuật toán chia để trị (Divide and Conquer) gồm 3 thành phần:
- Chia (Divide): Chia bài toán cần giải S ra thành các bài toán con S1, S2, S3,
- Trị (Conquer): Giải các bài toán con một cách đệ quy
- Tổng hợp (Combine): Tổng hợp lời giải của bài toán S1, S2, S3, thành lời giải của bài toán S
Để phân tích độ phức tạp của thuật toán có thể sử dụng công thứ đệ quy
Vấn đề đặt ra là cần giải các bài toán con độc lập bằng cách nào? Đó là vấn đề trung tâm của bài toán
Trang 31.2/ Thuật toán:
1.2.1) Ý tưởng:
Chia bài toán thành nhiều bài toán nhỏ hơn thuộc cùng thể loại, cứ như vậy lặp lại nhiều lần, cho đến khi bài toán thu được đủ đơn giản để có thể giải quyết trực tiếp Sau đó lời giải của các bài toán nhỏ được tổng hợp lại thành lời giải cho bài toán ban đầu
1.2.2) Các bước giải:
Giả sử chúng ta có thuật toán α để giải bài toán kích thước dữ liệu vào n với thời gian bị chặn bởi cn2 (c: hằng số) Xét thuật giải β để giải chính bài toán đó bằng cách:
- Bước 1: Chia bài toán cần giải thành 3 bài toán con với kích thước n/2
- Bước 2: Giải 3 bài toán bằng thuật toán α
- Bước 3: Tổng hợp lời giải của 3 bài toán con để thu được lời giải của bài toán
3 Đánh giá thuật toán: Tính đúng đắn của thuật toán β
Giả sử bước 3 đòi hỏi thời gian dn(d: hằng số)
Gọi:
T α(n) = thời gian của thuật toán α
T β(n) = thời gian của thuật toán β
Ta có:
T α(n) = cn2 (theo giả thuyết)
T β(n) = 3.T α(n/2) + dn = ¾.cn2 + dn
Nếu:
dn < c n2/4 hay n > 4 d/c thì thuật toán β nhanh hơn thuật toán α
Do 4.d/c là hằng số nên với n đủ lớn ta luôn có n > 4 d/c
Điều đó cho thấy việc sử dụng thuật toán β để giải bài toán đặt ra bằng cách chia nó thành các bài toán con có kích thước càng ngày càng nhỏ đến khi thu được bài toán con kích thước n0 < 4.d/c sẽ thu được hiệu quả cao
1.2.3 Sơ đồ thuật toán chia để trị:
procedure Divide_and_Conquer(n);
{
if n ≤ n0 then Giải bài toán một cách trực tiếp;
else
{
Chia bài toán thành r bài toán con có kích thước n/k;
for (mỗi bài toán trong r bài toán con) do
Divide_and_Conquer(n/k);
Tổng hợp lời giải của r bài toán con để thu được lời giải của bài toán;
end;
end;
Trang 4CHƯƠNG 2:
ỨNG DỤNG CỦA THUẬT TOÁN CHIA ĐỂ TRỊ
GIẢI BÀI TOÁN SẮP XẾP
2.1/ Phát biểu bài toán:
Cho T[1 n] là một mảng n phần tử Vấn đề đặt ra là sắp xếp các phần tử này theo thứ
tự dãy không tăng hoặc không giảm
Đầu vào: T[1 n] là một mảng n phần tử
Đầu ra: sắp xếp các phần tử này theo thứ tự không giảm
2.2/ Phương pháp thiết kế:
Áp dụng thuật toán “chia để trị” để giải quyết bài toán sắp xếp trên.
2.3/ Ứng dụng:
Chúng ta đã có thể giải quyết vấn đề này bằng các phương pháp selection sort hay insertion sort hoặc là heapsort
Như chúng ta đã biết thời gian dùng selection sort hay insertion sort để sắp xếp mảng
T trong cả hai trường hợp: xấu nhất và trung bình đều vào cỡ n2 Còn heapsort vào khoảng nlogn Có một số giải thuật đặc biệt cho bài toán này theo mô hình chia để trị đó là mergesort và quicksort, chúng ta sẽ lần lượt đi nghiên cứu hai giải thuật này
2.3 1 Merge sort
a Ý tưởng:
Chia để trị tiếp cận tới bài toán này bằng việc tách mảng T thành hai phần mà kích thước của chúng sai khác nhau càng ít càng tốt, sắp xếp các phần này bằng cách gọi đệ qui
và sau đó trộn chúng lại (chú ý duy trì tính thứ tự) Để làm được điều này chúng ta cần một giải thuật hiệu quả cho việc trộn hai mảng đã được sắp U và V thành một mảng mới T
mà kích thước của mảng T bằng tổng kích thước của hai mảng U và V
Trang 5Hình vẽ 2: Quy trình sắp xếp bằng phương pháp chia để trị
Vấn đề này có thể thực hiện tốt hơn nếu ta thêm vào các ô nhớ có sẵn ở cuối của mảng U và V các giá trị đứng canh (giá trị lớn hơn tất cả các giá trị trong U và V)
b Giải thuật:
Procedure merge(U[1 m+1],V[1 n+1],Ta[1 m+n]);
(*Trộn 2 mảng U[1 m+1] và V[1 n+1] thành mảng T[1 m+n]); U[m+1],V[n+1] được dùng để chứa các giá trị cầm canh*)
{ i:=1;j:=1;
U[m+1]:= ∞ ; V[n+1]:= ∞ ;
For (k=1;k<(n+m);k++)
If (U[i]<V[j]) {
T[k]=U[i];
I++;
}
Else
{
Array
Split
Sub Array Sub Array
Split
SubArr
Stored SubArra y
Stored
SubArra
y
Combine
Store Array
Combine
Stored SubArray
Split
SubArr
Stored SubArra y
Stored SubArra y
Combine
Stored SubArray
Trang 6j:=j+1;
}
}
Giải thuật sắp xếp trộn sau đây có thể tốt hơn nếu các mảng U và V là các biến toàn cục và xem việc sắp xếp chèn Insert(T) như là giải thuật cơ bản
Procedure mergesort(T[1 n]);
{
If n đủ nhỏ then Insert(T)//giải thuật sx đơn giản
Else
{
int U[1 1+],V[1 1+;
U[1,l]:=T[1,mid];
V[1,r]:=T[mid +1,n];
}
Mergesrt(U);
mergesort(V);
merge(U,V,T);
c Đánh giá thuật toán:
Giải thuật sắp xếp này minh hoạ tất cả các khía cạnh của chia để trị Khi số lượng các phần tử cần sắp là nhỏ thì ta thường sử dụng các giải thuật sắp xếp đơn giản
Khi số phần tử đủ lớn thì ta chia mảng ra 2 phần, tiếp đến trị từng phần một và cuối cùng là kết hợp các lời giải
Giả sử t(n) là thời gian cần thiết để giải thuật này sắp xếp một mảng n phần tử Việc tách T thành U và V là tuyến tính Ta cũng dễ thấy merge(U,V,T) cũng tuyến tính Do vậy:
t(n)=t([n/2]) + t([n/2]) + g(n) trong đó g( n ) = O( n )
hay t( n )=2t( n /2)+g( n )
Theo định lý chủ
Ta có: l=2; b=2và k=1
Nên t(n)= Ρ (nlogn), vì bk =l
Như vậy hiệu quả của mergesort tương tự heapsort Trong thực tế sắp xếp trộn có thể nhanh hơn heapsort một ít nhưng nó cần nhiều hơn bộ nhớ cho các mảng trung gian U và
V Ta nhớ lại heapsort có thể sắp xếp tại chỗ (in-place), và cảm giác nó chỉ sử dụng một ít biến phụ mà thôi Theo lý thuyết mergesort cũng có thể làm được như vậy tuy nhiên giá thành có tăng một chút ít
Trang 7Khi giải bài toán theo thuật giải chia để trị chúng ta hết sức chú ý đến việc tạo ra các bài toán con, nếu không nó có thể tạo ra những thảm hại mà không thể lường trước được Giải thuật sau đây minh hoạ tính chất quan trọng này khi kích thước bài toán con là hỗn độn:
Procedure badmergesort(T[1 n]);
{
If n đủ nhỏ then Insert(T) Else
{
Int U[1 n-1, V[1 2]; U[1 n-1]:= T[1 n-1]; V[1]:= T[n];
badmergesort(U);
badergesort(V);
merge(U,V,T);
}
}
Gọi t(n) là thời gian cần để sắp n phần tử với giải thuật badmergesort trên
Rõ ràng là: t(n)=t(n-1)+ t(1) + g(n)
trong đó (n) Ρ(n).∈ Ρ(n)
Sự đệ qui này tạo ra (n) Ρ(n2), như vậy việc quên cân bằng kích thước của bài toán con ∈ Ρ(n)
đã ảnh hưởng đáng kể đến hiệu quả của việc sử dụng giải thuật chia đểtrị
d Chương trình sắp xếp dãy bằng Mergesort:
#include <iostream>
#include <cmath>
#include <conio.h>
#include <stdio.h>
using namespace std;
// A simple print function
void print(int *input,int n)
{
for ( int i = 0; i < n; i++ )
cout << input[i] << " ";
cout << endl;
getch();
}
void merge(int* input, int p, int r)
{
int mid = floor((p + r) / 2);
int i1 = 0;
int i2 = p;
int i3 = mid + 1;
// Temp array
int temp[r-p+1];
// Merge in sorted form the 2 arrays
Trang 8while ( i2 <= mid && i3 <= r )
if ( input[i2] < input[i3] )
temp[i1++] = input[i2++];
else
temp[i1++] = input[i3++];
// Merge the remaining elements in left array
while ( i2 <= mid )
temp[i1++] = input[i2++];
// Merge the remaining elements in right array
while ( i3 <= r )
temp[i1++] = input[i3++];
// Move from temp array to master array
for ( int i = p; i <= r; i++ )
input[i] = temp[i-p];
}
void merge_sort(int* input, int p, int r)
{
if ( p < r )
{
int mid = floor((p + r) / 2);
merge_sort(input, p, mid);
merge_sort(input, mid + 1, r);
merge(input, p, r);
}
}
int main()
{
cout <<"\n";
cout << " WELLCOME TO MERGESORT PROGRAMING\n";
cout << " -o0o -\n";
cout <<"\n";
//cout <<"\n";
int input[1001] ;
int i,n;
cout << " Please input array's size you wana sort : n = ";
cin >> n;
cout<< "\nThe number of member in array is: " <<n;
cout << "\n";
cout << "\nPlease input array's value : \n";
cout <<"\n";
for (i=0;i<n;i++){
cout << " Input ["<<i +1<<"]: ";
cout<<"\n";
}
cout << " Input array: ";
print(input,n);
cout<<"\n";
Trang 9merge_sort(input, 0, n-1);
cout << " Output: ";
print(input,n);
cout <<"You was Sorted Array by merge sort Algorithm ";
getch();
}
e Ví dụ bài toán sắp xếp thực tế:
Cho danh sách
Giải thuật trộn đệ quy là chia a thành hai(hay nhiều) danh sách con và tiến hành sắp xếp trên các danh sách con đó
Danh sách trái Danh sách phải 2,7,6 3,4,5,1
Sắp xếp trộn danh sách trái 2,7,6
Quá trình chia Quá trình trộn
Sắp xếp trộn danh sách phải 3,4,5,1
Quá trình chia Quá trình trộn
Trộn danh sách trái 2,6,7 với danh sách phải 1,3,4,5
Danh sách trái Danh sách phải Danh sách trộn 2,6,7 1,3,4,5 1,2,3,4,5,6,7
Dãy được sắp xếp: 1,2,3,4,5,6,7
2.3.2 Quicksort
a Ý tưởng:
- Ý tưởng của thuật toán này là "chia để trị", nói cách khác chúng ta tìm cách chia đôi dãy ban đầu bằng cách chọn ra một phần tử là chốt (pivot) Từ dãy ban đầu, tất cả phần tử nhỏ hơn phần tử chốt thì đưa về bên trái dãy, tất cả các phần tử lớn hơn hoặc bằng phần tử chốt thì đưa về bên phải dãy Sau bước này ta có phần tử chốt là đứng đúng vị trí Dãy ban đầu phân chia làm hai dãy con nằm hai bên chốt
- Tiếp tục phân chia các dãy con theo cách tương tự đến khi mọi dãy con đều có độ dài bằng 1
Trang 10- Có thể lựa chọn phần tử chốt nằm đầu, cuối hay giữa dãy Ở đây ta sẽ lựa chọn phần tử chốt nằm gần giữa dãy nhất
Vấn đề đặt ra là mảng con T[i j] cần được ngăn bởi vật trung tâm p=T[i] Để cân bằng kích thước của 2 mảng này ta có thể sử dụng phần tử ở giữa (median) như là vật trung tâm Đáng tiếc là việc tìm phần ở giữa cũng mất 1 thời gian đáng kể Để giải quyêt điều đó đơn giản là chúng ta sử dụng 1 phần tử tuỳ ý trong mảng cần sắp như là vật trung tâm và hi vọng nó là tốt nhất có thể
Một cách làm có thể chấp nhận được là:
Duyệt qua từng phần tử của của nó chỉ một lần nhưng bắt đầu từ hai phía (đầu và cuối mảng) Khi đó khởi tạo k=i; l=j+1, k tăng dần cho đến khi T[k] > p, l giảm dần cho đến khi T[l] ( l Tiếp đến hoán vị T[k] và T[l] Quá trình này tiếp tục cho đến khi k ( l Cuối cùng đổi chổ T[i] và T[l] cho nhau và lúc này ta xác định đúng vị trí của phần tử trung tâm
b Giải thuật:
Sắp xếp một đoạn bất kỳ X[L], ,X[R] với điều kiện L < R
- Bước 1: pirot = (L+R) div 2, key=X[pirot]
- Bước 2: i=L+1, j=R
- Bước 3:
* Nếu X[i] < key thì i=i+1;
* Nếu X[j] > key thì j=j-1;
- Bước 4: Nếu i>j thì đổi chổ X[i] với X[j], quay về Bước 3
- Bước 5: Lặp lại từ Bước 1 đến Bước 3 với đoạn X[L], ,X[j-1] và X[j] đến X[R], dừng khi tất cả đoạn có độ dài là 1
Pivot(T[i j], int l)
(* Hoán vị các phần tử trong mảng T[i j] và cuối cùng trả về giá trị l (1≤l≤j)
sao cho T[l]=p ,T[k]≤p với mọi k (i≤k < l) và T[k] > p với mọi k (l < k≤ j), ở đây
p được khởi tạo là T[i] *)
{
p:=T[i]; k:=i; l:=j+1;
repeat k:=k+1; until (T[k] > p) or (k≥j);
repeat l:=l-1; until (T[l] < p);
while (k<l)
{
Swap(T[k],T[l]); (* Đổi chỗ T[k] và T[l] *)
repeat k:=k+1; until (T[k] > p); repeat l:=l-1; until (T[l]≤p); Swap(T[i],T[l]); }
Sau đây là giải thuật sắp xếp với tên gọi là Quicksort dùng để sắp xếp mảng T[1 n]:
Quicksort(T[i j]); (* Sắp xếp theo thứ tự không giảm *)
Trang 11if n đủ nhỏ then Insert(T[i j]);
else {
pivot(T[i j],l);
quicksort(T[i l-1];
quicksort(T[l+1,j];
}
}
Hình vẽ sau cho thấy sự làm việc của pivot và quicksort
Hình 3: Mô phỏng quá trình sắp xếp bằng giải thuật quicksort
c Đánh giá thuật toán:
Việc thiết kế giải thuật ngăn cách mảng bằng vật trung tâm với thời gian tuyến tính không phải là sự thách đố (có thể làm được) Tuy nhiên điều đó là cần thiết để so sánh với các giải thuật sắp xếp khác như là heapsort Việc tìm phần ở giữa cũng mất 1 thời gian đáng kể Để giải quyêt điều đó đơn giản là chúng ta sử dụng 1 phần tử tuỳ ý trong mảng cần sắp như là vật trung tâm và hi vọng nó là tốt nhất có thể
Quicksort sẽ không hiệu quả nếu sử dụng việc gọi đệ quy của các bài toán con mà không chú ý đến sự cân bằng kích thước của chúng Tình huống xấu nhất là khi T đã được sắp trước mà gọi quicksort và thời gian dùng quicksort để sắp là O( n 2)
Gọi t( n ) là thời gian trung bình dùng quicksort để sắp mảng n phần tử T[1 n ] là giá trị trả về khi gọi pivot(T[1 n],l) Theo pivot() thì l nằm giữa 1 ( n và xác suất là 1/n Thời gian để tìm vật trung tâm g(n) là tuyến tính Thời gian để dùng đệ qui để sắp xếp hai
Trang 12mảng con kích thước (l-1) và (n-l) tương ứng là t(n-1) và t(n-l) Như vậy với n đủ lớn ta có:
T(n)=
n l
l n t n
t n g
1
Hay rõ ràng hơn ta chọn n0 là giá trị đủ lớn để sử dụng công thức trên Nghĩa là nếu n n<n0 thì ta dùng sắp xếp chèn Khi đó gọi d là hằng số sao cho g( n ) ≤ dn vớin>n0
Ta có t(n) ≤ dn + t(n)=
n l
l n t n
t n g
1
với n > n0
≤ dn +
n k k t
2
(2.7.1)
Với công thức như trên quả là khó phân tích độ phức tạp Ta dự đoán nó tương tự mergesort và hi vọng nó là như vậy tức là vào cỡ O(nlogn) Thật vậy ta có định lý sau:
Định lý: Quicksort cần nlogn thời gian để sắp xếp n phần tử trong trường hợp trung bình.
Chứng minh
Gọi t( n ) là thời gian cần thiết để sắp n phần tử trong trường hợp trung bình a,n 0 là các hằng số giống như công thức 2.7.1
Ta chứng minh t( n ) = cnlogn với mọi n≥ 2 với c là hằng số
Dùng phương pháp qui nạp để chứng minh:
- Với mọinnguyên dương: (2 ≤n≤n) Dễ thấy t( n ) ≤ cnlogn
Chọn c ≥ , ,
log
) (
n n n
n t
2 ≤ n ≤ n0 (2.7.2)
- Bước qui nạp
Ta có
Giả thiết rằng: t(k) ≤ cklogk với mọi 2 ≤ k < n
Ta chỉ ra c sao cho t( n ) ≤ c.nlogn
Lấy a = t(0) +t(1)
T(n) = dn +
1
0 ) (
2 n k k t n
Theo giả thiết qui nạp : t(k) = cklogk
T(n) ≤ dn +
1
2 log 2
k
k ck n
n a
dn +
n
k
xdx x
n
c n
a
2
log 2
2
4 2
log 2
n
c n
a
k=2,n