🔎 C++ Programming • Binary Search

TÌM KIẾM NHỊ PHÂN
và Chặt nhị phân kết quả

Bài giảng được trình bày theo hướng hiện đại, ngắn gọn, dễ hiểu: nắm ý tưởng, nhận biết dạng bài, dùng hàm thư viện và luyện bài tập có ví dụ.

O(log n)Tốc độ tìm kiếm nhanh
sort()Dãy cần có thứ tự
lower_boundTìm vị trí gần đúng
P(x)Chặt nhị phân đáp án
1

Lý thuyết tìm kiếm nhị phân

Bài toán tìm kiếm

Cho một dãy gồm n đối tượng, mỗi đối tượng có một khóa tìm kiếm. Cần xác định có đối tượng nào có khóa bằng giá trị k hay không.

Dữ liệuKhóa kTìm thấy / Không tìm thấy

Tìm kiếm tuần tự có thể phải duyệt hết dãy, độ phức tạp thường là O(n).

Khi nào dùng nhị phân?

Tìm kiếm nhị phân chỉ dùng được khi không gian tìm kiếm có thứ tự, thường là dãy đã được sắp xếp tăng hoặc giảm.

Điểm mấu chốt: mỗi lần so sánh với phần tử giữa, ta loại bỏ được một nửa không gian tìm kiếm.

Độ phức tạp tìm kiếm là O(log n). Nếu phải sắp xếp trước thì cần tính thêm chi phí sắp xếp.

Ý tưởng thuật toán

leftmidright

Giả sử dãy tăng dần. Tại mỗi bước, xét phần tử giữa mid:

  • Nếu a[mid] == x: tìm thấy.
  • Nếu a[mid] < x: bỏ nửa trái, tìm tiếp bên phải.
  • Nếu a[mid] > x: bỏ nửa phải, tìm tiếp bên trái.
binary_search(a, x):
    left = 1, right = n
    while left <= right:
        mid = (left + right) / 2
        if a[mid] == x: return true
        if a[mid] < x: left = mid + 1
        else: right = mid - 1
    return false
2

Các hàm tìm kiếm có sẵn trong C++

HàmÝ nghĩaGhi nhớ nhanh
binary_search(first,last,x)Kiểm tra x có tồn tại trong đoạn đã sắp xếp hay không.Trả về true / false.
lower_bound(first,last,x)Tìm vị trí đầu tiên có giá trị không nhỏ hơn x.Đầu tiên ≥ x.
upper_bound(first,last,x)Tìm vị trí đầu tiên có giá trị lớn hơn x.Đầu tiên > x.
equal_range(first,last,x)Trả về đoạn chứa các phần tử bằng x.Dùng để kiểm tra hoặc đếm số lần xuất hiện.
Lưu ý: các hàm trên hoạt động đúng khi dãy đã được sắp xếp theo cùng tiêu chí so sánh.

Tìm phần tử gần nhất ≤ x

Dùng upper_bound để tìm phần tử đầu tiên lớn hơn x, sau đó lùi lại một vị trí.

Dãy

3 5 10 11 13 18 18 25 27 31

x = 20

Vị trí: 7
Giá trị: 18

Tìm phần tử gần nhất ≥ x

Dùng lower_bound để tìm phần tử đầu tiên không nhỏ hơn x.

Dãy

1 2 4 4 6 9 10

x = 4

Vị trí đầu tiên ≥ 4: 3
3

Chặt nhị phân trên kết quả

Ý tưởng chung

Chặt nhị phân không chỉ dùng để tìm trong mảng. Nó còn dùng để tìm kết quả nhỏ nhất hoặc lớn nhất thỏa mãn điều kiện, miễn là ta xây dựng được hàm kiểm tra P(x) có tính đơn điệu.

falsefalsetruetrue
Dạng thường gặp: Tìm x nhỏ nhất sao cho P(x) = true, hoặc tìm x lớn nhất sao cho P(x) = false.
Cần kiểm tra: khoảng tìm kiếm đủ rộng chưa, hàm P(x) có thật sự đơn điệu chưa, và mục tiêu là tìm nhỏ nhất hay lớn nhất.

Tìm x nhỏ nhất thỏa P(x)

res = -1
while left <= right:
    mid = (left + right) / 2
    if P(mid) == true:
        res = mid
        right = mid - 1
    else:
        left = mid + 1

Tìm x lớn nhất thỏa P(x)

res = -1
while left <= right:
    mid = (left + right) / 2
    if P(mid) == true:
        res = mid
        left = mid + 1
    else:
        right = mid - 1
4

Bảng tổng quan bài tập

BàiTên bàiKỹ năng chínhMức độ
1Kiểm tra tồn tạibinary_searchDễ
2Đếm số lần xuất hiệnlower_bound / upper_boundDễ
3Phần tử gần nhất ≤ xupper_boundTrung bình
4Phần tử gần nhất ≥ xlower_boundTrung bình
5Số chính phương thiếuSắp xếp + tìm kiếm nhị phânTrung bình
6Căn bậc hai nguyênChặt nhị phân kết quảTrung bình
7Cắt dâyTìm giá trị lớn nhất thỏa điều kiệnKhá
8Chia sáchTìm giá trị nhỏ nhất thỏa điều kiệnKhá
9Tìm căn số thựcNhị phân trên số thựcTrung bình
10Khoảng cách routerBinary search on answerKhá
5

Nội dung bài tập

Bài 1. Kiểm tra tồn tại

EXIST.*

Cho dãy số nguyên gồm n phần tử và q truy vấn. Mỗi truy vấn cho một số nguyên x. Hãy cho biết x có xuất hiện trong dãy hay không.

Dữ liệu vàon, q; dãy a; q dòng truy vấn x.
Kết quảMỗi truy vấn in YES hoặc NO.
Giới hạn1 ≤ n, q ≤ 105

Input

6 4
1 8 3 3 10 5
3
2
10
7

Output

YES
NO
YES
NO
Gợi ý nhanh: Sắp xếp dãy, sau đó dùng binary_search cho từng truy vấn.

Bài 2. Đếm số lần xuất hiện

COUNTX.*

Cho dãy số nguyên gồm n phần tử và q truy vấn. Với mỗi truy vấn x, hãy in ra số lần x xuất hiện trong dãy.

Dữ liệu vàon, q; dãy a; q dòng x.
Kết quảMỗi dòng là số lần xuất hiện của x.
Giới hạn|a[i]|, |x| ≤ 109

Input

8 3
1 2 2 2 5 5 9 10
2
5
4

Output

3
2
0
Gợi ý nhanh: Sau khi sắp xếp, số lần xuất hiện = upper_bound(x) - lower_bound(x).

Bài 3. Phần tử gần nhất bé hơn hoặc bằng x

LEQX.*

Cho dãy số đã sắp xếp tăng dần. Với mỗi truy vấn x, hãy tìm vị trí lớn nhất của phần tử có giá trị ≤ x. Nếu không tồn tại, in -1.

Dữ liệu vàon, q; dãy a; q truy vấn x.
Kết quảVị trí tìm được, đánh số từ 1.
Giới hạn1 ≤ n, q ≤ 105

Input

10 3
3 5 10 11 13 18 18 25 27 31
20
2
18

Output

7
-1
7
Gợi ý nhanh: Dùng upper_bound(x), nếu vị trí trả về không phải đầu dãy thì lùi lại một phần tử.

Bài 4. Phần tử gần nhất lớn hơn hoặc bằng x

GEQX.*

Cho dãy số đã sắp xếp tăng dần. Với mỗi truy vấn x, hãy tìm vị trí nhỏ nhất của phần tử có giá trị ≥ x. Nếu không tồn tại, in -1.

Dữ liệu vàon, q; dãy a; q truy vấn x.
Kết quảVị trí tìm được, đánh số từ 1.
Giới hạn1 ≤ n, q ≤ 105

Input

7 4
1 2 4 4 6 9 10
4
5
11
0

Output

3
5
-1
1
Gợi ý nhanh: Dùng lower_bound(x). Nếu trả về cuối dãy thì không có đáp án.

Bài 5. Số chính phương nhỏ nhất không xuất hiện

MISSQUARE.*

Cho dãy số nguyên dương a gồm n phần tử. Hãy tìm số chính phương nhỏ nhất không xuất hiện trong dãy.

Dữ liệu vàon và n số nguyên dương.
Kết quảSố chính phương nhỏ nhất còn thiếu.
Giới hạn1 ≤ n ≤ 105

Input

7
1 4 9 16 25 36 49

Output

0
Gợi ý nhanh: Sắp xếp dãy. Duyệt i từ 0 đến √max + 1, kiểm tra i² có xuất hiện bằng binary_search.

Bài 6. Căn bậc hai nguyên

SQRTINT.*

Cho số nguyên dương n. Hãy tìm số nguyên x lớn nhất sao cho x² ≤ n.

Dữ liệu vàoMột số nguyên n.
Kết quảSố nguyên x.
Giới hạn1 ≤ n ≤ 1018

Input

30

Output

5
Gợi ý nhanh: Chặt nhị phân trên x. Hàm kiểm tra P(x): x² ≤ n.

Bài 7. Cắt dây

CUTWIRE.*

Có n đoạn dây, đoạn thứ i dài a[i]. Cần cắt thành ít nhất k đoạn dây bằng nhau có độ dài nguyên. Tìm độ dài lớn nhất có thể.

Dữ liệu vàon, k và n độ dài dây.
Kết quảĐộ dài lớn nhất.
Giới hạn1 ≤ n ≤ 105

Input

4 11
802 743 457 539

Output

200
Gợi ý nhanh: Với độ dài thử L, tính tổng a[i] / L. Nếu tổng ≥ k thì L hợp lệ.

Bài 8. Chia sách

BOOKS.*

Có n quyển sách theo thứ tự, quyển i có a[i] trang. Chia cho k học sinh, mỗi học sinh nhận một đoạn liên tiếp các quyển sách. Hãy tối thiểu hóa số trang nhiều nhất mà một học sinh phải đọc.

Dữ liệu vàon, k và n số a[i].
Kết quảSố trang lớn nhất nhỏ nhất có thể.
Giới hạn1 ≤ k ≤ n ≤ 105

Input

4 2
12 34 67 90

Output

113
Gợi ý nhanh: Chặt nhị phân đáp án M. Kiểm tra có thể chia sao cho mỗi nhóm có tổng trang ≤ M hay không.

Bài 9. Tìm căn số thực

SQRTREAL.*

Cho số thực dương n. Hãy tính căn bậc hai của n với độ chính xác 6 chữ số thập phân.

Dữ liệu vàoMột số thực n.
Kết quả√n, in 6 chữ số sau dấu phẩy.
Giới hạn0 < n ≤ 109

Input

2

Output

1.414214
Gợi ý nhanh: Tìm kiếm nhị phân trên số thực trong đoạn [0, max(1,n)], lặp khoảng 100 lần.

Bài 10. Khoảng cách router

ROUTER.*

Có n vị trí trên một đường thẳng. Cần đặt k router vào k vị trí khác nhau sao cho khoảng cách nhỏ nhất giữa hai router bất kỳ là lớn nhất.

Dữ liệu vàon, k và n vị trí.
Kết quảKhoảng cách nhỏ nhất lớn nhất có thể.
Giới hạn2 ≤ k ≤ n ≤ 105

Input

5 3
1 2 8 4 9

Output

3
Gợi ý nhanh: Sắp xếp vị trí. Với khoảng cách thử d, tham lam đặt router từ trái sang phải để kiểm tra có đặt đủ k router không.
style> body { user-select: none; }