Nhiều người đứng rải trên một con đường, cần chọn một điểm hẹn sao cho người đến muộn nhất đến càng sớm càng tốt. Hẹn quá về bên trái thì người ở xa bên phải đến muộn; quá về bên phải thì ngược lại. Thời gian chờ giảm dần rồi tăng dần, và đáy nằm ở giữa.
Hàm kiểu đó gọi là đơn đỉnh. Tìm kiếm tam phân so giá trị tại hai điểm chia ba, rồi bỏ đi một phần ba chắc chắn không chứa đáy. Trên số thực, chạy một số vòng cố định; trên số nguyên, gọn và an toàn hơn là chặt nhị phân trên hiệu . 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
Hàm trên đoạn là đơn đỉnh (theo cực tiểu) nếu có để giảm ngặt trên và tăng ngặt trên . Lấy trong đoạn: nếu thì đáy không thể nằm bên phải , bỏ ; ngược lại bỏ . Mỗi vòng đoạn còn .
Hàm lồi là đơn đỉnh. Nếu có đoạn nằm ngang ở đáy thì cũng không sao, vì mọi điểm ở đó đều là cực tiểu. Các phép giữ tính lồi hay gặp:
- , , là lồi; tổng các hàm lồi là lồi.
- 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ư khoảng cách Euclid.
Hàm có đoạn ngang không ở đáy (ví dụ hàm bậc thang) làm tam phân hỏng: không cho biết đáy ở phía nào.
Trong hình, 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?
của các hàm lồi thường có nhiều đáy, mỗi là một đáy. Ba hàm còn lại là tổng hoặc cực đại của các hàm lồi nên lồi.
Trên số thực, đừng dừng theo sai số : với tọa độ lớn, nhỏ có thể không bao giờ đạt được do làm tròn, vòng lặp chạy mãi. Hãy chạy số vòng cố định: sau vòng đoạn dài , 100 vòng đã nhỏ hơn lần độ dài ban đầu.
Ví dụĐiểm Hẹn Nhanh Nhất
Đề. người trên trục số, người ở đi với vận tốc tối đa (tới ). Chọn điểm hẹn thực để thời gian người tới cuối cùng, , nhỏ nhất.
Lời giải. Mỗi lồi theo , nên cực đại của chúng cũng lồi: tam phân trên .
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 ở , .
ld f(ld p) {
ld 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à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ỏ ; ngược lại bỏ . Mỗi vòng giữ lại khoảng; 200 vòng thu từ về cỡ , dư cho độ chính xác của long double.
for (ll it = 0; it < 200; it++) {
ld m1 = l + (r - l) / 3, m2 = r - (r - l) / 3;
if (f(m1) < f(m2)) r = m2;
else l = m1;
}
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
const ll N = 6e4 + 5;
ll n, x[N], v[N];
ld f(ld p) {
ld 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];
ld l = 1, r = 1e9;
for (ll it = 0; it < 200; it++) {
ld 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
10.000000000000000
Ước lượng phép tính: phép. Hai chỗ có thể bớt: kiểu số và số vòng.
long double chậm hơn double khoảng 3 lần, và 100 vòng cho cùng đáp án như 200 vòng nhưng nhanh gấp đôi. Quy tắc: double với 100 vòng là đủ cho đa số bài; long double chỉ khi đề đòi nhiều chữ số hay tọa độ rất lớn.
Hàm tính bằng thuật toán
Nhiều khi 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ố . Tam phân vẫn dùng được miễn chứng minh được lồi, thường bằng cách viết 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 , nên phải chạy nhanh.
Ví dụĐộ Lệch Đoạn Liên Tiếp
Đề. Dãy số trong . Chọn số thực , trừ 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, lồi theo ; cực đại trên mọi đoạn vẫn lồi. Tam phân trên như ví dụ trước, chỉ khác cách tính .
Bước 1: bằng Kadane hai chiều. Trên dãy : hi là tổng lớn nhất của đoạn kết thúc tại , 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.
ld f(ld x) {
ld hi = 0, lo = 0, best = 0;
for (ll i = 1; i <= n; i++) {
ld b = a[i] - x;
hi = max(hi, (ld)0) + b;
lo = min(lo, (ld)0) + b;
best = max(best, max(hi, -lo));
}
return best;
}
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
const ll N = 2e5 + 5;
ll n, a[N];
ld f(ld x) {
ld hi = 0, lo = 0, best = 0;
for (ll i = 1; i <= n; i++) {
ld b = a[i] - x;
hi = max(hi, (ld)0) + b;
lo = min(lo, (ld)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];
ld l = -1e4, r = 1e4;
for (ll it = 0; it < 150; it++) {
ld 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
1.000000000000000
Độ phức tạp: 150 vòng, mỗi vòng hai lần .
Miền nguyên
Trên số nguyên, lồi khi và chỉ khi hiệu không giảm. Cực tiểu là nhỏ nhất có , tìm bằng chặt nhị phân l, r, res quen thuộc, mỗi bước tính hai lần. Cách này khỏi phải xử lý “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ư ), hiệu kề có thể sai dấu ở gần đáy, nơi hai giá trị gần như bằng nhau. Khi đó tại điểm tìm được vẫn đủ gần cực tiểu theo sai số đề cho.
Hỏi nhanh
trên số nguyên . Đáp án là nào?
, , . Hiệu còn , nên là nhỏ nhất có hiệu không âm.
Ví dụNgưỡng Điều Chỉnh Chung
Đề. Hai dãy (dài ) và (dài ), , phần tử tới . Mỗi thao tác tăng hay giảm một phần tử 1 đơn vị. Cần đạt với ít thao tác nhất.
Lời giải. Chọn ngưỡng : kéo mọi lên , đẩy mọi xuống . Chi phí là tổng các hàm lồi. Với hàm lồi trên số nguyên, hiệu kề 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. ; chi phí tới , dùng long long.
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ề. nhỏ nhất có f(t + 1) - f(t) >= 0: từ đó hàm không còn giảm, nên là cực tiểu.
while (l <= r) {
ll mid = (l + r) / 2;
if (f(mid + 1) - f(mid) >= 0) res = mid, r = mid - 1;
else l = mid + 1;
}
#include <bits/stdc++.h>
using namespace std;
#define 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
8
Độ phức tạp: bước chặt, mỗi bước .
Ví dụMột Lần Điều Chỉnh
Đề. Chọn số nguyên để nhỏ nhất, với . In giá trị nhỏ nhất, sai số .
Lời giải. tuyến tính, lồi, tổng lồi theo . Đáy gần , không vượt , nên chặt hiệu kề như ví dụ trước trên , khoảng 41 bước.
Bước 1: tính bằng số thực. tới không vừa long long, và là số thực, nên dùng long double.
ld f(ll k) {
return (ld)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 .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
ll A, B;
ld f(ll k) {
return (ld)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
12.9442719100
Độ phức tạp: .
Tam phân lồng
Hàm hai biến lồi theo cả hai biến cùng lúc thì cũng lồi. Tam phân ngoài trên , mỗi lần tính bằng một tam phân trong trên . Chi phí là tích số vòng hai tầng nhân chi phí một lần tính , nên giữ số vòng vừa đủ.
Ví dụĐường Tròn Bao Nhỏ Nhất
Đề. điểm nguyên trong . 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 , 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 , vòng trong theo .
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.
ld f(ld x, ld y) {
ld 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 cố định, .
ld g(ld x) {
ld l = 0, r = 1000;
for (ll it = 0; it < 120; it++) {
ld 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 dùng g. lồi vì lồi theo cả hai biến cùng lúc. Cuối cùng lấy căn để ra bán kính.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
ll n, px[55], py[55];
ld f(ld x, ld y) {
ld 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;
}
ld g(ld x) {
ld l = 0, r = 1000;
for (ll it = 0; it < 120; it++) {
ld 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];
ld l = 0, r = 1000;
for (ll it = 0; it < 120; it++) {
ld 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
1.414213562373095
Ước lượng phép tính: phép.
Khi đi thi
- Chứng minh hàm lồi trước khi tam phân: tổng, cực đại, tuyến tính cộng lồi. Hàm bậc thang hay của nhiều hàm thì không dùng được.
- Số thực: 100 đến 200 vòng cố định, không dừng theo ;
doublethường đủ. - Số nguyên: chặt nhị phân nhỏ nhất có ; đáy có công thức gần đúng thì thử vài giá trị quanh đó.
Tóm tắt
- Tam phân giữ đoạn chứa đáy bằng cách so , ; mỗi vòng còn .
- 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 .
Bài tập
Đọc thêm
- Tìm kiếm tam phânVNOI Wiki
- Ternary Searchcp-algorithms
- Optimizing Unimodal FunctionsUSACO Guide
- 二分OI Wiki