Tiêu đề: Thuật toán chia để trị. 1. Giới thiệu: Chia để trị là 1 phương pháp áp dụng cho các bài toán có thể giải quyết bằng cách chia nhỏ ra thành các bài toán con từ việc giải quyết các bài toán con này. 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. Hình ảnh minh hoạ cho cách chia bài toán lớn thành nhiều bài toán nhỏ đơn giản hơn rồi sau đó tổng hợp lại thành lời giải cho bài toán ban đầu 2. Nguyên tắc hoạt động Nguyên tắc cơ bản của thuật toán chia để trị bao gồm ba bước chính: Bước 1: Chia\tách nhỏ Tại bước này thì bài toán ban đầu sẽ được chia thành các bài toán con cho đến khi không thể chia nhỏ được nữa. Các bài toán con kiểu sẽ trở thành 1 bước nhỏ trong việc giải quyết bài toán lớn. Bước 2: Trị\giải quyết bài toán con Tại bước này ta sẽ phải tìm phương án để giải quyết cho bài toán con một cách cụ thể. Bước 3: Kết hợp lời giải lại để suy ra lời giải Khi đã giải quyết xong cái bài toán nhỏ, lặp lại các bước giải quyết đó và kết hợp lại những lời giải đó để suy ra kết quả cần tìm (có thể ở dạng đệ quy). 3. Ví dụ minh hoạ Bài toán tình kiếm nhị phân Tìm kiếm nhị phân là một thuật toán dùng để tìm kiếm 1 phần tử trong một danh sách đã được sắp xếp. Thuật toán hoạt động như sau: Bước 1(Chia): Danh sách ban đầu sẽ được chia thành 2 nửa Bước 2 (Trị): Trong mỗi bước, so sánh phần tử cần tìm với phần tử nằm ở chính giữa danh sách. Nếu hai phần tử bằng nhau thì phép tìm kiếm thành công và thuật toán kết thúc. Nếu chúng không bằng nhau thì tùy vào phần tử nào lớn hơn, thuật toán lặp lại bước so sánh trên với nửa đầu hoặc nửa sau của danh sách. Vì số lượng phần tử trong danh sách cần xem xét giảm đi một nửa sau mỗi bước, nên thời gian thực thi của thuật toán là hàm lôgarit. Bước 3: Bằng việc lặp lại cách giải quyết như bước 2 ta sẽ tìm được kết quả. int binarySearch(int array[], int left, int right, int x) { // nếu chỉ số left > right dừng lại và return -1 không có kết quả if (left > right) return -1; // tìm chỉ số ở giữa của mảng int mid = (left + right) / 2; // nếu số cần tìm bằng số ở giữa của mảng thì return if (x == array[mid]) return mid; // nếu số cần tìm nhỏ hơn số ở giữa của mảng thì tìm sang nửa bên trái if (x < array[mid]) return binarySearch(array, left , mid-1, x); // nếu số cần tìm lớn hơn số ở giữa của mảng thì tìm sang nửa bên phải if (x > a[mid]) return binarySearch(a, mid+1 , right, x); } Bài toán Quicksort Bước 1(chia): Thuật toán quicksort chia danh sách cần sắp xếp mảng array[1..n] thành hai danh sách con có kích thước tương đối bằng nhau nhờ chỉ số của phần tử gọi là chốt, ta có thể chọn chốt là phần tử ở giữa, ở cuối, ở đầu hoặc phần tử ngẫu nhiên nào trong mảng. Bước 2(trị): Sau khi đã chia thành 2 mảng dựa vào phần tử chốt nhiệm vụ của bước này là phải sắp xếp sao cho: những phần tử nhỏ hơn hoặc bằng phần tử chốt được đưa về phía trước và nằm trong danh sách con thứ nhất, các phần tử lớn hơn chốt được đưa về phía sau và thuộc danh sách đứng sau(Trường hợp sắp xếp tăng dần). Cứ tiếp tục chia như vậy tới khi các danh sách con đều có độ dài bằng 1 Bước 3: Bằng việc lặp các bước giải quyết các bài toán con trên ta sẽ thu được kết quả là mảng sẽ được sắp xếp. Dưới đây là hình ảnh minh họa việc thực hiện thuật toán quicksort: // hàm giải quyết việc sắp xếp các phần tử ở hai đầu của mảng // dựa vào phần tử chốt là phần tử cuối mảng int partition(int arr[], int low, int high) { // chốt được chọn ở đây là phần tử cuối mảng int pivot = arr[high]; int i = (low-1); for (int j=low; j<high; j++) { // nếu phần tử nhỏ hơn hoặc bằng với chốt if (arr[j] <= pivot) { i++; // đổi chỗ arr[i] và arr[j] int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } // đổi chỗ arr[i+1] và arr[high] (chốt) int temp = arr[i+1]; arr[i+1] = arr[high]; arr[high] = temp; // trả về chỉ số của chốt return i+1; } // Hàm thực hiện quicksort void sort(int arr[], int low, int high) { // nếu chỉ số của đầu mảng nhỏ hơn chỉ số cuối mảng if (low < high) { // tìm chỉ số của chốt sau khi đã thực hiện sắp xếp int pi = partition(arr, low, high); // lặp lại các bước với mảng từ phần tử đầu tiên đến chốt - 1 // và từ chốt + 1 đến phần tử cuối cùng của mảng. sort(arr, low, pi-1); sort(arr, pi+1, high); } } 4. Ưu và nhược điểm Ưu điểm Ưu điểm đầu tiên và cũng dễ nhận biết nhất của phương pháp chia để trị là nó giúp ta giải các bài toán khó. Ví dụ như bài toán tháp Hà Nội là một trò chơi giải đố toán học khó, nhưng với phương pháp chia để trị, nhờ việc chia bài toán thành các bài toán nhỏ hơn nó đã giải được một cách dễ dàng. Một ưu điểm khác của mô hình này, nó góp phần khám phá ra các thuật toán hiệu quả như Quick sort hay Merge sort. Nó sử dụng bộ nhớ cache một cách hiệu quả do khi bài toán con đủ đơn giản, chúng có thể được giải bên trong cache mà không phải truy cập bộ nhớ chính. Như trong bài toán sắp xếp nhanh, thay vì chia mảng thành các mảng con có 1 phần tử, thuật toán có thể kết hợp sắp xếp chèn cho mảng con có ít phần tử để sử dụng bộ nhớ cache hiệu quả hơn. Ngoài ra, việc tính toán một số bài toán con có thể thực hiện song song trên các bộ vi xử lý đa nhân, làm giảm thời gian tính toán một cách đáng kể. Nhược điểm Một trong những vấn đề phổ biến của chia để trị là việc sử dụng đệ quy khi cài đặt thuật toán. Trong một số trường hợp, làm giảm đi các ưu thế của phương pháp này. Đầu tiên việc một hàm tự gọi chính nó trong đệ quy cần nhiều không gian bộ nhớ và việc cấp phát bộ nhớ làm giảm tốc độ thuật toán khi đệ quy. Tiếp theo, một bài toán có thể chia thành nhiều bài toán con, và một bài toán con có thể xuất hiện nhiều lần làm thuật toán kém hiệu quả như trong bài toán tính số Fibonacci thứ n sử dụng phương pháp chia đệ trị bằng đệ quy. Một nhược điểm khác của phương pháp này là việc cài đặt thuật toán bằng phương pháp chia để trị đôi khi phức tạp hơn đáng kể so với phương pháp lặp truyền thống. 5. Bài tập Bài 1: Cho một dãy gồm n số nguyên và một số nguyên x. Hãy đếm xem trong dãy có bao nhiêu phần tử có giá trị x. Ví dụ: Test mẫu 1: Input Output 5 12324 2 2 Với a = [1, 2, 3, 2, 4] và x = 2 thì kết quả mong muốn là 2. Test mẫu 2: Input Output 4 1342 5 0 Với a = [1, 3, 4, 2] và x = 5 thì kết quả mong muốn là 0. Bài giải: #include<iostream> using namespace std; int a[100001]; int count(int a[], int l, int r, int x){ if (l == r){ if (a[l] == x) return 1; else return 0; } else { int m = (l+r)/2; return count(a, l, m, x) + count(a, m+1, r, x); } } int main(){ int n, x; cin >> n; for (int i = 0; i < n; i++){ cin >> a[i]; } cin >> x; cout << count(a, 0, n-1, x); return 0; }