Tìm Kiếm Nhị Phân Nâng Cao
1. Khái niệm & bản chất của tìm kiếm nhị phân trong không gian nghiệm
Tìm kiếm nhị phân (Binary Search) không chỉ giới hạn ở việc tìm kiếm một phần tử trên mảng đã sắp xếp trong $\mathcal{O}(\log N)$, mà ở cấp độ thi đấu nâng cao, nó là một phương pháp tối ưu hóa tổng quát trên không gian hàm đơn điệu:
- Chặt nhị phân kết quả (Binary Search on Answer): Chuyển đổi một bài toán tối ưu hóa khó ("Tìm giá trị $X$ nhỏ nhất/lớn nhất thỏa mãn điều kiện...") thành một chuỗi các bài toán kiểm tra tính khả thi dễ dàng ("Với giá trị $X$ cho trước, có thể đạt được mục tiêu hay không?") thông qua một hàm kiểm tra đơn điệu
check(X). - Tìm kiếm nhị phân trên số thực (Real Binary Search): Tìm nghiệm của phương trình hoặc hàm số liên tục với độ chính xác tuyệt đối $\epsilon = 10^{-7}$.
- Tìm kiếm tam phân (Ternary Search): Tìm điểm cực trị (cực đại/cực tiểu) của hàm số đơn phong (unimodal function) trong $\mathcal{O}(\log N)$.
2. Bản chất toán học của tính đơn điệu (Monotonicity Criterion)
2.1. Điều kiện tiên quyết để chặt nhị phân
Một bài toán có thể giải bằng chặt nhị phân kết quả khi và chỉ khi hàm kiểm tra check(X) có tính đơn điệu trên toàn bộ không gian tìm kiếm:
$$\text{Không gian tìm Min:} \quad [\text{False, False, False, } \dots \mathbf{\text{True, True, True}}]$$ $$\text{Không gian tìm Max:} \quad [\text{True, True, True, } \dots \mathbf{\text{False, False, False}}]$$
Điểm chuyển giao giữa False và True (hoặc True và False) chính là nghiệm tối ưu toàn cục cần tìm.
2.2. Bảng mô phỏng: Bài toán chia $N$ đoạn gỗ thành $\ge K$ phần bằng nhau
Cho 3 khúc gỗ độ dài $[15, 20, 25]$, cần cắt thành ít nhất $K = 5$ đoạn có độ dài nguyên $X$. Hàm kiểm tra: $\text{check}(X) = \lfloor 15/X \rfloor + \lfloor 20/X \rfloor + \lfloor 25/X \rfloor \ge 5$.
| Độ dài thử $X$ | Số đoạn cắt được | Điều kiện $\ge 5$ (check(X)) |
Đánh giá & Thu hẹp không gian |
|---|---|---|---|
| $1$ | $15 + 20 + 25 = 60$ | True | Khả thi $\implies$ Thử tăng $X$ |
| $5$ | $3 + 4 + 5 = 12$ | True | Khả thi $\implies$ Thử tăng $X$ |
| $10$ | $1 + 2 + 2 = 5$ | True | Khả thi (Ghi nhận đáp án $X = 10$) |
| $11$ | $1 + 1 + 2 = 4$ | False | Không đủ đoạn $\implies$ Giảm $X$ |
| $12$ | $1 + 1 + 2 = 4$ | False | Không đủ đoạn |
$$\implies \text{Độ dài lớn nhất tìm được là } X = \mathbf{10}.$$
3. Hai mẫu cài đặt chặt nhị phân chuẩn thi đấu (Không bao giờ lặp vô tận)
3.1. Mẫu 1: Tìm giá trị nhỏ nhất thỏa mãn check(mid) == true (Tìm Min)
long long low = MIN_VAL, high = MAX_VAL;
long long ans = -1;
while (low <= high) {
long long mid = low + (high - low) / 2;
if (check(mid)) {
ans = mid; // Ghi nhận nghiệm hợp lệ
high = mid - 1; // Thu hẹp về nửa trái để tìm nghiệm nhỏ hơn
} else {
low = mid + 1; // Không đạt, buộc phải tăng nghiệm
}
}
3.2. Mẫu 2: Tìm giá trị lớn nhất thỏa mãn check(mid) == true (Tìm Max)
long long low = MIN_VAL, high = MAX_VAL;
long long ans = -1;
while (low <= high) {
long long mid = low + (high - low) / 2;
if (check(mid)) {
ans = mid; // Ghi nhận nghiệm hợp lệ
low = mid + 1; // Thu hẹp về nửa phải để tìm nghiệm lớn hơn
} else {
high = mid - 1; // Quá lớn, phải giảm nghiệm
}
}
Lưu ý chống tràn số: Luôn viết
mid = low + (high - low) / 2thay vì(low + high) / 2để tránh tràn số khilow + high > 2 \cdot 10^9.
4. Chặt nhị phân số thực
4.1. Tìm kiếm nhị phân trên số thực (Fixed Iterations)
Khi tìm nghiệm số thực, thay vì dùng while (high - low > EPS) dễ bị lỗi làm tròn vô tận, phương pháp chuẩn thi đấu là chạy lặp cố định 100 lần (đạt độ chính xác $\approx 2^{-100} \approx 10^{-30}$):
double low = 0.0, high = 1e9;
for (int iter = 0; iter < 100; ++iter) {
double mid = (low + high) / 2.0;
if (check_real(mid)) low = mid;
else high = mid;
}
cout << fixed << setprecision(6) << low << "\n";
4.2. Tìm kiếm tam phân (Ternary Search) trên hàm cực đại
Chia đoạn $[low, high]$ thành 3 phần bằng 2 điểm $m_1 = low + \frac{high - low}{3}$ và $m_2 = high - \frac{high - low}{3}$:
- Nếu $f(m_1) < f(m_2) \implies$ Cực đại nằm ở đoạn $[m_1, high] \implies low = m_1$.
- Ngược lại $\implies high = m_2$.
double ternary_search_max(double low, double high) {
for (int iter = 0; iter < 100; ++iter) {
double m1 = low + (high - low) / 3.0;
double m2 = high - (high - low) / 3.0;
if (f(m1) < f(m2)) low = m1;
else high = m2;
}
return f(low);
}
5. Mẫu cài đặt chuẩn thi đấu: Bài toán phân bổ công việc (Painter's Partition)
#include <bits/stdc++.h>
using namespace std;
// Kiểm tra xem có thể chia mảng thành <= K đoạn có tổng mỗi đoạn <= max_sum hay không
bool check(long long max_sum, const vector<long long> &a, int k) {
int count = 1;
long long current_sum = 0;
for (long long x : a) {
if (x > max_sum) return false;
if (current_sum + x > max_sum) {
count++;
current_sum = x;
} else {
current_sum += x;
}
}
return count <= k;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
if (!(cin >> n >> k)) return 0;
vector<long long> a(n);
long long low = 0, high = 0;
for (int i = 0; i < n; ++i) {
cin >> a[i];
low = max(low, a[i]);
high += a[i];
}
long long ans = high;
while (low <= high) {
long long mid = low + (high - low) / 2;
if (check(mid, a, k)) {
ans = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
cout << ans << "\n";
return 0;
}
6. Ranh giới áp dụng: Khi nào chặt nhị phân mảng vs Chặt nhị phân kết quả?
| Đặc Điểm | Binary Search trên Mảng | Binary Search trên Đáp Án (Answer) |
|---|---|---|
| Đối tượng tìm kiếm | Vị trí / phần tử trong mảng tĩnh | Giá trị mục tiêu $X$ trong không gian nghiệm $[L, R]$ |
| Yêu cầu bắt buộc | Mảng đã được sắp xếp | Hàm kiểm tra $\text{check}(X)$ có tính đơn điệu |
| Độ phức tạp | $\mathcal{O}(\log N)$ | $\mathcal{O}(\text{Time}(\text{check}) \times \log(\text{Range}))$ |
| Dấu hiệu đề bài | "Tìm vị trí đầu tiên $\ge K$" | "Tìm giá trị lớn nhất / nhỏ nhất sao cho..." |