C++ 三分查找:在单调与凸函数中高效定位极值的算法

在算法设计中,当面对单调函数凸函数(单峰 / 单谷函数)时,三分查找(Ternary Search)是一种比二分查找更灵活的高效搜索算法。它通过不断将搜索区间分为三份,逐步逼近目标值或函数极值点,时间复杂度为 O (log n),在数值优化、最值求解等场景中有着广泛应用。本文将从算法原理、实现细节到应用场景,全面解析三分查找的特性与实战技巧。

一、三分查找的核心原理

1.1 适用场景

三分查找的核心适用场景是具有单调性或凸性的函数

  • 单调函数:在定义域内单调递增或递减(此时可视为凸函数的特例)。
  • 单峰函数 :函数值先递增后递减,存在唯一最大值(如抛物线 f(x) = -x² + 2x)。
  • 单谷函数 :函数值先递减后递增,存在唯一最小值(如抛物线 f(x) = x²)。

对于这类函数,通过不断将区间三等分并比较中间点的函数值,可快速定位最值点或特定目标值。

1.2 算法流程

单峰函数求最大值为例,三分查找的步骤如下:

  1. 确定区间 :定义搜索范围 [left, right](函数定义域)。
  2. 三等分区间 :计算两个中间点 mid1 = left + (right - left) / 3mid2 = right - (right - left) / 3
  3. 比较函数值
    • f(mid1) < f(mid2):最大值一定在 [mid1, right] 内(左侧区间可舍弃)。
    • f(mid1) > f(mid2):最大值一定在 [left, mid2] 内(右侧区间可舍弃)。
    • f(mid1) == f(mid2):最大值在 [mid1, mid2] 内(两端区间可舍弃)。
  4. 迭代收缩 :重复步骤 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;
}

关键细节

  • 整数域中 mid1mid2 需避免重叠(通过 mid1 + 1mid2 - 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²)),可通过嵌套三分查找求解极值:

  1. 固定 x,对 y 进行三分查找,得到当前 x 对应的最优 y 及函数值 f(x)
  2. 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)。其核心优势在于:

  • 适用于非单调但凸性明确的函数,弥补了二分查找的局限性。
  • 实现简单,仅需几行代码即可完成核心逻辑。
  • 在函数极值求解、最优化问题等场景中表现优异。

使用三分查找时,需注意:

  1. 确保函数满足单峰 / 单谷特性,否则可能得到错误结果。
  2. 合理设置精度或迭代次数,平衡效率与准确性。
  3. 正确初始化搜索区间,确保覆盖目标极值点。

掌握三分查找,不仅能解决特定场景下的算法问题,更能培养对函数特性的分析能力,为处理复杂优化问题提供有力工具。

相关推荐
我命由我123452 小时前
Element Plus 组件库 - Select 选择器 value 为 index 时的一些问题
开发语言·前端·javascript·vue.js·html·ecmascript·js
沐知全栈开发2 小时前
MySQL 删除数据库指南
开发语言
立志成为大牛的小牛2 小时前
数据结构——四十二、二叉排序树(王道408)
数据结构·笔记·程序人生·考研·算法
qq. 28040339843 小时前
js 原型链分析
开发语言·javascript·ecmascript
Elnaij3 小时前
从C++开始的编程生活(13)——list和浅谈stack、queue
开发语言·c++
Funny_AI_LAB4 小时前
李飞飞联合杨立昆发表最新论文:超感知AI模型从视频中“看懂”并“预见”三维世界
人工智能·算法·语言模型·音视频
RTC老炮7 小时前
webrtc降噪-PriorSignalModelEstimator类源码分析与算法原理
算法·webrtc
深思慎考7 小时前
微服务即时通讯系统(服务端)——用户子服务实现逻辑全解析(4)
linux·c++·微服务·云原生·架构·通讯系统·大学生项目
一晌小贪欢7 小时前
【Python数据分析】数据分析与可视化
开发语言·python·数据分析·数据可视化·数据清洗