Chương 0·Bài 05

Binary Search

Trò đoán số từ 1 đến 100, mỗi lần chỉ được nghe “lớn hơn” hay “nhỏ hơn”: người chơi giỏi đoán 50 trước, rồi 25 hoặc 75, mỗi lần loại một nửa, nên bảy lần là chắc chắn ra. Tìm kiếm nhị phân (binary search) làm đúng như vậy trên dãy đã sắp: dãy 2⋅1052 \cdot 10^5 phần tử chỉ cần khoảng 18 lần so sánh, thay vì duyệt cả dãy.

Giá trị lớn hơn của kỹ thuật là nó không cần 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, thì ranh giới giữa hai phần tìm được sau vài chục lần kiểm. Đó 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;
#define 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

Tìm ranh giới

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;
#define 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.

Dãy không đổi giữa các truy vấn thì đừng bỏ vào set: cùng O(log⁡n)O(\log n) mỗi truy vấn, nhưng mảng đã sắp nằm liền trong bộ nhớ, còn set là các nút rải rác.

Một triệu số, một triệu truy vấn lower_bound, trung vị 5 lần chạy, i7-10850H, g++ -O2.set, s.lower_bound1378 ms · gấp 6 lầnmảng đã sắp, lower_bound230 ms
Dựng rồi trả lời một triệu truy vấn trên một triệu số, bằng set và bằng mảng đã sắp, đo trên i7-10850H, g++ -O2.

Mảng đã sắp nhanh hơn khoảng 6 lần, tính cả lúc dựng. set chỉ đáng dùng khi phải chèn, xóa xen giữa các truy vấ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;
#define 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;
#define 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.

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;
#define 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ảng một lần rồi dùng lower_bound, upper_bound, không cần 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

Đọc thêm