[最优化技术] 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》课程课件,授课教师:颜彬老师

浙公网安备 33010602011771号