Chương 5·Bài 57

Ternary Search

Tìm kiếm tam phân tìm cực tiểu của hàm đơn đỉnh: giảm rồi tăng (hoặc tăng rồi giảm khi tìm cực đại). So giá trị tại hai điểm chia ba và bỏ đi một phần ba không thể chứa đáy. Trên số thực, chạy một số vòng cố định; trên số nguyên, cách gọn và an toàn hơn là chặt nhị phân trên hiệu f(k+1)−f(k)f(k + 1) - f(k). Phần khó thật sự là chứng minh hàm đơn đỉnh, thường bằng cách chỉ ra nó lồi.

Hàm lồi và tam phân trên số thực

Hàm ff trên đoạn [l,r][l, r] là đơn đỉnh (theo cực tiểu) nếu có pp để ff giảm ngặt trên [l,p][l, p] và tăng ngặt trên [p,r][p, r]. Lấy m1<m2m_1 < m_2 trong đoạn: nếu f(m1)<f(m2)f(m_1) < f(m_2) thì đáy không thể nằm bên phải m2m_2, bỏ (m2,r](m_2, r]; ngược lại bỏ [l,m1)[l, m_1). Mỗi vòng đoạn còn 2/32/3.

Hàm lồi đơn đỉnh trừ khi có đoạn nằm ngang ở đáy, và đoạn ngang ở đáy không làm sai kết quả (mọi điểm ở đó đều là cực tiểu). Các phép giữ tính lồi hay gặp:

  • ∣x−a∣|x - a|, (x−a)2(x - a)^2, c⋅xc \cdot x là lồi; tổng các hàm lồi là lồi.
  • max⁡\max của các hàm lồi là lồi: thời gian chờ người chậm nhất, khoảng cách tới điểm xa nhất.
  • Hàm lồi theo mỗi biến khi cố định biến kia chưa đủ cho tam phân lồng; cần lồi theo cả hai biến cùng lúc, như max⁡\max khoảng cách Euclid.

Hàm đơn đỉnh nhưng có đoạn ngang không ở đáy (ví dụ hàm bậc thang) làm tam phân hỏng: f(m1)=f(m2)f(m_1) = f(m_2) không cho biết đáy ở phía nào.

Dải xanh là đoạn còn lại; chấm đỏ là điểm thấp hơn trong hai điểm chia ba. Kéo vị trí đáy để thử.

Hỏi nhanh

Hàm nào không chắc đơn đỉnh trên đoạn?

Trên số thực, không dừng theo sai số r−l<εr - l < \varepsilon: với tọa độ lớn, ε\varepsilon nhỏ có thể không bao giờ đạt được do làm tròn, vòng lặp chạy mãi. Chạy số vòng cố định: sau kk vòng đoạn dài (r−l)(2/3)k(r - l)(2/3)^k, 100 vòng đã nhỏ hơn 10−1710^{-17} lần độ dài ban đầu. Khi đề yêu cầu in nhiều chữ số hoặc giá trị lớn, dùng long double.

Ví dụĐiểm Hẹn Nhanh Nhất

Đề. n≤6⋅104n \le 6 \cdot 10^4 người trên trục số, người ii ở xix_i đi với vận tốc tối đa viv_i (tới 10910^9). Chọn điểm hẹn thực pp để thời gian người tới cuối cùng, max⁡i∣xi−p∣/vi\max_i |x_i - p| / v_i, nhỏ nhất.

Lời giải. Mỗi ∣xi−p∣/vi|x_i - p|/v_i lồi theo pp, nên cực đại của chúng cũng lồi: tam phân trên [1,109][1, 10^9].

Bước 1: hàm cần cực tiểu. Thời gian của người tới cuối cùng khi hẹn ở pp, O(n)O(n).

C++
long double f(long double p) {
    long double t = 0;
    for (ll i = 1; i <= n; i++) t = max(t, fabsl(x[i] - p) / v[i]);
    return t;
}

Bước 2: tam phân. Chia [l,r][l, r] làm ba. Nếu f(m1) < f(m2) thì cực tiểu không thể nằm bên phải m2 (hàm lồi đã bắt đầu tăng), bỏ (m2,r](m2, r]; ngược lại bỏ [l,m1)[l, m1). Mỗi vòng giữ lại 2/32/3 khoảng; 200 vòng thu từ 10910^9 về cỡ 10−2610^{-26}, dư cho độ chính xác của long double.

C++
for (ll it = 0; it < 200; it++) {
    long double m1 = l + (r - l) / 3, m2 = r - (r - l) / 3;
    if (f(m1) < f(m2)) r = m2;
    else l = m1;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 6e4 + 5;
ll n, x[N], v[N];

long double f(long double p) {
    long double t = 0;
    for (ll i = 1; i <= n; i++) t = max(t, fabsl(x[i] - p) / v[i]);
    return t;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) cin >> x[i];
    for (ll i = 1; i <= n; i++) cin >> v[i];
    long double l = 1, r = 1e9;
    for (ll it = 0; it < 200; it++) {
        long double m1 = l + (r - l) / 3, m2 = r - (r - l) / 3;
        if (f(m1) < f(m2)) r = m2;
        else l = m1;
    }
    cout << fixed << setprecision(15) << f((l + r) / 2) << '\n';
}
3
1 11 21
1 2 1

200⋅2⋅n=2,4⋅107200 \cdot 2 \cdot n = 2{,}4 \cdot 10^7 phép.

Hàm mục tiêu tính bằng thuật toán khác

Nhiều khi f(x)f(x) không có công thức mà phải tính bằng cả một thuật toán: tham lam, Kadane, DP, đường đi ngắn nhất với tham số xx. Tam phân vẫn dùng được miễn chứng minh được ff lồi, thường bằng cách viết ff là cực đại (hoặc tổng) của rất nhiều hàm lồi đơn giản, mỗi hàm ứng với một lựa chọn rời rạc. Độ phức tạp là số vòng nhân chi phí một lần tính ff, nên ff phải chạy nhanh.

Ví dụĐộ Lệch Đoạn Liên Tiếp

Đề. Dãy n≤2⋅105n \le 2 \cdot 10^5 số trong [−104,104][-10^4, 10^4]. Chọn số thực xx, trừ xx khỏi mọi phần tử; độ lệch là trị tuyệt đối lớn nhất của tổng một đoạn con. Tìm độ lệch nhỏ nhất.

Lời giải. Với mỗi đoạn cố định, ∣tổng−x⋅len∣|\text{tổng} - x \cdot len| lồi theo xx; cực đại trên mọi đoạn vẫn lồi. Tam phân trên [−104,104][-10^4, 10^4] như ví dụ trước, chỉ khác cách tính ff.

Bước 1: f(x)f(x) bằng Kadane hai chiều. Trên dãy ai−xa_i - x: hi là tổng lớn nhất của đoạn kết thúc tại ii, lo là nhỏ nhất. max(hi, 0) + b bỏ phần trước nếu nó âm, như Kadane thường; lo đối xứng. best là trị tuyệt đối lớn nhất của tổng một đoạn con.

C++
long double f(long double x) {
    long double hi = 0, lo = 0, best = 0;
    for (ll i = 1; i <= n; i++) {
        long double b = a[i] - x;
        hi = max(hi, (long double)0) + b;
        lo = min(lo, (long double)0) + b;
        best = max(best, max(hi, -lo));
    }
    return best;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, a[N];

long double f(long double x) {
    long double hi = 0, lo = 0, best = 0;
    for (ll i = 1; i <= n; i++) {
        long double b = a[i] - x;
        hi = max(hi, (long double)0) + b;
        lo = min(lo, (long double)0) + b;
        best = max(best, max(hi, -lo));
    }
    return best;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    long double l = -1e4, r = 1e4;
    for (ll it = 0; it < 150; it++) {
        long double m1 = l + (r - l) / 3, m2 = r - (r - l) / 3;
        if (f(m1) < f(m2)) r = m2;
        else l = m1;
    }
    cout << fixed << setprecision(15) << f((l + r) / 2) << '\n';
}
2
0 2

150 vòng, mỗi vòng hai lần O(n)O(n).

Miền nguyên: chặt nhị phân trên hiệu kề

Trên số nguyên, ff lồi khi và chỉ khi hiệu d(k)=f(k+1)−f(k)d(k) = f(k + 1) - f(k) không giảm. Cực tiểu là kk nhỏ nhất có d(k)≥0d(k) \ge 0, tìm bằng mẫu chặt nhị phân l, r, res quen thuộc, mỗi bước tính ff hai lần. Cách này tránh hẳn các chi tiết “dừng khi còn ba phần tử rồi vét” của tam phân nguyên.

Với hàm có giá trị thực (như A/k+1A/\sqrt{k+1}), hiệu kề tính bằng long double có thể sai dấu ở gần đáy, nơi hai giá trị gần như bằng nhau; khi đó ff tại điểm tìm được vẫn đủ gần cực tiểu theo sai số đề cho.

Hỏi nhanh

f(k)=k⋅1+20/k+1f(k) = k \cdot 1 + 20/\sqrt{k + 1} trên số nguyên k≥0k \ge 0. Đáp án là kk nào?

Ví dụNgưỡng Điều Chỉnh Chung

Đề. Hai dãy aa (dài nn) và bb (dài mm), n,m≤105n, m \le 10^5, phần tử tới 10910^9. Mỗi thao tác tăng hay giảm một phần tử 1 đơn vị. Cần đạt min⁡(a)≥max⁡(b)\min(a) \ge \max(b) với ít thao tác nhất.

Lời giải. Chọn ngưỡng tt: kéo mọi ai<ta_i < t lên tt, đẩy mọi bj>tb_j > t xuống tt. Chi phí f(t)=∑max⁡(0,t−ai)+∑max⁡(0,bj−t)f(t) = \sum \max(0, t - a_i) + \sum \max(0, b_j - t) là tổng các hàm lồi. Với hàm lồi trên số nguyên, hiệu kề f(t+1)−f(t)f(t + 1) - f(t) không giảm, nên chặt nhị phân trên hiệu kề thay cho tam phân.

Bước 1: chi phí một ngưỡng. O(n+m)O(n + m); chi phí tới 2⋅105⋅1092 \cdot 10^5 \cdot 10^9, dùng long long.

C++
ll f(ll t) {
    ll s = 0;
    for (ll i = 1; i <= n; i++)
        if (a[i] < t) s += t - a[i];
    for (ll j = 1; j <= m; j++)
        if (b[j] > t) s += b[j] - t;
    return s;
}

Bước 2: chặt hiệu kề. tt nhỏ nhất có f(t + 1) - f(t) >= 0: từ đó hàm không còn giảm, nên tt là cực tiểu.

C++
while (l <= r) {
    ll mid = (l + r) / 2;
    if (f(mid + 1) - f(mid) >= 0) res = mid, r = mid - 1;
    else l = mid + 1;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 1e5 + 5;
ll n, m, a[N], b[N];

ll f(ll t) {
    ll s = 0;
    for (ll i = 1; i <= n; i++)
        if (a[i] < t) s += t - a[i];
    for (ll j = 1; j <= m; j++)
        if (b[j] > t) s += b[j] - t;
    return s;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    for (ll j = 1; j <= m; j++) cin >> b[j];
    ll l = 1, r = 1e9, res = 1e9;
    while (l <= r) {
        ll mid = (l + r) / 2;
        if (f(mid + 1) - f(mid) >= 0) res = mid, r = mid - 1;
        else l = mid + 1;
    }
    cout << f(res) << '\n';
}
3 2
1 4 9
5 8

3030 bước chặt, mỗi bước O(n+m)O(n + m).

Ví dụMột Lần Điều Chỉnh

Đề. Chọn số nguyên k≥0k \ge 0 để kB+A/k+1kB + A/\sqrt{k + 1} nhỏ nhất, với A,B≤1018A, B \le 10^{18}. In giá trị nhỏ nhất, sai số 10−610^{-6}.

Lời giải. BkBk tuyến tính, A/k+1A/\sqrt{k+1} lồi, tổng lồi theo kk. Đáy gần (A/2B)2/3(A/2B)^{2/3}, không vượt 101210^{12}, nên chặt hiệu kề như ví dụ trước trên [0,2⋅1012][0, 2 \cdot 10^{12}], khoảng 41 bước.

Bước 1: tính bằng số thực. kBkB tới 2⋅10302 \cdot 10^{30} không vừa long long, và A/k+1A/\sqrt{k + 1} là số thực, nên dùng long double.

C++
long double f(ll k) {
    return (long double)k * B + A / sqrtl(k + 1);
}

Gần đáy, hai giá trị kề nhau gần như bằng nhau và hiệu có thể sai dấu do làm tròn; điểm tìm được vẫn nằm sát cực tiểu, sai số giá trị nhỏ hơn nhiều so với 10−610^{-6}.

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

ll A, B;

long double f(ll k) {
    return (long double)k * B + A / sqrtl(k + 1);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> A >> B;
    ll l = 0, r = 2e12, res = 2e12;
    while (l <= r) {
        ll mid = (l + r) / 2;
        if (f(mid + 1) - f(mid) >= 0) res = mid, r = mid - 1;
        else l = mid + 1;
    }
    cout << fixed << setprecision(10) << f(res) << '\n';
}
20 1

O(log⁡1012)O(\log 10^{12}).

Tam phân lồng nhau

Hàm hai biến f(x,y)f(x, y) lồi theo cả hai biến cùng lúc thì g(x)=min⁡yf(x,y)g(x) = \min_y f(x, y) cũng lồi. Tam phân ngoài trên xx, mỗi lần tính g(x)g(x) bằng một tam phân trong trên yy. Chi phí là tích số vòng hai tầng nhân chi phí một lần tính ff, nên giữ số vòng vừa đủ.

Ví dụĐường Tròn Bao Nhỏ Nhất

Đề. n≤50n \le 50 điểm nguyên trong [0,1000]2[0, 1000]^2. Tìm bán kính nhỏ nhất của đường tròn chứa mọi điểm.

Lời giải. Với tâm (x,y)(x, y), bán kính cần là khoảng cách tới điểm xa nhất, một hàm lồi hai biến. Tâm tối ưu nằm trong hình vuông chứa các điểm. Tam phân lồng: vòng ngoài theo xx, vòng trong theo yy.

Bước 1: hàm hai biến. Bình phương khoảng cách tới điểm xa nhất. Bình phương đạt cực tiểu cùng chỗ với khoảng cách và vẫn lồi theo từng biến, nên khỏi gọi sqrt trong vòng lặp.

C++
long double f(long double x, long double y) {
    long double d = 0;
    for (ll i = 1; i <= n; i++) d = max(d, (x - px[i]) * (x - px[i]) + (y - py[i]) * (y - py[i]));
    return d;
}

Bước 2: tam phân trong. Với xx cố định, g(x)=min⁡yf(x,y)g(x) = \min_y f(x, y).

C++
long double g(long double x) {
    long double l = 0, r = 1000;
    for (ll it = 0; it < 120; it++) {
        long double m1 = l + (r - l) / 3, m2 = r - (r - l) / 3;
        if (f(x, m1) < f(x, m2)) r = m2;
        else l = m1;
    }
    return f(x, (l + r) / 2);
}

Bước 3: tam phân ngoài trên xx dùng g. gg lồi vì ff lồi theo cả hai biến cùng lúc. Cuối cùng lấy căn để ra bán kính.

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

ll n, px[55], py[55];

long double f(long double x, long double y) {
    long double d = 0;
    for (ll i = 1; i <= n; i++) d = max(d, (x - px[i]) * (x - px[i]) + (y - py[i]) * (y - py[i]));
    return d;
}

long double g(long double x) {
    long double l = 0, r = 1000;
    for (ll it = 0; it < 120; it++) {
        long double m1 = l + (r - l) / 3, m2 = r - (r - l) / 3;
        if (f(x, m1) < f(x, m2)) r = m2;
        else l = m1;
    }
    return f(x, (l + r) / 2);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) cin >> px[i] >> py[i];
    long double l = 0, r = 1000;
    for (ll it = 0; it < 120; it++) {
        long double m1 = l + (r - l) / 3, m2 = r - (r - l) / 3;
        if (g(m1) < g(m2)) r = m2;
        else l = m1;
    }
    cout << fixed << setprecision(15) << sqrtl(g((l + r) / 2)) << '\n';
}
4
0 0
0 2
2 0
2 2

120⋅2⋅120⋅2⋅n≈2,9⋅106120 \cdot 2 \cdot 120 \cdot 2 \cdot n \approx 2{,}9 \cdot 10^6 phép.

Khi đi thi

  • Trước khi tam phân, chứng minh hàm lồi bằng các quy tắc: tổng, cực đại, hàm tuyến tính cộng hàm lồi. Hàm bậc thang hay min⁡\min của nhiều hàm thì không dùng được.
  • Số thực: chạy 100 đến 200 vòng cố định, không dừng theo ε\varepsilon; dùng long double khi in nhiều chữ số.
  • Số nguyên: chặt nhị phân kk nhỏ nhất có f(k+1)−f(k)≥0f(k + 1) - f(k) \ge 0; nếu đáy có công thức gần đúng, chỉ cần thử vài giá trị quanh đó.

Tóm tắt

  • Tam phân giữ đoạn chứa đáy bằng cách so f(m1)f(m_1), f(m2)f(m_2); mỗi vòng còn 2/32/3.
  • Hàm lồi là đơn đỉnh; tổng và cực đại của hàm lồi vẫn lồi.
  • Trên số nguyên, cực tiểu của hàm lồi là điểm đầu tiên hiệu kề không âm.
  • Hàm lồi hai biến: tam phân lồng, tầng trong tính min⁡yf(x,y)\min_y f(x, y).

Bài tập

1
Bậc Giá Ưu Đãicanuc80k5700Doanh thu theo kk là hàm lõm bậc hai; đáp án tại k=b−ak = b - a kẹp vào [0,min⁡(n,b)][0, \min(n, b)], hoặc chặt nhị phân hiệu kề.
LQDOJ
2
Thời Điểm Khởi Chạycanuc80k5701x+P⋅2−x/1.5x + P \cdot 2^{-x/1.5} lồi theo x≥0x \ge 0; tam phân trên số thực.
LQDOJ
3
Người Tiên Phongcanuc80k5703Sắp sức mạnh; chỉ cần thử phần tử nhỏ nhất không dưới xx và phần tử lớn nhất dưới xx, tìm bằng chặt nhị phân.
LQDOJ
4
Điểm Chia Cân Bằngcanuc80k5704L−RL - R tăng theo ii; chặt nhị phân chỗ đổi dấu và so hai vị trí hai bên.
LQDOJ
5
Tọa Độ Họp Chungcanuc80k5706Thời điểm cuối là max⁡(ci+∣xi−p∣)\max(c_i + |x_i - p|), lồi theo pp; tam phân, hoặc lấy trung điểm của min⁡(xi−ci)\min(x_i - c_i) và max⁡(xi+ci)\max(x_i + c_i).
LQDOJ
6
San Bằng Các Cộtcanuc80k5709Chuyển một đơn vị tốn min⁡(M,A+R)\min(M, A + R); chi phí theo chiều cao chung là đơn đỉnh, tam phân trên chiều cao nguyên.
LQDOJ
7
Dãy Tuyến Tính Gần Nhấtcanuc80k5710Đặt ci=Ai−ic_i = A_i - i, tổng ∣ci−b∣|c_i - b| lồi theo bb và đạt cực tiểu tại trung vị.
LQDOJ
8
Đồng Bộ Ma Trậncanuc80k5711Mọi ô phải cùng dư theo dd, không thì −1-1; số thao tác là tổng ∣a−t∣/d|a - t|/d, nhỏ nhất tại trung vị.
LQDOJ
9
Đoạn Luyện Tập Tối Ưucanuc80k5713Điểm theo tổng ss là hàm lõm, đỉnh gần s=us = u; chặt nhị phân trên tổng tiền tố rồi so hai ứng viên kề.
LQDOJ
10
Chia Đội Cân Bằngcanuc80k5716Điểm theo số đội kk là hàm lõm; mỗi kk chia đều từng loại, tính trong O(n)O(n), tìm đỉnh bằng tam phân nguyên.
LQDOJ
11
Chuỗi Sát Thương Hồi Phụccanuc80k5718Nếu a>bca > bc thì −1-1; ngược lại sát thương tích lũy theo số lần dùng là hàm lõm bậc hai, đỉnh gần a/(bd)a/(bd).
LQDOJ
12
Khoảng Cách Tới Trung Bìnhcanuc80k5720Cố định phần tử lớn nhất là phần tử mới thêm; trung bình của nó với kk số nhỏ nhất là hàm đơn đỉnh theo kk.
LQDOJ
13
Phân Chia Ngân Sách Lũy Thừacanuc80k5721Hàm mục tiêu lõm sau khi lấy log; chia SS tỉ lệ với các số mũ (số mũ 0 thì phần đó bằng 0).
LQDOJ
14
Đường Tròn Tiếp Xúc Trụccanuc80k5724Có điểm ở hai phía trục thì −1-1; bán kính cần theo hoành độ tâm là cực đại của các hàm lồi, tam phân trên số thực.
LQDOJ

Đọc thêm