[最优化技术] 3-3 格点法与平分法

3-3 格点法与平分法

格点法与平分法

简述

\(\qquad\)本篇介绍两种较为简单的一维搜索方法:格点法(Grid Search)与平分法(Bisection Method)。

\(\qquad\)与黄金分割法和二次插值法不同,这两种方法逻辑更为直观。格点法通过等距采样寻找极小值区域,虽然计算量较大但实现极其简单;平分法则利用了函数的导数信息(或差分近似),每次将搜索区间缩减一半,收敛速度稳定。由于这两种方法推导过程较少,故合并在一篇中介绍。

格点法

原理

\(\qquad\)格点法的基本思想是将搜索区间等分为若干份,通过计算各分点处的函数值,找出最小值点,然后在该点附近缩小区间,重复此过程直至满足精度要求。

具体步骤

对于给定区间 \([a, b]\) 和精度 \(\varepsilon\),设定等分份数 \(n\)(通常 \(n \ge 2\)):

  • ① 将区间 \([a, b]\) 等分为 \(n+1\) 个小区间,产生 \(n\) 个内点 \(a_1, a_2, \dots, a_n\)。计算各点函数值 \(f(a_1), \dots, f(a_n)\)
  • ② 找出函数值最小的点 \(a_m\),即 \(f(a_m) = \min \{f(a_1), \dots, f(a_n)\}\)。取 \([a_{m-1}, a_{m+1}]\) 作为新的搜索区间(若 \(m=1\) 则取 \([a, a_2]\),若 \(m=n\) 则取 \([a_{n-1}, b]\))。
  • ③ 重复上述过程,直到区间长度 \(|a_{m-1} - a_{m+1}| \le \varepsilon\)(或新区间长度足够小),取极小值点 \(x^* = a_m\)

流程框图

格点法流程图

代码示例

格点法 C++ 代码示例:
// 格点法 求最小值点(横坐标)
// n: 每次将区间分为 n+1 份(即 n 个内点)
double grid_search(function<double(double)> func, double l, double r, double eps, int n = 10) {
    double a = l, b = r;
    int step = 0;
    printf("[debug] step %d: [%.5lf, %.5lf]\n", step, a, b);
    
    while (b - a > eps) {
        double min_val = 1e18;
        int min_idx = -1;
        double step_size = (b - a) / (n + 1);
        
        // 寻找 n 个内点中的最小值
        // 实际区间点是 a + step_size, a + 2*step_size ...
        vector<double> points(n);
        for (int i = 1; i <= n; i++) {
            points[i-1] = a + i * step_size;
            double val = func(points[i-1]);
            if (val < min_val) {
                min_val = val;
                min_idx = i - 1; // 0-based index
            }
        }
        
        double xm = points[min_idx];
        
        // 确定新区间 [a_{m-1}, a_{m+1}]
        double new_l = (min_idx == 0) ? a : points[min_idx - 1];
        double new_r = (min_idx == n - 1) ? b : points[min_idx + 1];
        
        a = new_l;
        b = new_r;
        
        step++;
        printf("[debug] step %d: [%.5lf, %.5lf], min_x=%.5lf\n", step, a, b, xm);
        
        // 如果区间已经非常小,直接返回当前最小点
        if (b - a <= eps) {
            return xm;
        }
    }
    return (a + b) / 2.0;
}

平分法

原理

\(\qquad\)平分法(又称二分法)利用了单谷函数在极值点左侧导数为负、右侧导数为正的性质。通过不断取区间中点并判断导数符号,可以将搜索区间每次缩减一半。

具体步骤(基于导数)

对于给定区间 \([a, b]\) 和精度 \(\varepsilon\)

  • ① 计算中点 \(a_k = \frac{a+b}{2}\),并求出导数值 \(f'(a_k)\)
  • ② 若 \(f'(a_k) < 0\),说明极小值点在右侧,缩小区间为 \([a_k, b]\)
    \(\quad\)\(f'(a_k) > 0\),说明极小值点在左侧,缩小区间为 \([a, a_k]\)
    \(\quad\)\(f'(a_k) = 0\),则 \(a_k\) 即为极小值点。
  • ③ 重复上述过程,直到:
    a. \(f'(a_k) = 0\)(或 \(|f'(a_k)| < \varepsilon\)),则 \(x^* = a_k\)
    b. \(|b - a| \le \varepsilon\),则 \(x^* = \frac{a+b}{2}\)

关于导数难求的情况

对于一阶导数 \(f'(x)\) 难以求出的函数,可以通过计算两个点的函数值 \(f(\alpha_k - \delta)\)\(f(\alpha_k + \delta)\)\(\delta\) 为极小量)来近似判断导数符号:

  • \(f(\alpha_k - \delta) > f(\alpha_k + \delta)\),说明函数在下降,导数近似为负,区间缩为 \([\alpha_k, b]\)
  • \(f(\alpha_k - \delta) < f(\alpha_k + \delta)\),说明函数在上升,导数近似为正,区间缩为 \([a, \alpha_k]\)

此时缩减率仍然接近 50%。

代码示例

平分法 C++ 代码示例:
// 平分法 求最小值点(横坐标)
// 需要传入原函数 func 和 导函数 deriv
double bisection_method(function<double(double)> func, function<double(double)> deriv, double l, double r, double eps) {
    double a = l, b = r;
    int step = 0;
    printf("[debug] step %d: [%.5lf, %.5lf]\n", step, a, b);
    
    while (b - a > eps) {
        double mid = (a + b) / 2.0;
        double d_val = deriv(mid);
        
        // 导数接近0,认为是极值点
        if (abs(d_val) < eps)
            return mid;
        
        if (d_val < 0)
            a = mid; // 极小值在右侧
        else
            b = mid; // 极小值在左侧
        
        step++;
        printf("[debug] step %d: [%.5lf, %.5lf], mid=%.5lf, f'=%.5lf\n", step, a, b, mid, d_val);
    }
    return (a + b) / 2.0;
}

// 平分法(无导数版本/差分近似)
double bisection_no_deriv(function<double(double)> func, double l, double r, double eps) {
    double a = l, b = r;
    double delta = 1e-6; // 差分步长
    int step = 0;
    printf("[debug] step %d: [%.5lf, %.5lf]\n", step, a, b);
    
    while (b - a > eps) {
        double mid = (a + b) / 2.0;
        double y_left = func(mid - delta);
        double y_right = func(mid + delta);
        
        if (y_left > y_right)
            a = mid; // 近似导数 < 0
        else
            b = mid; // 近似导数 > 0
        
        step++;
        printf("[debug] step %d: [%.5lf, %.5lf], mid=%.5lf\n", step, a, b, mid);
    }
    return (a + b) / 2.0;
}

\(\qquad\)结合之前文章所讲的进退法确定初始区间,我们可以轻松求解出任何一维单谷函数的极小值点。给出如下完整C++代码仅供参考(以平分法为例)。

完整 C++ 代码实现:
#include <iostream>
#include <cmath>
#include <functional>
#include <utility>
#include <vector>
#include <stdexcept>
using namespace std;

// 进退法求初始区间
pair<double, double> bracket_minimum(function<double(double)> func, double a0, double h) {
    const int MAX_ITER = 1e4;
    if (h <= 0) throw invalid_argument("Step size h must be positive.");
    if (func(a0) <= func(a0 + h)) {
        printf("[debug] left search.\n");
        double x0 = a0 + h, x1 = a0;
        h *= 2;
        double x2 = x1 - h;
        int iter = 0;
        while (func(x2) < func(x1)) {
            if (iter++ > MAX_ITER) throw runtime_error("Max iterations exceeded");
            x0 = x1; x1 = x2; h *= 2; x2 = x1 - h;
        }
        return make_pair(x2, x0);
    } else {
        printf("[debug] right search.\n");
        double x0 = a0;
        double x1 = a0 + h;
        h *= 2;
        double x2 = x1 + h;
        int iter = 0;
        while (func(x2) < func(x1)) {
            if (iter++ > MAX_ITER) throw runtime_error("Max iterations exceeded");
            x0 = x1; x1 = x2; h *= 2; x2 = x1 + h;
        }
        return make_pair(x0, x2);
    }
}

// 平分法(需导数)
double bisection_method(function<double(double)> func, function<double(double)> deriv, double l, double r, double eps) {
    double a = l, b = r;
    int step = 0;
    printf("[debug] step %d: [%.5lf, %.5lf]\n", step, a, b);
    while (b - a > eps) {
        double mid = (a + b) / 2.0;
        double d_val = deriv(mid);
        if (abs(d_val) < eps) return mid;
        if (d_val < 0) a = mid;
        else b = mid;
        step++;
        printf("[debug] step %d: [%.5lf, %.5lf]\n", step, a, b);
    }
    return (a + b) / 2.0;
}

int main() {
    // 定义一个一元凸函数 f(x) = x^2 - 2x - 5
    // 导数 f'(x) = 2x - 2
    auto f = [](double x) { return x * x - 2 * x - 5; };
    auto df = [](double x) { return 2 * x - 2; };

    double a0, h, eps;
    cout << "输入初始点a0: "; cin >> a0;
    cout << "输入初始步长h: "; cin >> h;
    cout << "输入误差精度epsilon: "; cin >> eps;

    pair<double, double> range;
    try {
        range = bracket_minimum(f, a0, h);
        cout << "初始搜索区间为:(" << range.first << ", " << range.second << ").\n";
    } catch (const exception& e) {
        cerr << "Error: " << e.what() << endl;
        return 1;
    }

    double res = bisection_method(f, df, range.first, range.second, eps);
    cout << "最小值点 为:" << res << endl;
    cout << "最小值   为:" << f(res) << endl;
    return 0;
}

结语

\(\qquad\)在之前的几篇文章中,我们讲解了多种一维搜索方法,包括黄金分割法二次插值法格点法以及平分法,它们适用于求解一元函数凸函数的极小值。在接下来的几篇文章中,我们将详细讲解几个适用于多元非线性函数的无约束优化问题。

本学习笔记参考资料:

[1] 白清顺, 孙靖民, 梁迎春. 机械优化设计 第7版[M]. 北京: 机械工业出版社, 2024. ISBN: 978-7-111-75103-8

[2] 武汉理工大学《最优化技术B》课程课件,授课教师:颜彬老师

posted @ 2026-07-31 09:18  H_Elden  阅读(55)  评论(0)    收藏  举报