在算法设计中,当面对单调函数 或凸函数(单峰 / 单谷函数)时,三分查找(Ternary Search)是一种比二分查找更灵活的高效搜索算法。它通过不断将搜索区间分为三份,逐步逼近目标值或函数极值点,时间复杂度为 O (log n),在数值优化、最值求解等场景中有着广泛应用。本文将从算法原理、实现细节到应用场景,全面解析三分查找的特性与实战技巧。
一、三分查找的核心原理
1.1 适用场景
三分查找的核心适用场景是具有单调性或凸性的函数:
- 单调函数:在定义域内单调递增或递减(此时可视为凸函数的特例)。
- 单峰函数 :函数值先递增后递减,存在唯一最大值(如抛物线
f(x) = -x² + 2x)。 - 单谷函数 :函数值先递减后递增,存在唯一最小值(如抛物线
f(x) = x²)。
对于这类函数,通过不断将区间三等分并比较中间点的函数值,可快速定位最值点或特定目标值。
1.2 算法流程
以单峰函数求最大值为例,三分查找的步骤如下:
- 确定区间 :定义搜索范围
[left, right](函数定义域)。 - 三等分区间 :计算两个中间点
mid1 = left + (right - left) / 3和mid2 = right - (right - left) / 3。 - 比较函数值 :
- 若
f(mid1) < f(mid2):最大值一定在[mid1, right]内(左侧区间可舍弃)。 - 若
f(mid1) > f(mid2):最大值一定在[left, mid2]内(右侧区间可舍弃)。 - 若
f(mid1) == f(mid2):最大值在[mid1, mid2]内(两端区间可舍弃)。
- 若
- 迭代收缩 :重复步骤 2-3,直至区间长度小于预设精度(如
1e-6),此时区间内任意点可视为极值点。
示意图:在单峰函数中定位最大值
plaintext
初始区间 [left, right]
计算 mid1 和 mid2 → 比较 f(mid1) 与 f(mid2) → 收缩至 [mid1, right]
再次计算新 mid1 和 mid2 → 继续收缩... → 区间足够小时返回结果
二、三分查找的实现(数值型)
2.1 整数域三分(离散场景)
当搜索区间为整数(如数组索引)时,三分查找的终止条件为 left < right,适用于离散单峰 / 单谷数组:
cpp
运行
#include <iostream>
#include <vector>
using namespace std;
// 单峰数组求最大值(数组先增后减)
int findPeak(vector<int>& arr) {
int left = 0;
int right = arr.size() - 1;
while (left < right) {
int mid1 = left + (right - left) / 3;
int mid2 = right - (right - left) / 3;
if (arr[mid1] < arr[mid2]) {
// 最大值在 [mid1+1, right](mid1 不可能是最大值)
left = mid1 + 1;
} else {
// 最大值在 [left, mid2-1](mid2 不可能是最大值)
right = mid2 - 1;
}
}
// 循环结束时 left == right,即为峰值索引
return left;
}
int main() {
vector<int> arr = {1, 3, 5, 7, 6, 4, 2}; // 单峰数组,峰值为7(索引3)
cout << "峰值索引:" << findPeak(arr) << endl; // 输出3
cout << "峰值大小:" << arr[findPeak(arr)] << endl; // 输出7
return 0;
}
关键细节:
- 整数域中
mid1和mid2需避免重叠(通过mid1 + 1和mid2 - 1收缩区间)。 - 适用于严格单峰 / 单谷数组(无平坡段),否则可能漏判。
2.2 实数域三分(连续场景)
当搜索区间为实数(如函数定义域)时,终止条件为区间长度小于预设精度(如 1e-7),适用于连续凸函数:
cpp
运行
#include <iostream>
#include <cmath>
using namespace std;
// 定义单峰函数 f(x) = -x² + 4x(最大值在 x=2 处)
double func(double x) {
return -x * x + 4 * x;
}
// 实数域三分查找最大值
double findMax(double left, double right, double eps = 1e-7) {
while (right - left > eps) {
double mid1 = left + (right - left) / 3;
double mid2 = right - (right - left) / 3;
double f1 = func(mid1);
double f2 = func(mid2);
if (f1 < f2) {
// 最大值在 [mid1, right]
left = mid1;
} else {
// 最大值在 [left, mid2]
right = mid2;
}
}
// 返回区间中点作为最优解
return (left + right) / 2;
}
int main() {
double max_x = findMax(0.0, 5.0); // 函数在 [0,5] 内的最大值点
cout << "最大值点 x = " << max_x << endl; // 约为 2.0
cout << "最大值 f(x) = " << func(max_x) << endl; // 约为 4.0
return 0;
}
关键细节:
- 精度
eps需根据问题要求设置(如工程问题取1e-6,数学问题取1e-10)。 - 最终结果可取区间内任意点(通常取中点),函数值接近真实极值。
三、三分查找的变种与扩展
3.1 单谷函数求最小值
单谷函数(先减后增)的三分查找与单峰函数类似,仅需调整比较逻辑:
cpp
运行
// 单谷函数 f(x) = x² - 2x + 1(最小值在 x=1 处)
double func(double x) {
return x * x - 2 * x + 1;
}
// 查找单谷函数的最小值点
double findMin(double left, double right, double eps = 1e-7) {
while (right - left > eps) {
double mid1 = left + (right - left) / 3;
double mid2 = right - (right - left) / 3;
double f1 = func(mid1);
double f2 = func(mid2);
if (f1 < f2) {
// 最小值在 [left, mid2](右侧区间可舍弃)
right = mid2;
} else {
// 最小值在 [mid1, right](左侧区间可舍弃)
left = mid1;
}
}
return (left + right) / 2;
}
3.2 二维三分查找(单峰函数的扩展)
对于二维单峰函数 (如 f(x,y) = -(x² + y²)),可通过嵌套三分查找求解极值:
- 固定
x,对y进行三分查找,得到当前x对应的最优y及函数值f(x)。 - 对
x进行三分查找,找到使f(x)最大的x值,即为二维极值点。
cpp
运行
// 二维单峰函数:f(x,y) = -(x-2)² - (y+1)²(最大值在 (2,-1))
double func2d(double x, double y) {
return -(x-2)*(x-2) - (y+1)*(y+1);
}
// 固定 x,对 y 进行三分查找,返回最大函数值
double maxOverY(double x, double y_left, double y_right, double eps) {
while (y_right - y_left > eps) {
double y1 = y_left + (y_right - y_left)/3;
double y2 = y_right - (y_right - y_left)/3;
if (func2d(x, y1) < func2d(x, y2)) {
y_left = y1;
} else {
y_right = y2;
}
}
return func2d(x, (y_left + y_right)/2);
}
// 二维三分查找最大值点 x 坐标
double find2dMaxX(double x_left, double x_right, double eps = 1e-7) {
while (x_right - x_left > eps) {
double x1 = x_left + (x_right - x_left)/3;
double x2 = x_right - (x_right - x_left)/3;
double f1 = maxOverY(x1, -5.0, 5.0, eps); // y 范围设为 [-5,5]
double f2 = maxOverY(x2, -5.0, 5.0, eps);
if (f1 < f2) {
x_left = x1;
} else {
x_right = x2;
}
}
return (x_left + x_right)/2;
}
3.3 三分查找与二分查找的结合
在某些场景中(如单调函数中查找特定值),三分查找可与二分查找结合使用,但需注意适用条件:
- 二分查找要求函数严格单调,且可通过比较判断目标在左 / 右区间。
- 三分查找可处理更广泛的凸函数,但逻辑更复杂。
示例:在单调递增函数中查找目标值(二分更高效,但三分也可实现):
cpp
运行
// 单调递增函数 f(x) = x³,查找 x 使 f(x) = 8(即 x=2)
double func(double x) { return x * x * x; }
double findTarget(double target, double left, double right, double eps) {
while (right - left > eps) {
double mid1 = left + (right - left)/3;
double mid2 = right - (right - left)/3;
double f1 = func(mid1);
double f2 = func(mid2);
if (f1 < target) {
left = mid1; // 目标在右侧
} else {
right = mid1; // 目标在左侧
}
}
return (left + right)/2;
}
四、三分查找的应用场景
4.1 函数极值求解
三分查找的典型应用是求解连续单峰 / 单谷函数的极值,例如:
- 求二次函数
f(x) = ax² + bx + c的顶点(a>0时为最小值,a<0时为最大值)。 - 求复杂函数(如
f(x) = sin(x) - x/5)在特定区间内的极值。
cpp
运行
// 求 f(x) = sin(x) 在 [0, π] 内的最大值(理论上 x=π/2 时 f(x)=1)
double func(double x) { return sin(x); }
double findSinMax() {
return findMax(0.0, M_PI); // M_PI 是 π 的宏定义(需包含 <cmath>)
}
4.2 最优化问题
在工程优化、资源分配等问题中,常需找到使目标函数最优的参数:
- 最大利润问题:根据成本与销量的关系(单峰函数),找到最优定价。
- 路径最短问题:在凸形地形中,找到两点间的最短路径(折射定律场景)。
示例 :最优定价问题假设销量 s(p) = 100 - p,成本 c(p) = 10,利润 f(p) = (p - c) * s = (p-10)(100-p),这是一个单峰函数,最大值对应最优定价:
cpp
运行
double profit(double price) {
double cost = 10;
double sales = 100 - price; // 价格越高,销量越低
return (price - cost) * sales;
}
double findBestPrice() {
return findMax(10.0, 100.0); // 价格范围 [成本, 销量为0的价格]
}
4.3 二分查找无法解决的场景
当函数非单调但凸性明确时,三分查找是唯一选择:
- 数组
[1, 2, 3, 5, 4, 3, 2]是单峰数组,二分查找无法定位最大值,三分查找可高效解决。 - 函数
f(x) = x³ - 6x² + 9x + 1先增后减(单峰),三分查找可快速找到最大值点。
五、三分查找的注意事项与优化
5.1 关键前提:函数的凸性
三分查找的正确性依赖于函数的严格凸性(单峰 / 单谷),若函数存在平坡或多峰,可能导致错误:
- 平坡问题 :若
f(mid1) == f(mid2)且区间为平坡,收缩逻辑可能误判极值位置,需特殊处理。 - 多峰问题:函数存在多个极值时,三分查找只能找到局部极值,无法保证全局最优。
解决方案:
- 先通过数学分析或采样验证函数的凸性。
- 对多峰函数,可分段应用三分查找(需已知分段点)。
5.2 精度控制
实数域三分查找的精度设置需平衡效率与准确性:
- 精度过高(如
1e-15)会增加迭代次数,降低效率。 - 精度过低(如
1e-3)可能导致结果误差过大。
建议:
-
根据问题要求设置精度(如输出保留 6 位小数时,精度设为
1e-8)。 -
结合迭代次数控制(如最多迭代 100 次,避免无限循环): cpp
运行
double findMax(double left, double right) { for (int iter = 0; iter < 100; ++iter) { // 固定迭代次数 double mid1 = left + (right - left)/3; double mid2 = right - (right - left)/3; if (func(mid1) < func(mid2)) left = mid1; else right = mid2; } return (left + right)/2; }
5.3 区间初始化
区间 [left, right] 的选择需覆盖函数的极值点,否则会查找失败:
-
对于有明确定义域的函数(如
x ∈ [0, 10]),直接使用定义域。 -
对于无明确范围的函数,可通过倍增法 动态扩展区间:
cpp
运行
// 倍增法确定包含极值的区间 pair<double, double> findRange() { double left = 0, right = 1; // 扩展右边界直至函数值开始下降(单峰函数) while (func(right) > func(right * 2)) { right *= 2; } return {left, right}; }
六、总结
三分查找是一种针对凸函数(单峰 / 单谷)的高效搜索算法,通过三等分区间并比较中间点函数值,逐步逼近极值点,时间复杂度为 O (log n)。其核心优势在于:
- 适用于非单调但凸性明确的函数,弥补了二分查找的局限性。
- 实现简单,仅需几行代码即可完成核心逻辑。
- 在函数极值求解、最优化问题等场景中表现优异。
使用三分查找时,需注意:
- 确保函数满足单峰 / 单谷特性,否则可能得到错误结果。
- 合理设置精度或迭代次数,平衡效率与准确性。
- 正确初始化搜索区间,确保覆盖目标极值点。
掌握三分查找,不仅能解决特定场景下的算法问题,更能培养对函数特性的分析能力,为处理复杂优化问题提供有力工具。