每日算法 — 使用java实现三分搜索:单峰函数极值查找与最优决策

算法背景

三分搜索(Ternary Search)是二分搜索的自然推广,用于在单峰函数(unimodal function)上快速定位极值点。与二分搜索每次排除一半区间不同,三分搜索通过两个中间点将区间划分为三部分,排除不含极值的两段之一,从而实现 $O(\log n)$ 的最优化搜索。

该算法在机器学习超参数调优、经济学边际分析、游戏AI决策评估、物理系统最优控制等场景中均有重要应用。

核心思想

对于定义在区间 $[l, r]$ 上的单峰函数 $f(x)$(先严格递增后严格递减,或反之),取两个三分点:

$$m_1 = l + \frac{r-l}{3}, \quad m_2 = r – \frac{r-l}{3}$$

比较 $f(m_1)$ 与 $f(m_2)$:
– 若 $f(m_1) < f(m_2)$,极值点在 $[m_1, r]$
– 若 $f(m_1) > f(m_2)$,极值点在 $[l, m_2]$
– 若相等,极值点在 $[m_1, m_2]$

每次迭代将搜索区间缩小约 $2/3$。

Java实现:整数域三分搜索

public class TernarySearch {

    /**
     * 在整数区间[l, r]上查找单峰函数的最大值点
     * @param f 单峰函数 (先增后减)
     * @return 使f(x)最大的整数x
     */
    public static int findMaxInt(IntUnaryOperator f, int l, int r) {
        while (r - l > 3) {
            int m1 = l + (r - l) / 3;
            int m2 = r - (r - l) / 3;

            if (f.applyAsInt(m1) < f.applyAsInt(m2)) {
                l = m1; // 最大值在右侧
            } else {
                r = m2; // 最大值在左侧或中间
            }
        }

        // 区间缩小到3个以内时,线性扫描
        int maxVal = f.applyAsInt(l);
        int maxPos = l;
        for (int i = l + 1; i <= r; i++) {
            int val = f.applyAsInt(i);
            if (val > maxVal) {
                maxVal = val;
                maxPos = i;
            }
        }
        return maxPos;
    }

    /**
     * 在整数区间[l, r]上查找单峰函数的最小值点
     */
    public static int findMinInt(IntUnaryOperator f, int l, int r) {
        while (r - l > 3) {
            int m1 = l + (r - l) / 3;
            int m2 = r - (r - l) / 3;

            if (f.applyAsInt(m1) > f.applyAsInt(m2)) {
                l = m1;
            } else {
                r = m2;
            }
        }

        int minVal = f.applyAsInt(l);
        int minPos = l;
        for (int i = l + 1; i <= r; i++) {
            int val = f.applyAsInt(i);
            if (val < minVal) {
                minVal = val;
                minPos = i;
            }
        }
        return minPos;
    }

    public static void main(String[] args) {
        // 示例: f(x) = -x^2 + 10x + 5, 在[0, 20]上的最大值在x=5
        IntUnaryOperator f = x -> -x * x + 10 * x + 5;
        int maxPos = findMaxInt(f, 0, 20);
        System.out.println("Maximum at x = " + maxPos + ", value = " + f.applyAsInt(maxPos));

        // 示例: 数组中找峰值 (先增后减的数组)
        int[] arr = {1, 3, 5, 7, 9, 11, 10, 8, 6, 4, 2};
        IntUnaryOperator arrF = x -> arr[x];
        int peak = findMaxInt(arrF, 0, arr.length - 1);
        System.out.println("Peak index = " + peak + ", value = " + arr[peak]);
    }
}

Java实现:实数域三分搜索

public class TernarySearchDouble {

    private static final double EPS = 1e-9;

    /**
     * 在实数区间[l, r]上查找单峰函数的极大值点
     */
    public static double findMaxDouble(DoubleUnaryOperator f, double l, double r) {
        while (r - l > EPS) {
            double m1 = l + (r - l) / 3.0;
            double m2 = r - (r - l) / 3.0;

            if (f.applyAsDouble(m1) < f.applyAsDouble(m2)) {
                l = m1;
            } else {
                r = m2;
            }
        }
        return (l + r) / 2.0;
    }

    /**
     * 在实数区间[l, r]上查找单峰函数的极小值点
     */
    public static double findMinDouble(DoubleUnaryOperator f, double l, double r) {
        while (r - l > EPS) {
            double m1 = l + (r - l) / 3.0;
            double m2 = r - (r - l) / 3.0;

            if (f.applyAsDouble(m1) > f.applyAsDouble(m2)) {
                l = m1;
            } else {
                r = m2;
            }
        }
        return (l + r) / 2.0;
    }

    public static void main(String[] args) {
        // 示例1: 抛物线顶点 f(x) = -(x-3)^2 + 10
        DoubleUnaryOperator parabola = x -> -(x - 3) * (x - 3) + 10;
        double maxX = findMaxDouble(parabola, -10, 10);
        System.out.printf("Parabola max at x = %.9f, value = %.9f%n", maxX, parabola.applyAsDouble(maxX));

        // 示例2: 寻找最优点使得成本最小
        // cost(x) = x^2 + 10/x, x > 0
        DoubleUnaryOperator cost = x -> x * x + 10.0 / x;
        double optimalX = findMinDouble(cost, 0.1, 10);
        System.out.printf("Optimal x = %.9f, min cost = %.9f%n", optimalX, cost.applyAsDouble(optimalX));
    }
}

游戏AI应用:最优射击角度

public class GameAITernarySearch {

    /**
     * 模拟抛体运动,计算命中目标的射击角度
     * 使用三分搜索寻找使落点最接近目标的角度
     */
    static class ProjectileResult {
        double distance; // 落点距离
        double maxHeight; // 最大高度

        ProjectileResult(double d, double h) { this.distance = d; this.maxHeight = h; }
    }

    static ProjectileResult simulate(double angleDeg, double velocity, double gravity) {
        double angleRad = Math.toRadians(angleDeg);
        double vx = velocity * Math.cos(angleRad);
        double vy = velocity * Math.sin(angleRad);
        double time = 2 * vy / gravity;
        double dist = vx * time;
        double maxH = vy * vy / (2 * gravity);
        return new ProjectileResult(dist, maxH);
    }

    /**
     * 三分搜索最优射击角度
     */
    public static double findOptimalAngle(double targetDist, double velocity, double gravity) {
        DoubleUnaryOperator error = angle -> {
            ProjectileResult r = simulate(angle, velocity, gravity);
            return -Math.abs(r.distance - targetDist); // 负数,越接近0越好
        };

        // 在[1, 89]度范围内搜索
        return TernarySearchDouble.findMaxDouble(error, 1.0, 89.0);
    }

    public static void main(String[] args) {
        double target = 500.0; // 目标距离500米
        double v0 = 100.0;     // 初速100m/s
        double g = 9.8;        // 重力加速度

        double bestAngle = findOptimalAngle(target, v0, g);
        ProjectileResult result = simulate(bestAngle, v0, g);

        System.out.printf("Target distance: %.1f m%n", target);
        System.out.printf("Optimal angle: %.4f degrees%n", bestAngle);
        System.out.printf("Actual distance: %.4f m%n", result.distance);
        System.out.printf("Max height: %.4f m%n", result.maxHeight);
    }
}

高维扩展:三分套三分

对于二元单峰函数 $f(x, y)$,可以外层对 $x$ 三分,内层对 $y$ 三分:

public class NestedTernarySearch {

    /**
     * 二元单峰函数极值搜索
     * 外层三分x,内层三分y
     */
    public static double[] findMax2D(BiFunction<Double, Double, Double> f,
                                      double xl, double xr,
                                      double yl, double yr) {
        double bestX = 0, bestY = 0;

        while (xr - xl > 1e-7) {
            double xm1 = xl + (xr - xl) / 3.0;
            double xm2 = xr - (xr - xl) / 3.0;

            // 对固定的x,三分搜索最优y
            double y1 = findBestY(f, xm1, yl, yr);
            double y2 = findBestY(f, xm2, yl, yr);

            if (f.apply(xm1, y1) < f.apply(xm2, y2)) {
                xl = xm1;
                bestX = xm2;
                bestY = y2;
            } else {
                xr = xm2;
                bestX = xm1;
                bestY = y1;
            }
        }

        return new double[]{bestX, bestY};
    }

    private static double findBestY(BiFunction<Double, Double, Double> f,
                                    double x, double yl, double yr) {
        while (yr - yl > 1e-7) {
            double ym1 = yl + (yr - yl) / 3.0;
            double ym2 = yr - (yr - yl) / 3.0;
            if (f.apply(x, ym1) < f.apply(x, ym2)) {
                yl = ym1;
            } else {
                yr = ym2;
            }
        }
        return (yl + yr) / 2.0;
    }
}

复杂度分析

指标 整数域 实数域
时间复杂度 $O(\log_3 (r-l))$ $O(\log(1/\epsilon))$
空间复杂度 $O(1)$ $O(1)$
每次迭代比较次数 2次函数求值 2次函数求值

与黄金分割搜索的对比

特性 三分搜索 黄金分割搜索
区间缩小比例 $2/3$ $1/\phi \approx 0.618$
是否复用函数值 否 是(更优)
实现复杂度 简单 稍复杂
适用场景 函数求值代价低 函数求值代价高

应用场景

  1. 机器学习: 学习率、正则化系数等超参数的网格搜索优化
  2. 经济学: 边际收益等于边际成本时的最优产量决策
  3. 游戏开发: NPC行为参数的最优配置
  4. 信号处理: 最佳滤波器系数的自适应调整
  5. 运筹学: 单变量凸优化问题的快速求解

三分搜索以其实现简洁、理论清晰的特点,成为单峰优化问题的首选算法之一。掌握其原理后,可灵活应用于各类最优决策场景。