1. 引言
三分算法(Ternary Search)是一种用于在单峰函数 或单谷函数上寻找极值点的搜索算法。与二分查找要求序列单调不同,三分算法适用于函数值先增后减(或先减后增)的场景,通过不断缩小区间来逼近极值点。
三分算法在算法竞赛、数值优化、工程调参等领域有着广泛的应用,例如求解凸函数的最小值、寻找最优参数、计算几何中的最值问题等。本文将系统讲解三分算法的原理、实现方式以及典型应用场景。
2. 算法原理
2.1 适用场景
三分算法适用于单峰函数 (先严格递增后严格递减)或单谷函数(先严格递减后严格递增)。所谓单峰,是指函数在定义域内只有一个极值点,且极值点两侧分别单调。
常见的适用场景包括:
- 二次函数求极值
- 距离类函数求最值
- 几何问题中的最优位置搜索
- 参数调优中的损失函数最小化
2.2 核心思想
假设我们要在区间 [l, r] 上寻找单峰函数 f(x) 的最大值。三分算法的核心思想是:
- 取区间的两个三等分点
m1和m2,其中m1 = l + (r - l) / 3,m2 = r - (r - l) / 3。 - 比较
f(m1)与f(m2)的大小:- 若
f(m1) < f(m2),说明峰值在[m1, r]区间内,令l = m1。 - 若
f(m1) > f(m2),说明峰值在[l, m2]区间内,令r = m2。 - 若相等,峰值在
[m1, m2]之间,可令l = m1, r = m2。
- 若
- 重复上述过程,直到区间长度小于预设精度。
每次迭代区间长度缩小为原来的 2/3,因此时间复杂度为 O(log_{3/2} n)。
2.3 与二分查找的区别
| 对比项 | 二分查找 | 三分算法 |
|---|---|---|
| 适用条件 | 单调序列 | 单峰/单谷函数 |
| 每次比较 | 1 个中点 | 2 个三等分点 |
| 区间缩小比例 | 1/2 | 2/3 |
| 目标 | 查找特定值 | 查找极值点 |
3. 代码实现
3.1 整数域三分(求最大值)
cpp
#include <bits/stdc++.h>
using namespace std;
// 单峰函数,求最大值
int f(int x) {
return -(x - 5) * (x - 5) + 10; // 顶点在 x=5,最大值为 10
}
int ternarySearch(int l, int r) {
while (r - l > 2) { // 整数域保留 2 的容差
int m1 = l + (r - l) / 3;
int m2 = r - (r - l) / 3;
if (f(m1) < f(m2)) {
l = m1;
} else {
r = m2;
}
}
// 在 [l, r] 内暴力枚举找最大值
int ans = f(l);
for (int i = l + 1; i <= r; i++) {
ans = max(ans, f(i));
}
return ans;
}
int main() {
cout << ternarySearch(0, 10) << endl; // 输出 10
return 0;
}
3.2 实数域三分(求最小值)
cpp
#include <bits/stdc++.h>
using namespace std;
// 单谷函数,求最小值
double f(double x) {
return (x - 3.0) * (x - 3.0) + 2.0; // 顶点在 x=3,最小值为 2
}
double ternarySearch(double l, double r) {
const double eps = 1e-9; // 精度控制
while (r - l > eps) {
double m1 = l + (r - l) / 3.0;
double m2 = r - (r - l) / 3.0;
if (f(m1) < f(m2)) {
r = m2; // 最小值在 [l, m2]
} else {
l = m1; // 最小值在 [m1, r]
}
}
return f(l);
}
int main() {
printf("%.10f\n", ternarySearch(0.0, 10.0)); // 输出 2.0000000000
return 0;
}
【模板】三分 / 函数 / ICPC 2010 Chengdu R Error Curves
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=100005,inf=INT_MAX;
const double eps=1e-9;
int n;
int a[N],b[N],c[N];
inline double f(double x)
{
double maxn=-inf;
for(int i=1;i<=n;i++){
maxn=max(maxn,a[i]*x*x+b[i]*x+c[i]);
}
return maxn;
}
signed main()
{
int t;
cin>>t;
while(t--)
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i]>>b[i]>>c[i];
}
double l=0,r=1000;
while(r-l>eps)
{
double m1=(2*l+r)/3;
double m2=(l+2*r)/3;
if(f(m1)<f(m2))
{
r=m2;
}
else
{
l=m1;
}
}
printf("%.4lf\n",f(l));
}
return 0;
}
4. 注意事项与优化
4.1 函数必须严格单峰/单谷
如果函数存在多个极值点或平台区间,三分算法可能失效。此时需要先分析函数性质,或改用其他方法(如模拟退火、爬山法)。
4.2 时间复杂度
每次迭代区间缩小 2/3,迭代次数约为 log_{3/2}((r-l)/eps)。对于 1e9 的整数区间,约需 50 次迭代;对于实数域,迭代次数取决于精度要求。
三分套三分模板
cpp
#include<bits/stdc++.h>
using namespace std;
constexpr double eps=1e-9;
double ax,ay,bx,by;
double cx,cy,dx,dy;
double ex,ey,fx,fy;
double p,q,r;
double dis(double x,double y,double xx,double yy)
{
return sqrt((x-xx)*(x-xx)+(y-yy)*(y-yy));
}
double FF(double y);
double F(double x)
{
ex=ax+(bx-ax)*x,ey=ay+(by-ay)*x;
double l=0,r=1;
double lm,rm;
while(r-l>eps)//第二个三分
{
lm=(2*l+r)/3;
rm=(l+2*r)/3;
if(FF(lm)<FF(rm))
{
r=rm;
}
else
{
l=lm;
}
}
return FF((l+r)/2);
}
double FF(double y)
{
fx=cx+(dx-cx)*y,fy=cy+(dy-cy)*y;
return dis(ax,ay,ex,ey)/p+dis(ex,ey,fx,fy)/r+dis(fx,fy,dx,dy)/p;
}
int main()
{
cin>>ax>>ay>>bx>>by;
cin>>cx>>cy>>dx>>dy;
cin>>p>>q>>r;
double l=0,r=1;
double lm,rm;
while(r-l>eps)//第一个三分
{
lm=(2*l+r)/3;
rm=(l+2*r)/3;
if(F(lm)<F(rm))
{
r=rm;
}
else
{
l=lm;
}
}
printf("%.2lf",F((l+r)/2));
return 0;
}
5. 总结
三分算法是一种简洁而强大的极值搜索工具,适用于单峰/单谷函数,通过不断缩小区间逼近极值点。本文系统梳理了以下要点:
- 原理 :每次取两个三等分点
m1、m2,比较函数值后舍弃一侧区间,每次迭代区间缩小为原来的2/3,时间复杂度为O(log_{3/2} n)。 - 实现 :整数域需保留容差并在小范围内暴力枚举;实数域需设置精度
eps(通常1e-7~1e-12)。 - 注意事项:函数必须严格单峰/单谷,否则算法可能失效;需区分整数域与实数域的实现细节。