Bài 3: Tìm Kiếm Nhị Phân - Chia Để Trị Trong 20 Bước!¶
Tác giả: FPTOJ Team
Nội dung tham khảo từ: VNOI Wiki - Tìm kiếm nhị phân, Topcoder - Binary Search
Bản chất vấn đề¶
Bài toán: Tìm sách trong thư viện¶
Bạn vào thư viện có 1.000.000 cuốn sách xếp theo thứ tự ABC. Bạn muốn tìm cuốn sách có tên bắt đầu bằng chữ "T".
Cách 1 - Tìm tuyến tính (dùng tay): Đọc từng cuốn từ đầu đến cuối → Cần tới \(1.000.000\) lần kiểm tra.
Cách 2 - Tìm nhị phân (thông minh):
- Mở ra giữa giá sách → thấy chữ "M" → "T" nằm sau "M" → bỏ nửa đầu!
- Mở ra giữa nửa sau → thấy chữ "S" → "T" nằm sau "S" → bỏ thêm nửa!
- Mở ra giữa phần còn lại → thấy chữ "T" → Tìm thấy!
Chỉ cần \(\approx 20\) bước thay vì \(1.000.000\) bước! Đó là sức mạnh của Tìm kiếm nhị phân (Binary Search)!
Điều kiện áp dụng¶
Tìm kiếm nhị phân chỉ hoạt động khi dữ liệu có tính đơn điệu:
- Đơn điệu tăng: Nếu \(x\) tăng thì \(f(x)\) cũng tăng (hoặc giữ nguyên).
- Ví dụ: Mảng đã sắp xếp tăng \([1, 3, 5, 7, 9]\) → chỉ số tăng thì giá trị tăng.
- Tổng quát hơn: Hàm kiểm tra \(P(x)\) có dạng \(\underbrace{false, \ldots, false}_{k}, \underbrace{true, \ldots, true}_{n-k}\).
Nhận biết bài Binary Search¶
Bài toán có thể dùng Binary Search khi:
- Cần tìm giá trị min/max thỏa mãn điều kiện nào đó
- Hàm kiểm tra có tính chất đơn điệu: nếu \(x\) thỏa mãn thì mọi \(y > x\) (hoặc \(y < x\)) cũng thỏa mãn
- Dễ dàng viết hàm kiểm tra \(P(x)\) với độ phức tạp hợp lý
Tư duy cốt lõi¶
Tìm kiếm nhị phân cơ bản - Tìm phần tử trong mảng đã sắp xếp¶
Ý tưởng cốt lõi:
- Giữ 2 biến
lo(đầu) vàhi(cuối) đánh dấu khoảng tìm kiếm - Xét phần tử ở giữa
mid: - Nếu \(a[mid] = target\) → Tìm thấy!
- Nếu \(a[mid] < target\) → Target nằm bên phải →
lo = mid + 1 - Nếu \(a[mid] > target\) → Target nằm bên trái →
hi = mid - 1 - Lặp lại cho đến khi
lo > hi(không tìm thấy)
Minh họa: Tìm \(55\) trong mảng \([0, 5, 13, 19, 21, 41, 55, 68, 72, 81, 98]\)
| Bước | lo | hi | mid | \(a[mid]\) | So sánh | Kết quả |
|---|---|---|---|---|---|---|
| 1 | 0 | 10 | 5 | 41 | \(41 < 55\) | lo = 6 |
| 2 | 6 | 10 | 8 | 72 | \(72 > 55\) | hi = 7 |
| 3 | 6 | 7 | 6 | 55 | \(55 = 55\) | Tìm thấy! |

Tìm kiếm nhị phân tổng quát - "Ma thuật" thực sự!¶
Ngoài việc tìm phần tử trong mảng, Binary Search còn giải được rất nhiều bài toán khác! Bí quyết là:
Thiết kế hàm kiểm tra \(P(x)\) có tính chất: Nếu \(P(x) = true\) thì mọi \(y > x\) cũng \(= true\).
Khi đó dãy \(P(x)\) sẽ có dạng: \(false, false, \ldots, false, true, true, \ldots, true\)
→ Dùng Binary Search tìm giá trị \(x\) nhỏ nhất mà \(P(x) = true\)!
Ví dụ thực tế - Bài toán vận chuyển:
Có \(N\) gói hàng, cần chuyển trong \(days\) ngày. Mỗi ngày chỉ chở được tối đa \(MAX\) kg. Tìm \(MAX\) nhỏ nhất!
Hàm kiểm tra \(P(MAX)\): "Với sức chở \(MAX\), có thể chuyển hết hàng trong \(days\) ngày không?"
- \(P(MAX) = true\) → mọi \(MAX' > MAX\) cũng \(= true\) (chở được nhiều hơn → càng dễ xong)
- \(P(MAX) = false\) → mọi \(MAX' < MAX\) cũng \(= false\) (chở ít hơn → càng không xong)
→ Dùng Binary Search tìm \(MAX\) nhỏ nhất mà \(P(MAX) = true\)!
Các biến thể Binary Search¶
| Biểu thức | Ý nghĩa | Khi nào dùng |
|---|---|---|
lower_bound(a.begin(), a.end(), x) |
Vị trí đầu tiên mà \(a[i] \geq x\) | Tìm phần tử \(\geq x\) |
upper_bound(a.begin(), a.end(), x) |
Vị trí đầu tiên mà \(a[i] > x\) | Tìm phần tử \(> x\) |
lower_bound + upper_bound |
Khoảng \([l, r)\) mà \(a[i] = x\) | Đếm số phần tử bằng \(x\) |
Làm tròn khi chia số nguyên¶
Khi tính mid = (lo + hi) / 2 với số nguyên:
- \((3 + 7) / 2 = 5\)
- \((3 + 8) / 2 = 5\) (làm tròn xuống)
Lưu ý quan trọng: Dùng mid = lo + (hi - lo) / 2 thay vì (lo + hi) / 2 để tránh tràn số khi \(lo + hi\) quá lớn!
Phân tích tính đúng đắn¶
Tại sao Binary Search luôn đúng?¶
Bất biến (Invariant): Trong mỗi bước, đáp án luôn nằm trong khoảng \([lo, hi]\).
- Ban đầu: \([lo, hi] = [0, n-1]\) chứa toàn bộ mảng → đáp án chắc chắn nằm trong đây.
- Mỗi bước: Ta loại bỏ chính xác nửa không gian tìm kiếm không chứa đáp án.
- Nếu \(a[mid] < target\): mọi phần tử \(\leq a[mid]\) đều \(< target\) → bỏ an toàn.
- Nếu \(a[mid] > target\): mọi phần tử \(\geq a[mid]\) đều \(> target\) → bỏ an toàn.
- Khi dừng: \(lo > hi\) → khoảng rỗng → không có phần tử nào bằng \(target\).
Tại sao template tổng quát đúng?¶
Với template tìm \(x\) nhỏ nhất mà \(P(x) = true\):
Bất biến: Đáp án luôn nằm trong \([lo, hi]\), và \(P(hi) = true\).
- Nếu \(P(mid) = true\): \(mid\) có thể là đáp án, nhưng cũng có thể có đáp án nhỏ hơn → giữ \(mid\) (gán \(hi = mid\)).
- Nếu \(P(mid) = false\): \(mid\) chắc chắn không phải đáp án, và mọi \(x < mid\) cũng \(= false\) → loại bỏ \(mid\) (gán \(lo = mid + 1\)).
Bẫy lặp vô hạn¶
Khi lo = hi - 1, nếu dùng mid = lo + (hi - lo) / 2 thì \(mid = lo\).
- Nếu \(P(mid) = false\) mà gán
lo = mid→lokhông đổi → lặp vô hạn! - Giải pháp: Luôn gán
lo = mid + 1khi \(P(mid) = false\).
Khi tìm \(x\) lớn nhất mà \(P(x) = false\), cần dùng mid = lo + (hi - lo + 1) / 2 (làm tròn lên) để tránh lặp vô hạn.
Đánh giá độ phức tạp¶
Độ phức tạp thời gian: \(O(\log N)\)¶
Mỗi bước giảm một nửa không gian tìm kiếm:
Số bước tối đa: \(\lceil \log_2 N \rceil\)
| \(N\) | Số bước tối đa |
|---|---|
| \(1.000\) | \(10\) |
| \(1.000.000\) | \(20\) |
| \(1.000.000.000\) | \(30\) |
| \(10^{18}\) | \(60\) |
Lưu ý: Nếu mỗi bước gọi hàm kiểm tra \(P(x)\) với độ phức tạp \(O(M)\), tổng độ phức tạp là \(O(M \log N)\).
Độ phức tạp không gian: \(O(1)\)¶
Chỉ sử dụng một số biến cố định (lo, hi, mid) → không gian \(O(1)\).
Code minh họa¶
Binary Search cơ bản¶
Binary Search tổng quát - Tìm \(x\) nhỏ nhất mà \(P(x) = true\)¶
Binary Search tìm \(x\) lớn nhất mà \(P(x) = true\)¶
Bài toán vận chuyển (LeetCode 1011)¶
Dùng thư viện STL / Python¶
Cạm bẫy hay gặp¶
Bẫy 1: Lặp vô hạn¶
Bẫy 2: Tràn số khi tính mid¶
Bẫy 3: Sai cận khi tìm "false cuối cùng"¶
Khi tìm \(x\) lớn nhất mà \(P(x) = false\), cần dùng mid = lo + (hi - lo + 1) / 2 (làm tròn lên):
Lý do: Nếu dùng mid = lo + (hi - lo) / 2 và chỉ còn 2 phần tử \([false, true]\), mid sẽ \(= lo\), vòng lặp sẽ lặp vô hạn!
Bẫy 4: Quên kiểm tra điều kiện mảng đã sắp xếp¶
Binary Search chỉ hoạt động trên dữ liệu đã sắp xếp! Nếu mảng chưa sắp xếp → kết quả sai hoàn toàn.
Bẫy 5: Binary Search trên số thực¶
Dừng khi khoảng đủ nhỏ (\(\varepsilon = 10^{-9}\)) hoặc lặp đúng 100 lần:
Bài tập luyện tập¶
| Bài | Nền tảng | Độ khó | Chủ đề |
|---|---|---|---|
| CSES - Distinct Values | CSES | ⭐ | Binary search cơ bản |
| CSES - Factory Machines | CSES | ⭐⭐ | BS on answer |
| CSES - Array Division | CSES | ⭐⭐⭐ | Tổng max min |
| LeetCode 704 - Binary Search | LC | ⭐ | BS cơ bản |
| LeetCode 34 - Find First and Last Position | LC | ⭐⭐ | lower/upper bound |
| LeetCode 875 - Koko Eating Bananas | LC | ⭐⭐ | BS on answer |
| LeetCode 1011 - Capacity To Ship | LC | ⭐⭐ | BS on answer |
| VNOJ - Sort | VNOJ | ⭐⭐ | BS + sorting |
| VNOJ - VOSTR | VNOJ | ⭐⭐⭐ | BS trên xâu |
| SPOJ - Aggressive Cows | SPOJ | ⭐⭐ | Khoảng cách min |
Tài liệu tham khảo¶
- VNOI Wiki - Tìm kiếm nhị phân
- Topcoder - Binary Search
- CP-Algorithms - Binary Search
- USACO Guide - Binary Search
- YouTube - Binary Search (Errichto)
- LeetCode - Binary Search Study Plan
Bài tiếp theo: Kĩ thuật hai con trỏ →