Chương 0·Bài 05

Binary Search

Cho một dãy nn số đã sắp tăng dần và qq câu hỏi, mỗi câu hỏi xem số xx có nằm trong dãy hay không. Duyệt cả dãy cho từng câu hỏi tốn n⋅qn \cdot q phép so sánh; với n=q=2⋅105n = q = 2 \cdot 10^5 con số này là 4⋅10104 \cdot 10^{10}, quá xa giới hạn một giây. Nhưng dãy đã sắp, nên một lần so sánh với phần tử ở giữa đã loại được cả một nửa dãy. Tìm kiếm nhị phân (binary search) lặp lại việc đó và trả lời mỗi câu hỏi trong khoảng log⁡2n≈18\log_2 n \approx 18 bước.

Giá trị lớn hơn của kỹ thuật là nó không cần có mảng. Chỉ cần một điều kiện mà các giá trị lần lượt cho kết quả sai, sai, …, sai, đúng, đúng, …, đúng, thì ranh giới giữa hai đoạn tìm được sau khoảng log⁡2L\log_2 L lần kiểm tra, với LL là độ dài của miền. Đó là chặt nhị phân trên đáp án: thời gian nhỏ nhất, độ dài lớn nhất, giá trị lớn nhất mà tổng vẫn không vượt giới hạn. Phần lớn bài của chủ đề này thuộc dạng đó.

Tìm trong dãy đã sắp

Bài toán: cho a1≤a2≤⋯≤ana_1 \le a_2 \le \dots \le a_n và số xx, tìm vị trí đầu tiên có ai≥xa_i \ge x. Câu hỏi này luôn có đáp án, dù xx vắng mặt hay lặp lại nhiều lần (nếu không có vị trí nào thì quy ước đáp án là n+1n + 1), và từ nó đọc ra mọi thứ khác: xx có mặt khi vị trí đó ≤n\le n và giá trị ở đó bằng xx.

Xét vị trí giữa midmid. Nếu amid≥xa_{mid} \ge x thì midmid là một ứng viên, và vị trí đầu tiên cần tìm không nằm sau midmid. Nếu amid<xa_{mid} < x thì mọi vị trí từ midmid trở về trước đều nhỏ hơn xx vì dãy tăng. Mỗi lần so sánh loại được ít nhất một nửa đoạn còn nghi ngờ.

C++
ll l = 1, r = n, res = n + 1;
while (l <= r) {
    ll mid = (l + r) >> 1;
    if (a[mid] >= x) r = (res = mid) - 1;
    else l = mid + 1;
}

Vì sao đúng. Đoạn [l,r][l, r] là các vị trí chưa biết. Mọi vị trí trước ll đã biết là ai<xa_i < x, mọi vị trí sau rr đã biết là ai≥xa_i \ge x. Biến res luôn bằng r+1r + 1: lúc đầu res=n+1=r+1res = n + 1 = r + 1, mỗi lần r = (res = mid) - 1 lại gán cả hai cùng lúc, còn nhánh l = mid + 1 không đụng tới rr. Vì midmid luôn nằm trong [l,r][l, r] nên đoạn chưa biết co lại ít nhất một ô mỗi vòng, và vòng lặp dừng khi l=r+1l = r + 1: không còn vị trí nào chưa biết, mọi vị trí trước resres có ai<xa_i < x, mọi vị trí từ resres trở đi có ai≥xa_i \ge x. Vậy resres đúng là vị trí đầu tiên cần tìm, kể cả khi bằng n+1n + 1.

Với a=(2,5,8,12,16,23,38,56,72,91)a = (2, 5, 8, 12, 16, 23, 38, 56, 72, 91) và x=23x = 23: mid=5mid = 5, a5=16<23a_5 = 16 < 23 nên l=6l = 6; mid=8mid = 8, 56≥2356 \ge 23 nên res=8res = 8, r=7r = 7; mid=6mid = 6, 23≥2323 \ge 23 nên res=6res = 6, r=5r = 5. Dừng: vị trí đầu tiên có ai≥23a_i \ge 23 là 6.

Ô xanh dương là vùng chưa biết, ô cam là mid; mỗi lần so sánh loại một nửa vùng chưa biết.

Mỗi vòng đoạn [l,r][l, r] còn không quá một nửa, nên sau tối đa ⌈log⁡2(n+1)⌉\lceil \log_2 (n+1) \rceil vòng là xong: 18 vòng với n=2⋅105n = 2 \cdot 10^5, 60 vòng với miền cỡ 101810^{18}. Ba chỗ hay sai, và đều tránh được nếu giữ đúng khuôn:

  • Lặp vô hạn. Viết while (l < r) rồi nhánh l = mid thì khi r=l+1r = l + 1 ta có mid=lmid = l, ll không nhúc nhích và vòng lặp không bao giờ dừng. Khuôn trên luôn bỏ midmid ra khỏi đoạn chưa biết (mid - 1 hoặc mid + 1) nên không rơi vào chuyện đó.
  • Tràn số. (l + r) >> 1 dịch tổng sang phải một bit, tức chia đôi rồi bỏ phần lẻ, như (l + r) / 2 với số không âm. Nó an toàn với long long khi hai đầu không vượt cỡ 4⋅10184 \cdot 10^{18}. Với int, tổng hai số cỡ 10910^9 đã tràn.
  • Điều kiện không đơn điệu. Chương trình vẫn dừng và vẫn in ra một số, nhưng số đó vô nghĩa. Trước khi viết code, hãy nói được vì sao nếu xx thỏa thì x+1x + 1 (hay x−1x - 1) cũng thỏa.

Hỏi nhanh

Một bạn viết chặt nhị phân tìm vị trí đầu tiên có a[i] >= x như sau: l = 1, r = n; lặp while (l < r) với mid = (l + r) / 2; nếu a[mid] < x thì l = mid, ngược lại r = mid. Với n = 2, a = {1, 5} và x = 5, chuyện gì xảy ra?

Ví dụĐếm Không Vượt Ngưỡng

Đề. Mảng aa gồm n≤2⋅105n \le 2 \cdot 10^5 số, m≤2⋅105m \le 2 \cdot 10^5 truy vấn bjb_j. Với mỗi bjb_j đếm số phần tử ai≤bja_i \le b_j.

Lời giải. Sắp aa một lần. Số phần tử không vượt bb chính là vị trí đầu tiên có ai>ba_i > b, trừ 1: mọi phần tử trước vị trí đó đều ≤b\le b. Tìm vị trí đó bằng khuôn trên với điều kiện a[mid] > b. Cả bài O((n+m)log⁡n)O((n + m) \log n).

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll N = 2e5 + 5;
ll n, m, a[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    for (ll j = 1; j <= m; j++) {
        ll b;
        cin >> b;
        ll l = 1, r = n, res = n + 1;
        while (l <= r) {
            ll mid = (l + r) >> 1;
            if (a[mid] > b) r = (res = mid) - 1;
            else l = mid + 1;
        }
        cout << res - 1 << " \n"[j == m];
    }
}
6 4
-2 5 5 0 9 1
-3 0 5 10

Khuôn tổng quát: tìm ranh giới của một điều kiện

Chương trình ở mục trước chỉ chạm vào mảng ở một biểu thức, a[mid] >= x. Thay nó bằng một điều kiện P(x)P(x) bất kỳ đơn điệu, tức nếu P(x)P(x) đúng thì P(y)P(y) đúng với mọi y>xy > x, thì cùng vòng lặp ấy tìm ra giá trị nhỏ nhất mà PP đúng. Dãy giá trị của PP có dạng sai, …, sai, đúng, …, đúng, và ta đi tìm ranh giới. Trong code, PP là một hàm check.

C++
ll l = lo, r = hi, res = -1;
while (l <= r) {
    ll mid = (l + r) >> 1;
    if (check(mid)) r = (res = mid) - 1;
    else l = mid + 1;
}

Nhiều bài hỏi giá trị lớn nhất thỏa điều kiện; khi đó điều kiện đúng ở bên trái và sai ở bên phải. Chỉ cần đổi hướng: thỏa thì ghi nhận rồi tìm tiếp bên phải.

C++
if (check(mid)) l = (res = mid) + 1;
else r = mid - 1;

Chứng minh y hệt mục trước. res = -1 (hay một giá trị nằm ngoài miền) cho biết không có giá trị nào thỏa, ta kiểm bằng một phép so sánh, không cần nhánh xử lý riêng.

Ví dụ gọn nhất là tính ⌊n⌋\lfloor \sqrt n \rfloor với n≤1018n \le 10^{18} mà không dùng số thực. Điều kiện x2≤nx^2 \le n đúng với xx nhỏ, sai với xx lớn; đáp án là giá trị lớn nhất còn đúng. Miền tìm là [0,109][0, 10^9], và mid2≤1018mid^2 \le 10^{18} vẫn nằm trong long long.

C++
ll l = 0, r = 1e9, res = 0;
while (l <= r) {
    ll mid = (l + r) >> 1;
    if (mid * mid <= n) l = (res = mid) + 1;
    else r = mid - 1;
}

Dạng “phần tử thứ kk” cũng đi qua khuôn này. Nếu đếm nhanh được cnt(x)\mathrm{cnt}(x) là số phần tử không vượt xx, thì phần tử thứ kk là giá trị nhỏ nhất có cnt(x)≥k\mathrm{cnt}(x) \ge k, kể cả khi ta không liệt kê nổi các phần tử. cnt\mathrm{cnt} không giảm theo xx nên điều kiện đơn điệu.

Ví dụPhần Tử Thứ K Ngoài Các Bội

Đề. Xét dãy tăng các số nguyên dương không chia hết cho nn (n≤109n \le 10^9). Tìm phần tử thứ kk (k≤109k \le 10^9). t≤1000t \le 1000 test.

Lời giải. Trong 1..x1..x có ⌊x/n⌋\lfloor x/n \rfloor bội của nn, nên cnt(x)=x−⌊x/n⌋\mathrm{cnt}(x) = x - \lfloor x/n \rfloor số không chia hết. Đáp án là xx nhỏ nhất có cnt(x)≥k\mathrm{cnt}(x) \ge k. Vì n≥2n \ge 2, cứ hai số liên tiếp có ít nhất một số không chia hết cho nn, nên cnt(2k)≥k\mathrm{cnt}(2k) \ge k và miền tìm [1,2k][1, 2k] chắc chắn chứa đáp án. Mỗi test khoảng 31 vòng.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll t, n, k;

signed main() {
    cin >> t;
    while (t--) {
        cin >> n >> k;
        ll l = 1, r = 2 * k, res = 2 * k;
        while (l <= r) {
            ll mid = (l + r) >> 1;
            if (mid - mid / n >= k) r = (res = mid) - 1;
            else l = mid + 1;
        }
        cout << res << '\n';
    }
}
4
4 8
2 6
5 1
100 105

lower_bound và upper_bound

Hai hàm của STL là chặt nhị phân trên mảng đã sắp, đóng gói sẵn. Với dãy tăng và số xx:

  • lower_bound trả vị trí đầu tiên có ai≥xa_i \ge x, đúng kết quả của mục đầu;
  • upper_bound trả vị trí đầu tiên có ai>xa_i > x.

Cả hai trả end khi không có vị trí nào. Đọc theo nghĩa đếm dễ nhớ hơn: với mảng a1..ana_1..a_n, lower_bound(a + 1, a + n + 1, x) - a - 1 bằng số phần tử nhỏ hơn xx, còn upper_bound trừ tương tự bằng số phần tử không lớn hơn xx. Hiệu hai hàm là số phần tử bằng xx.

Kéo x: hai hàm là hai chỗ dãy 0/1 đổi sang 1; các số bằng x nằm giữa hai chỗ đó.

Gọi lb là vị trí lower_bound (đánh số từ 1), các câu hỏi thường gặp đọc thẳng ra: xx có mặt khi lb <= n && a[lb] == x; phần tử nhỏ nhất không nhỏ hơn xx là a[lb]; phần tử gần xx nhất là một trong hai a[lb - 1] và a[lb], vì mọi phần tử trước a[lb - 1] và sau a[lb] đều xa xx hơn.

Ba điều cần nhớ khi dùng. Dãy phải được sắp đúng thứ tự mà hàm dùng; sắp giảm dần rồi gọi lower_bound mặc định thì kết quả sai mà không báo lỗi. Với set và map, gọi hàm thành viên s.lower_bound(x), tốn O(log⁡n)O(\log n); hàm tự do lower_bound(s.begin(), s.end(), x) vẫn đúng nhưng đi từng nút và tốn O(n)O(n). Và khi cần điều kiện khác “nhỏ hơn xx”, viết khuôn ở mục trước còn nhanh hơn đi tìm hàm thư viện.

Hỏi nhanh

Dãy đã sắp là a = {2, 4, 4, 4, 9} (lưu từ a[0]). Có bao nhiêu phần tử không vượt quá 4, và biểu thức nào cho đúng số đó?

Ví dụChi Phí Đưa Về Cùng Mốc

Đề. Dãy nn số, qq truy vấn xx (độc lập), mỗi truy vấn hỏi tổng ∣a1−x∣+∣a2−x∣+⋯+∣an−x∣|a_1 - x| + |a_2 - x| + \dots + |a_n - x|. n,q≤2⋅105n, q \le 2 \cdot 10^5, 0≤ai,x≤1090 \le a_i, x \le 10^9.

Lời giải. Dấu giá trị tuyệt đối tách theo một điều kiện: phần tử nhỏ hơn xx góp x−aix - a_i, phần tử không nhỏ hơn xx góp ai−xa_i - x. Nếu aa đã sắp thì nhóm “nhỏ hơn xx” là kk phần tử đầu, với kk tìm bằng lower_bound, và tổng mỗi nhóm lấy bằng tổng tiền tố (bài 02):

(k⋅x−prek)+((pren−prek)−(n−k)⋅x).\bigl(k \cdot x - \mathrm{pre}_k\bigr) + \bigl((\mathrm{pre}_n - \mathrm{pre}_k) - (n - k) \cdot x\bigr).

Mỗi truy vấn O(log⁡n)O(\log n); duyệt cả dãy cho từng truy vấn tốn nq=4⋅1010nq = 4 \cdot 10^{10}. Tích k⋅xk \cdot x tới 2⋅10142 \cdot 10^{14}, dùng long long.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll N = 2e5 + 5;
ll n, q, a[N], pre[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> q;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    for (ll i = 1; i <= n; i++) pre[i] = pre[i - 1] + a[i];
    while (q--) {
        ll x;
        cin >> x;
        ll k = lower_bound(a + 1, a + n + 1, x) - a - 1;
        cout << k * x - pre[k] + (pre[n] - pre[k]) - (n - k) * x << '\n';
    }
}
3 3
1 4 8
0
5
10

Chặt nhị phân trên đáp án

Nhiều bài không có mảng nào để tìm, nhưng đáp án là một số nguyên trong miền rất lớn, và có một câu hỏi rẻ hơn câu hỏi gốc: “giá trị xx đã đủ chưa?”. Nếu câu hỏi đó đơn điệu theo xx thì đáp án là ranh giới của nó.

Bài toán thử: nn sợi dây dài ℓi\ell_i, cắt mỗi sợi thành các đoạn nguyên dài đúng xx, phần thừa bỏ đi, cần ít nhất kk đoạn; tìm xx lớn nhất. Thử từng xx tốn 109⋅n10^9 \cdot n phép. Nhưng nếu độ dài xx cắt đủ kk đoạn thì mọi x′<xx' < x cũng đủ, vì ⌊ℓi/x′⌋≥⌊ℓi/x⌋\lfloor \ell_i / x' \rfloor \ge \lfloor \ell_i / x \rfloor: điều kiện đúng ở bên trái, sai ở bên phải, đúng dạng “giá trị lớn nhất”.

Cột là số đoạn cắt được với độ dài x; số trên cột là thứ tự lần thử.

Mọi bài kiểu này đi qua bốn câu hỏi, trả lời chúng trước khi gõ code:

  1. Đáp án là gì, nằm trong miền nào? Câu trả lời cho hai đầu l, r và giá trị ban đầu của res (trường hợp vô nghiệm).
  2. Hàm check(x) là gì, tốn bao lâu? Thường là một lượt duyệt O(n)O(n), đôi khi kèm tham lam.
  3. Vì sao check đơn điệu? Một câu dạng “nếu xx làm được thì x−1x - 1 cũng làm được, vì…”.
  4. Hàm kiểm tra có tràn số không? Tổng chi phí là ⌈log⁡2L⌉⋅O(check)\lceil \log_2 L \rceil \cdot O(\text{check}); đặt miền rộng gần như miễn phí vì độ dài miền chỉ nằm trong logarit, nhưng giá trị xx lớn có thể làm tổng trong check vượt long long. Cách chữa quen thuộc là dừng ngay khi tổng đã đủ mức cần.

Vài dạng lặp lại để nhận ra khi đọc đề. Tối thiểu hóa giá trị lớn nhất: “chia dãy thành kk đoạn sao cho tổng đoạn lớn nhất nhỏ nhất”; hỏi ngược có làm được với mọi phần không vượt MM không, điều kiện đúng từ một MM nào đó trở lên. Tối đa hóa giá trị nhỏ nhất: “đặt cc trạm sao cho khoảng cách nhỏ nhất lớn nhất”; hỏi có làm được với mọi khoảng cách không nhỏ hơn DD không. Hàm kiểm tra của cả hai thường là một lượt tham lam (bài 04).

Hỏi nhanh

Một bạn định tìm số nguyên dương xx nhỏ nhất có tổng chữ số không nhỏ hơn 10, bằng chặt nhị phân trên đoạn từ 1 đến 100 với điều kiện “tổng chữ số của xx không nhỏ hơn 10”. Có làm được không?

Ví dụChia Dãy Cân Bằng

Đề. Dãy n≤2⋅105n \le 2 \cdot 10^5 số nguyên dương ai≤109a_i \le 10^9, chia thành đúng kk đoạn liên tiếp không rỗng; giá của cách chia là tổng lớn nhất trong các đoạn. Tìm giá nhỏ nhất.

Lời giải. Dạng tối thiểu hóa giá trị lớn nhất. Gọi P(M)P(M) là “chia được thành kk đoạn mà đoạn nào cũng có tổng không vượt MM”; P(M)P(M) đúng thì P(M+1)P(M + 1) đúng, nên tìm MM nhỏ nhất thỏa.

Kiểm P(M)P(M) bằng tham lam: quét từ trái, cộng phần tử vào đoạn hiện tại, khi cộng thêm sẽ vượt MM thì mở đoạn mới. Tham lam dùng ít đoạn nhất có thể, vì sau jj đoạn nó luôn phủ được ít nhất bằng jj đoạn đầu của bất kỳ cách chia hợp lệ nào (các số dương nên đoạn nào kéo dài thêm cũng chỉ tốn hơn). Nếu nó dùng ít hơn kk đoạn thì tách bớt đoạn dài là đủ kk đoạn, tổng không tăng. Vậy P(M)P(M) đúng khi và chỉ khi tham lam dùng không quá kk đoạn, với điều kiện MM không nhỏ hơn phần tử lớn nhất.

Miền tìm là từ phần tử lớn nhất tới tổng mọi phần tử, tối đa 48 lần kiểm, mỗi lần O(n)O(n): cỡ 10710^7 phép tính. Tổng tới 2⋅10142 \cdot 10^{14} nên dùng long long.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll N = 2e5 + 5;
ll n, k, a[N];

bool check(ll M) {
    ll parts = 1, cur = 0;
    for (ll i = 1; i <= n; i++) {
        if (cur + a[i] > M) {
            parts++;
            cur = a[i];
        } else cur += a[i];
    }
    return parts <= k;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> k;
    ll mx = 0, sum = 0;
    for (ll i = 1; i <= n; i++) {
        cin >> a[i];
        mx = max(mx, a[i]);
        sum += a[i];
    }
    ll l = mx, r = sum, res = sum;
    while (l <= r) {
        ll mid = (l + r) >> 1;
        if (check(mid)) r = (res = mid) - 1;
        else l = mid + 1;
    }
    cout << res << '\n';
}
7 3
4 2 7 3 5 1 6

Trên ví dụ: thử M=17M = 17 được hai đoạn, đúng; M=11M = 11 cần bốn đoạn (4 2)(7 3)(5 1)(6)(4\,2)(7\,3)(5\,1)(6), sai; M=14M = 14 được ba đoạn, đúng; M=12M = 12 được (4 2)(7 3)(5 1 6)(4\,2)(7\,3)(5\,1\,6), đúng. Đáp án 12.

Chặt nhị phân trên số thực

Khi đáp án là số thực, không còn “hai số nguyên liền nhau” để làm điều kiện dừng. Vòng lặp vẫn là chia đôi miền, nhưng ta chạy một số vòng cố định:

C++
double l = 0, r = 1e9;
for (ll it = 1; it <= 100; it++) {
    double mid = (l + r) / 2;
    if (check(mid)) l = mid;
    else r = mid;
}

Sau tt vòng, độ dài miền còn (r0−l0)/2t(r_0 - l_0) / 2^t. Với miền cỡ 10910^9 và t=100t = 100, đó là khoảng 8⋅10−228 \cdot 10^{-22}, nhỏ hơn mọi chênh lệch mà double phân biệt được; các vòng thừa vô hại vì khi hai đầu đã sát nhau, mid trùng một đầu và phép gán không đổi gì. Nếu check nặng thì 50 vòng đã đủ cho sai số 10−610^{-6} trên miền 10910^9.

Lý do ta không viết while (r - l > 1e-9): double chỉ giữ được khoảng 16 chữ số có nghĩa, nên ở cỡ 10810^8 hai số liền nhau cách nhau khoảng 1,5⋅10−81{,}5 \cdot 10^{-8}. Tới lúc l và r liền nhau, r - l kẹt ở khoảng cách đó và vòng lặp không dừng.

Hỏi nhanh

Một bạn chặt nhị phân trên số thực với miền [0,109][0, 10^9], đáp án cỡ 10810^8, và viết while (r - l > 1e-9). Điều gì có thể xảy ra?

Chặt nhị phân chỉ tìm ranh giới của điều kiện đơn điệu. Hàm có một đỉnh (tăng rồi giảm) thì cần chặt tam phân, bài 57.

Ví dụChia Bánh Tròn

Đề. N≤104N \le 10^4 bánh tròn bán kính ri≤104r_i \le 10^4, chia cho F+1F + 1 người, mỗi người một miếng cùng diện tích SS, mỗi miếng nằm trọn trong một bánh, phần thừa bỏ đi. Tìm SS lớn nhất, sai số 10−310^{-3}.

Lời giải. Bánh diện tích Ai=πri2A_i = \pi r_i^2 cho ⌊Ai/S⌋\lfloor A_i / S \rfloor miếng; tổng số miếng không tăng khi SS tăng, nên điều kiện “đủ F+1F + 1 miếng” đúng với SS nhỏ, sai với SS lớn. Miền là từ 0 tới diện tích bánh lớn nhất. Mỗi lần kiểm O(N)O(N), dừng sớm khi đã đủ người; 100 vòng mỗi test. Dùng acos(-1) cho π\pi.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll t, n, f;
double area[10005];

bool check(double s) {
    ll cnt = 0;
    for (ll i = 1; i <= n; i++) {
        cnt += (ll)(area[i] / s);
        if (cnt >= f + 1) return true;
    }
    return false;
}

signed main() {
    cin >> t;
    double pi = acos(-1);
    while (t--) {
        cin >> n >> f;
        double l = 0, r = 0;
        for (ll i = 1; i <= n; i++) {
            ll x;
            cin >> x;
            area[i] = pi * x * x;
            r = max(r, area[i]);
        }
        for (ll it = 1; it <= 100; it++) {
            double mid = (l + r) / 2;
            if (check(mid)) l = mid;
            else r = mid;
        }
        printf("%.4f\n", l);
    }
}
2
3 3
4 4 4
2 1
1 2

Khi đi thi

  • Thấy “lớn nhất có thể mà vẫn…”, “nhỏ nhất để…”, “tối thiểu hóa giá trị lớn nhất” và có cách kiểm một giá trị cụ thể trong O(n)O(n) hay O(nlog⁡n)O(n \log n), hãy nghĩ ngay chặt nhị phân trên đáp án; trước khi viết, nói được vì sao hàm kiểm tra đơn điệu.
  • Cài bằng khuôn l, r, res với long long cho cả hai đầu lẫn tổng trong hàm kiểm tra, rồi thử ba test nhỏ: miền chỉ có một giá trị, đáp án sát một đầu của miền, và trường hợp vô nghiệm.
  • Dãy tĩnh với nhiều câu hỏi “có bao nhiêu phần tử ≤x\le x”, “phần tử gần xx nhất” thì sắp một lần rồi dùng lower_bound, upper_bound (hàm thành viên với set); đáp án thực thì lặp cố định 100 lần chứ không dừng theo eps.

Tóm tắt

  • Khuôn while (l <= r): thỏa thì r = (res = mid) - 1 (tìm nhỏ nhất) hoặc l = (res = mid) + 1 (tìm lớn nhất), không thỏa thì bỏ mid về phía kia; mất khoảng log⁡2\log_2 (độ dài miền) vòng.
  • Khuôn đúng vì đoạn [l,r][l, r] luôn là các giá trị chưa biết và res là giá trị tốt nhất đã biết; chỉ cần điều kiện đơn điệu.
  • lower_bound cho số phần tử nhỏ hơn xx, upper_bound cho số phần tử không lớn hơn xx; với set dùng hàm thành viên.
  • Chặt nhị phân trên đáp án: viết check(x), chứng minh đơn điệu, chọn miền; chi phí là log⁡2\log_2(miền) nhân giá một lần kiểm tra, và hàm kiểm tra phải tránh tràn số.
  • Số thực: lặp số vòng cố định (50 đến 100) thay vì dừng theo eps.

Bài tập

1
Ngân Sách Theo Ngàycanuc80k0500Sắp các mức giá; số mức không vượt bb chính là vị trí upper_bound của bb.
LQDOJ
2
Nhãn Trong Các Khốicanuc80k0501Nhãn cuối của mỗi khối là một tổng tiền tố tăng dần; khối cần tìm là vị trí đầu tiên có tổng tiền tố không nhỏ hơn nhãn.
LQDOJ
3
Tổng Gần Không Nhấtcanuc80k0503Sắp một dãy; với mỗi bib_i, đáp án nằm ở một trong hai phần tử kề ranh giới quanh mốc −bi-b_i.
LQDOJ
4
Tầm Với Trên Dãy Bậccanuc80k0510Các bậc vượt được tạo thành một tiền tố; dựng dãy giá trị lớn nhất tiền tố và tổng tiền tố rồi tìm nhị phân theo kk.
LQDOJ
5
Độ Cao Lưỡi Cắtcanuc80k0519Lượng gỗ không tăng khi nâng lưỡi cắt; tìm độ cao lớn nhất còn đủ, và tổng gỗ có thể vượt int.
LQDOJ
6
Thời Điểm Đủ Sản Phẩmcanuc80k0507Số sản phẩm sau thời gian xx không giảm theo xx; cận trên rất lớn nên khi cộng dồn phải dừng sớm lúc đã đủ tt.
LQDOJ
7
Cặp Tổng Trong Khoảngcanuc80k0521Sắp dãy; với mỗi aia_i, đếm các aja_j nằm trong đoạn [l−ai,r−ai][l - a_i, r - a_i] bằng hai lần tìm nhị phân, chỉ xét j>ij > i.
LQDOJ
8
Số Dương Bị Khuyết Thứ Kcanuc80k0544Với mỗi AiA_i, đếm số số nguyên dương nhỏ hơn nó mà không thuộc AA; dãy đếm này không giảm, dùng nó để biết đáp án nằm sau bao nhiêu phần tử của AA.
LQDOJ
9
Khoảng Cách Đặt Trạmcanuc80k0520Tối đa hóa khoảng cách nhỏ nhất: chặt nhị phân khoảng cách, kiểm tra bằng cách đặt trạm sớm nhất có thể rồi đếm.
LQDOJ
10
Nâng Trung Vịcanuc80k0525Sau khi sắp, nửa đầu của dãy không ảnh hưởng tới trung vị; chi phí để trung vị đạt xx tăng theo xx, nên chặt nhị phân xx.
LQDOJ
11
Phần Tử Thứ K Trong Bảng Tíchcanuc80k0537Không dựng bảng: đếm xem hàng ii có bao nhiêu ô không vượt xx, cộng các hàng, rồi tìm xx đầu tiên đếm đủ kk.
LQDOJ
12
Xóa Ký Tự Có Thứ Tựcanuc80k0530Chặt nhị phân số lần xóa; kiểm tra pp còn là dãy con bằng một lượt quét bỏ qua các vị trí đã bị xóa.
LQDOJ
13
Bán Kính Phủ Sóngcanuc80k0523Với mỗi điểm nhu cầu, điểm phát gần nhất tìm bằng lower_bound; đáp án là khoảng cách lớn nhất trong các khoảng cách đó.
LQDOJ
14
Lịch Hạ Bóngcanuc80k0543Chặt nhị phân độ cao tối đa XX; mỗi quả bóng có hạn chót bắn, và kiểm tra xếp lịch bằng cách sắp hạn chót tăng dần.
LQDOJ

Đọc thêm

  • Tìm kiếm nhị phânVNOI WikiĐiều kiện đơn điệu, các cách viết và ví dụ chặt nhị phân trên đáp án bằng tiếng Việt.
  • Binary Searchcp-algorithmsBất biến của vòng lặp, tìm kiếm trên hàm đơn điệu, nhảy theo lũy thừa của 2 và chặt nhị phân song song.
  • 二分OI WikiChặt nhị phân trên đáp án, lưu ý về mid âm, chặt nhị phân thực và chặt tam phân.
  • Binary SearchUSACO GuideKhuôn first true và last true, ví dụ trung vị lớn nhất, và danh sách bài luyện.