Ternary Search
Tìm kiếm tam phân (ternary search) tìm điểm cực trị (cực tiểu hoặc cực đại) của một hàm đơn cực (unimodal) trên đoạn trong truy vấn giá trị hàm — thay vì duyệt toàn bộ điểm. Nó là người anh em của tìm kiếm nhị phân: binary search tìm ngưỡng trên hàm đơn điệu, còn ternary search tìm đỉnh/đáy trên hàm "tăng rồi giảm" (hoặc "giảm rồi tăng").
Điều kiện áp dụng (BẮT BUỘC): hàm phải đơn cực trên :
- Để tìm cực tiểu: phải giảm nghiêm ngặt rồi tăng nghiêm ngặt (hình chữ V / hàm lồi). Có đúng một đáy.
- Để tìm cực đại: phải tăng nghiêm ngặt rồi giảm nghiêm ngặt (hình chữ Λ / hàm lõm). Có đúng một đỉnh.
Nếu hàm có đoạn phẳng (hai điểm liền kề bằng nhau) hoặc nhiều cực trị, ternary search có thể trả về sai — đây là cái bẫy lớn nhất, sẽ nói kỹ ở mục Lỗi thường gặp.
Ý tưởng / Trực giác
Lấy ví dụ tìm cực tiểu của hàm lồi. Mỗi bước ta chia đoạn thành ba phần bằng hai điểm:
với . So sánh và cho phép loại bỏ một phần ba đoạn:
- Nếu : đáy không thể nằm bên phải . Vì sao? Hàm lồi giảm rồi tăng. Nếu đáy nằm trong thì cả đều ở nhánh giảm, suy ra — mâu thuẫn. Vậy đáy nằm trong , ta gán .
- Nếu : đối xứng, đáy nằm trong , ta gán .
- Nếu : do tính đơn cực nghiêm ngặt, đáy nằm strictly giữa và , nên thu hẹp về đều an toàn.
Điểm cốt lõi: mỗi lần so sánh cắt bỏ độ dài đoạn một cách an toàn, nên đoạn co lại theo cấp số nhân với hệ số . Sau bước, độ dài còn lần ban đầu — đó chính là nguồn gốc của . Khác với binary search dùng dấu của một điểm, ternary search cần hai điểm vì chỉ một giá trị không cho biết ta đang ở nhánh trái hay nhánh phải của đáy; phải so sánh hai điểm mới biết hướng dốc.
Ví dụ chạy tay
Tìm cực tiểu nguyên của trên (đáy thật ở , ). Bảng giá trị:
x : 0 1 2 3 4 5 6 7 8 9 10 11 12
f(x): 52 39 28 19 12 7 4 3 4 7 12 19 28
^đáy thật
Quy ước: L = lo, R = hi, a = m1, b = m2, vùng bị loại bôi x.
Bước 1: → , . ? Không, nên đáy ở phải: .
x : 0 1 2 3 4 5 6 7 8 9 10 11 12
x x x x L a? R
f(4)=12 > f(8)=4 -> bo trai, lo=4
Bước 2: → khoảng dài 8, , (chia nguyên). → đáy ở trái: .
x : 4 5 6 7 8 9 10 11 12
L a b R x x x
f(6)=4 < f(9)=7 -> bo phai, hi=9
Bước 3: , độ dài → , . ? Không → .
x : 4 5 6 7 8 9
x L a? b R
f(5)=7 > f(8)=4 -> lo=5
Bước 4: , độ dài → , . → . Giờ , tiếp. Bước 5: → , . → . Còn , độ dài , dừng vòng lặp.
Quét nốt : → nhỏ nhất tại , . Đúng.
Cài đặt
Trên số thực (lặp cố định số vòng)
#include <bits/stdc++.h>
using namespace std;
// Tìm x trong [lo, hi] TỐI THIỂU HÓA f (hàm lồi, giảm rồi tăng)
double ternary_min_real(double lo, double hi, function<double(double)> f) {
// Mỗi vòng đoạn co lại theo hệ số 2/3; ~200 vòng -> sai số ~ (2/3)^200
for (int iter = 0; iter < 200; iter++) {
double m1 = lo + (hi - lo) / 3;
double m2 = hi - (hi - lo) / 3;
if (f(m1) < f(m2)) hi = m2; // đáy không ở bên phải m2
else lo = m1; // đáy không ở bên trái m1
}
return (lo + hi) / 2; // trả về điểm giữa đoạn cuối
}
// Tìm CỰC ĐẠI (hàm lõm): chỉ đổi chiều so sánh
double ternary_max_real(double lo, double hi, function<double(double)> f) {
for (int iter = 0; iter < 200; iter++) {
double m1 = lo + (hi - lo) / 3;
double m2 = hi - (hi - lo) / 3;
if (f(m1) > f(m2)) hi = m2;
else lo = m1;
}
return (lo + hi) / 2;
}
Dùng số vòng cố định thay vì điều kiện while (hi - lo > eps) để tránh vòng lặp vô tận do sai số dấu phẩy động. Với vòng, sai số — thừa cho mọi yêu cầu (chỉ cần ~ vòng).
Trên số nguyên (thu hẹp tới khi còn ≤ 3 điểm rồi quét tay)
// Tìm x NGUYÊN trong [lo, hi] tối thiểu hóa f (đơn cực nghiêm ngặt)
long long ternary_min_int(long long lo, long long hi, function<long long(long long)> f) {
while (hi - lo > 2) { // còn > 3 điểm thì tiếp tục
long long m1 = lo + (hi - lo) / 3;
long long m2 = hi - (hi - lo) / 3;
if (f(m1) < f(m2)) hi = m2;
else lo = m1;
}
long long best = lo; // quét nốt các điểm còn lại [lo..hi]
for (long long x = lo; x <= hi; x++)
if (f(x) < f(best)) best = x;
return best; // trả về vị trí cực tiểu
}
Với số nguyên, không lặp tới hi == lo vì khi nhỏ, công thức chia nguyên có thể khiến hoặc trùng biên và vòng lặp không tiến (kẹt vô hạn). An toàn nhất là dừng khi còn điểm rồi so sánh trực tiếp.
Độ phức tạp
- Thời gian: , trong đó là độ dài miền tìm kiếm (số nguyên) hoặc tỉ lệ (số thực), còn là chi phí một lần tính . Mỗi vòng đoạn co theo hệ số nên số vòng để đạt độ rộng (hoặc ) là . Lưu ý: mỗi vòng tính hai lần — hằng số gấp đôi binary search, nhưng vẫn cùng bậc logarit. Nếu tốn (vd cộng khoảng cách tới điểm), tổng là .
- Bộ nhớ: ngoài dữ liệu đầu vào — chỉ giữ vài biến
lo, hi, m1, m2. Bản đệ quy sẽ tốn stack, nên ưu tiên vòng lặp.
⚠️ Lỗi thường gặp
- Hàm không thực sự đơn cực: đây là lỗi nghiêm trọng nhất. Nếu có nhiều cực trị, hoặc tăng/giảm không nghiêm ngặt, kết quả sai mà không báo lỗi. Hãy chứng minh tính đơn cực trước khi dùng (vd tổng các hàm lồi vẫn lồi; khoảng cách Euclid từ điểm cố định tới điểm chạy trên đoạn thẳng là hàm lồi). Khi nghi ngờ, kiểm thử bằng brute force trên dữ liệu nhỏ.
- Đoạn phẳng làm so sánh
<quyết định sai hướng: nếu tồn tại mà do hàm có "mặt bàn" phẳng, nhánhelse(gánlo = m1) có thể vứt bỏ luôn đáy thật. Với số nguyên có đoạn bằng nhau, hãy thu hẹp tới khoảng nhỏ rồi quét tuyến tính thay vì tin tưởng tuyệt đối vào so sánh. - Tràn số khi tính hoặc giá trị : với miền số nguyên lớn (vd ),
lo + (hi - lo)/3an toàn hơn(lo + 2*hi)/3(vế sau dễ trànint). Giá trị hàm như hay tổng khoảng cách dễ vượtint— khai báo trảlong long. - Vòng lặp vô hạn trên số nguyên: điều kiện
while (lo < hi)với chia nguyên có thể khiến , và đoạn không co. Luôn dừng khihi - lo <= 2rồi xử lý nốt bằng tay. - Dùng
while (hi - lo > eps)cho số thực: với hàm gần phẳng quanh đáy, sai số dấu phẩy động khiến điều kiện không bao giờ thoả → treo. Dùng số vòng cố định (100–200) thay cho ngưỡngeps. - Nhầm chiều min/max: tìm cực đại nhưng để nguyên dấu so sánh của bản tìm cực tiểu (hoặc ngược lại) sẽ cho kết quả ở đầu mút sai. Quy tắc: tìm min dùng
if (f(m1) < f(m2)) hi = m2;, tìm max chỉ việc đổi<thành>.
Biến thể / Mở rộng
- Bậc hai (golden-section search): thay vì chia ba đều, dùng tỉ lệ vàng để tái sử dụng một điểm đánh giá ở vòng sau, giảm số lần gọi từ xuống mỗi vòng — hữu ích khi rất đắt.
- Tối ưu trên dãy đơn cực rời rạc: mảng "tăng rồi giảm" (mountain array), tìm đỉnh; hoặc khoảng cách từ điểm tới các đỉnh của bao lồi (convex hull) là đơn cực theo chỉ số đỉnh, cho phép tìm đỉnh xa/gần nhất trong .
- Quy về binary search trên đạo hàm: nếu khả vi và đơn điệu, tìm nghiệm bằng binary search cũng cho cực trị, đôi khi ổn định hơn về số học.
- Ternary search lồng nhau (2D): tối ưu hàm lồi hai biến bằng cách ternary search theo , mỗi lần lại ternary search theo để tính — tổng truy vấn, áp dụng khi hàm lồi theo từng biến.
So sánh nhanh với tìm kiếm nhị phân:
| Binary Search | Ternary Search | |
|---|---|---|
| Điều kiện hàm | Đơn điệu (monotonic) | Đơn cực (unimodal) |
| Mục tiêu | Tìm ngưỡng / giá trị | Tìm điểm cực trị |
| Số lần gọi mỗi vòng | 1 | 2 |
| Hệ số co đoạn | ||
| Độ phức tạp |
Bài tập luyện
Hiện kho bài (pool) của CTOJ chưa có bài nào gắn đúng kỹ thuật tìm kiếm tam phân (không có tag ternary_search; tìm theo từ khoá "tam phân", "unimodal", "cực trị" và rà các bài tag optimization/math/geometry đều không thấy bài thực sự dùng kỹ thuật này). Để tránh gắn link sai chủ đề / link chết, mục này tạm để trống và sẽ được bổ sung khi có bài đúng kỹ thuật được thêm vào pool.
Trong lúc chờ, bạn có thể tự luyện bằng các dạng kinh điển ngoài CTOJ: cực tiểu hàm lồi một biến (vd tổng khoảng cách tới tập điểm), tìm đỉnh "mountain array", và tìm điểm xa nhất tới một đa giác lồi — tất cả đều là ứng dụng trực tiếp của ternary search.