Đệ Quy, Chia Để Trị & Kỹ Thuật Meet in the Middle
1. Khái niệm & bản chất của phân rã không gian tìm kiếm
Đệ quy (Recursion) và Chia để trị (Divide and Conquer) là nền tảng tư duy cốt lõi trong khoa học máy tính: chia bài toán lớn thành các bài toán con đồng dạng có kích thước nhỏ hơn, giải quyết độc lập và kết hợp nghiệm.
Ở Level 2, ta khai thác bước nhảy vọt về tư duy tối ưu hóa:
- Cây đệ quy & Định lý thợ (Master Theorem): Phân tích chính xác chi phí thời gian của các hàm đệ quy phân nhánh $T(N) = a T(N/b) + \mathcal{O}(N^d)$.
- Kỹ thuật Đếm nghịch thế (Inversion Count): Vận dụng Merge Sort để đếm số cặp nghịch thế $i < j$ mà $A_i > A_j$ trong $\mathcal{O}(N \log N)$ (thay vì duyệt ngây thơ $\mathcal{O}(N^2)$).
- Kỹ thuật Gặp nhau ở giữa (Meet in the Middle - MITM): Khi không gian tìm kiếm là $2^N$ với $N = 40$ ($2^{40} \approx 10^{12} \implies \text{TLE}$), ta chia đôi tập hợp thành hai nửa $N/2 = 20$. Duyệt hai nửa độc lập ($2 \times 2^{20} \approx 2 \times 10^6$) rồi dùng Two Pointers / Binary Search để ghép nghiệm $\implies$ Giảm độ phức tạp từ $\mathcal{O}(2^N)$ xuống $\mathcal{O}(2^{N/2} \log(2^{N/2}))$.
2. Kỹ thuật đếm số cặp nghịch thế bằng Merge Sort
2.1. Bản chất toán học
Trong quá trình trộn (merge) hai nửa đã sắp xếp $[L \dots mid]$ và $[mid+1 \dots R]$:
Nếu phần tử bên nửa phải $A[j]$ nhỏ hơn phần tử bên nửa trái $A[i]$ ($A[j] < A[i]$), thì do nửa trái đã tăng dần, $A[j]$ sẽ nhỏ hơn tất cả các phần tử từ $i$ đến $mid$.
Số lượng cặp nghịch thế tạo bởi $A[j]$ chính là:
$$\Delta = mid - i + 1$$
long long merge_and_count(vector<int> &a, int l, int mid, int r) {
vector<int> left(a.begin() + l, a.begin() + mid + 1);
vector<int> right(a.begin() + mid + 1, a.begin() + r + 1);
int i = 0, j = 0, k = l;
long long inv_count = 0;
while (i < left.size() && j < right.size()) {
if (left[i] <= right[j]) {
a[k++] = left[i++];
} else {
a[k++] = right[j++];
inv_count += (left.size() - i); // Khai thác tính chất tăng dần
}
}
while (i < left.size()) a[k++] = left[i++];
while (j < right.size()) a[k++] = right[j++];
return inv_count;
}
3. Kỹ thuật Meet in the Middle
3.1. Bài toán Knapsack với $N \le 40$ và $W \le 10^{18}$
- Không thể dùng Quy hoạch động vì $W = 10^{18}$ quá lớn.
- Không thể duyệt nhánh cận toàn phần vì $2^{40} \approx 10^{12}$ quá lớn.
3.2. Thuật toán 3 bước MITM
- Nửa 1 ($N_1 = 20$): Sinh tất cả $2^{20}$ tổng tập con, lưu vào
vector<long long> sum1. Sắp xếp và lọc bỏ các trạng thái không tối ưu. - Nửa 2 ($N_2 = 20$): Sinh tất cả $2^{20}$ tổng tập con, lưu vào
vector<long long> sum2. - Ghép nghiệm: Với mỗi giá trị $S \in sum2$, tìm giá trị lớn nhất trong $sum1$ mà $\le W - S$ bằng
upper_boundtrong $\mathcal{O}(\log(2^{N_1}))$.
4. Mẫu cài đặt chuẩn thi đấu: Meet in the Middle
#include <bits/stdc++.h>
using namespace std;
void generate_sums(int idx, int end_idx, long long current_sum, const vector<long long> &a, vector<long long> &res) {
if (idx == end_idx) {
res.push_back(current_sum);
return;
}
generate_sums(idx + 1, end_idx, current_sum, a, res); // Không chọn
generate_sums(idx + 1, end_idx, current_sum + a[idx], a, res); // Có chọn
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
long long w;
if (!(cin >> n >> w)) return 0;
vector<long long> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
int mid = n / 2;
vector<long long> sum1, sum2;
generate_sums(0, mid, 0, a, sum1);
generate_sums(mid, n, 0, a, sum2);
sort(sum2.begin(), sum2.end());
long long max_weight = 0;
for (long long s1 : sum1) {
if (s1 <= w) {
auto it = upper_bound(sum2.begin(), sum2.end(), w - s1);
if (it != sum2.begin()) {
--it;
max_weight = max(max_weight, s1 + *it);
}
}
}
cout << max_weight << "\n";
return 0;
}
5. Ranh giới áp dụng
| Phạm Vi $N$ | Thuật Toán Tối Ưu | Độ Phức Tạp |
|---|---|---|
| $N \le 20$ | Duyệt đệ quy / Bitmask toàn phần | $\mathcal{O}(2^N)$ |
| $N \le 40$ | Meet in the Middle (MITM) | $\mathcal{O}(2^{N/2} \log(2^{N/2}))$ |
| $N \le 10^5, W \le 10^5$ | Quy hoạch động Cái túi (DP Knapsack) | $\mathcal{O}(NW)$ |
| $N \le 10^5, W \le 10^{18}$ | Tham lam (nếu các phần tử chia hết) | $\mathcal{O}(N \log N)$ |