简介:这份MATLAB开发资源聚焦穷举搜索法(Exhaustive Search Method)的实现,面向正在学习优化算法、需要动手验证全搜索策略的初学者与工程人员。它解决的是解空间有限、可枚举场景下的最优解求解问题,帮助读者理解遍历所有候选解并比较目标函数值的基本流程,同时涉及剪枝、并行计算与内存管理等效率优化思路。压缩包共2个文件,包含1个m脚本与1个txt许可说明,前者承载穷举搜索的核心算法代码,后者用于明确使用条款与合规边界,整体约1KB,体量轻便,便于快速阅读与二次修改。目前已有314人学习下载。读者可从中获得可直接运行的算法脚本、目标函数与约束条件的组织方式、解空间生成与评估的代码骨架,以及结合MATLAB并行工具箱加速搜索的参考方向,适合作为优化课程实验或小规模问题求解的入门素材。
1. 穷举搜索法在 MATLAB 里的落地:从一份可运行源码说起
如果你写过优化算法,大概率遇到过这种场景:目标函数没有梯度、不可导,甚至带点离散跳变,梯度下降直接失效,牛顿法连第一步都迈不出去。这时候最朴素也最稳的办法就是穷举搜索——把可行域切成网格,一格一格试,谁的目标函数值最小就选谁。这份 MATLAB 源码包ExhaustiveSearchMethod干的就是这件事,它把一维和多维的穷举搜索封装成可直接调用的函数,附带示例脚本和结果可视化。适合正在学数值优化、需要交课程设计,或者手头有个小规模参数寻优任务、不想上遗传算法那套重装备的开发者。代码量不大,但麻雀虽小,五脏俱全,拿来当优化算法的入门脚手架正合适。
2. 项目结构与核心文件拆解:先看清骨架再动手
2.1 目录布局与文件职责
拿到源码包,第一件事不是急着run,而是把目录结构摸清楚。这个项目的组织方式很典型,属于「一个主函数 + 若干辅助函数 + 示例脚本」的布局。常见做法是长这样:
ExhaustiveSearchMethod/ ├── main.m % 入口脚本,跑示例 ├── exhaustiveSearch1D.m % 一维穷举搜索核心函数 ├── exhaustiveSearchND.m % 多维穷举搜索核心函数 ├── objectiveFunc.m % 目标函数定义(示例用) ├── plotResult.m % 结果可视化 └── README.txt % 使用说明main.m是入口,负责设定搜索区间、步长、调用核心函数、画图。exhaustiveSearch1D.m和exhaustiveSearchND.m是两个核心求解器,分别处理一维和多维情况。objectiveFunc.m是目标函数,示例里一般给个带多个局部极值的函数,用来体现穷举搜索「不会陷进局部最优」的优势。plotResult.m负责把搜索过程和结果画出来,方便直观验证。
提示:不同版本的源码包文件名可能略有差异,但职责划分基本一致。先打开
main.m看它调用了哪些函数,就能反推出整个调用链。
2.2 一维穷举搜索的核心实现
一维情况最简单,也最能说明穷举搜索的本质。核心逻辑就三步:生成网格点、逐点求值、取极值。下面是一段典型实现:
function [xOpt, fOpt, history] = exhaustiveSearch1D(f, a, b, step) % f 目标函数句柄 % a, b 搜索区间下界和上界 % step 网格步长 % xOpt 最优解 % fOpt 最优目标函数值 % history 搜索历史,用于可视化 x = a:step:b; % 生成网格点 n = length(x); fVals = zeros(1, n); history = zeros(n, 2); for k = 1:n fVals(k) = f(x(k)); % 逐点求值 history(k, :) = [x(k), fVals(k)]; end [fOpt, idx] = min(fVals); % 取最小值 xOpt = x(idx); end这段代码的关键参数是step。步长越小,搜索越精细,但计算量线性增长。history数组记录了每个网格点的坐标和函数值,后面画图时直接拿来用。注意min默认求最小值,如果目标函数是求最大值,要么取负号,要么把min换成max。
2.3 多维穷举搜索的实现思路
多维情况不能再用简单的冒号表达式,需要用ndgrid生成网格。核心代码如下:
function [xOpt, fOpt] = exhaustiveSearchND(f, lb, ub, steps) % f 目标函数句柄,输入为向量 % lb, ub 各维下界和上界,向量 % steps 各维步长,向量 dim = length(lb); grids = cell(1, dim); for d = 1:dim grids{d} = lb(d):steps(d):ub(d); % 每维生成网格 end [grids{:}] = ndgrid(grids{:}); % 生成多维网格 points = cellfun(@(g) g(:), grids, 'UniformOutput', false); X = [points{:}]; % 每行一个候选点 n = size(X, 1); fVals = zeros(n, 1); for k = 1:n fVals(k) = f(X(k, :)); % 逐点求值 end [fOpt, idx] = min(fVals); xOpt = X(idx, :); endndgrid是这里的核心,它把每一维的网格向量扩展成多维数组,再拉平成候选点矩阵X。steps是个向量,每一维可以设不同步长,比如第一维精度要求高就设小一点。计算量是各维网格点数的乘积,维度一高就会爆炸,所以这个函数只适合低维(一般不超过 4 维)问题。
2.4 目标函数与可视化脚本
示例目标函数一般会选一个多峰函数,比如:
function y = objectiveFunc(x) % 示例:一维多峰函数 y = x.^2 + 10*sin(5*x) + 7*cos(4*x); end这个函数在区间内有好几个局部极小值,梯度法容易卡在某个坑里,穷举搜索则能全局扫描。可视化脚本plotResult.m通常做两件事:画出目标函数曲线,再把搜索过的点标在上面,最后用红点标出最优解。这样一眼就能看出搜索是否覆盖了全局。
3. 运行与验证:从零跑通这份源码
3.1 环境准备与路径设置
MATLAB 版本没有特殊要求,R2016b 以上都行,因为用到了函数句柄和ndgrid这些老牌函数。把源码包解压到任意目录,然后在 MATLAB 里把当前文件夹切过去,或者用addpath把目录加进搜索路径:
addpath(genpath('ExhaustiveSearchMethod'));genpath会连同子目录一起加进来,省得手动一层层加。这一步不做的话,main.m里调用核心函数会报「未定义函数」的错。
3.2 跑通一维示例
打开main.m,一般会看到类似这样的调用:
f = @(x) objectiveFunc(x); a = -10; b = 10; step = 0.1; [xOpt, fOpt, history] = exhaustiveSearch1D(f, a, b, step); fprintf('最优解 x = %.4f, 最小值 f = %.4f\n', xOpt, fOpt); plotResult(history, f, xOpt, fOpt);直接运行,命令行会打印最优解,同时弹出图形窗口。如果图上看不到红点,多半是plotResult的参数顺序传错了,检查一下函数签名。
3.3 跑通多维示例
多维示例的调用方式类似,只是参数变成向量:
f = @(x) (x(1)-2).^2 + (x(2)+1).^2 + 0.5*sin(3*x(1)).*cos(3*x(2)); lb = [-5, -5]; ub = [5, 5]; steps = [0.2, 0.2]; [xOpt, fOpt] = exhaustiveSearchND(f, lb, ub, steps); fprintf('最优解 (% .4f, % .4f), 最小值 %.4f\n', xOpt(1), xOpt(2), fOpt);这里目标函数是个带扰动的二次函数,全局最小值在 (2, -1) 附近。步长 0.2 意味着每维 51 个点,总共 2601 次求值,秒出结果。如果换成 0.01 的步长,点数涨到 100 万,就得等一会儿了。
3.4 结果验证方法
怎么确认跑出来的结果是对的?两个办法。一是把步长减半再跑一次,如果最优解变化很小,说明当前精度够了。二是把目标函数换成已知解析解的简单函数,比如f = @(x) (x-3).^2,理论最优在 x=3,看程序能不能找到 3 附近。这两个验证手段在调参时特别有用。
4. 避坑与常见问题排查
4.1 步长选太小导致内存溢出
现象:多维搜索时 MATLAB 卡死或报「内存不足」。原因:ndgrid生成的多维数组大小是各维点数的乘积,步长设太小会让数组维度爆炸。解决:先用粗步长定位大致范围,再在最优解附近用小步长做二次精细搜索,也就是「粗扫 + 细扫」两阶段策略。
4.2 目标函数不支持向量化输入
现象:调用f(X(k,:))时报维度不匹配。原因:有些目标函数写的时候只考虑了标量输入,里面用了if判断或者矩阵运算。解决:把目标函数改成支持向量化,或者老老实实用循环逐点调用,别图省事传矩阵进去。
4.3 边界点被漏掉
现象:最优解明明在边界上,程序却找不到。原因:a:step:b这种写法,如果(b-a)不是step的整数倍,最后一个点会小于b,边界被截掉。解决:生成网格后手动补上端点,或者用linspace先生成固定点数再算步长。
4.4 求最大值时结果反了
现象:想找最大值,结果返回的是最小值。原因:min函数默认求最小。解决:要么把目标函数取负,f = @(x) -originalFunc(x),最后结果再取负;要么把核心函数里的min改成max。两种都行,但取负法更通用,不用改核心代码。
4.5 可视化时曲线和散点对不上
现象:图上散点位置和函数曲线不吻合。原因:plotResult里画曲线用的网格和搜索用的网格不是同一套,或者坐标轴范围没对齐。解决:画曲线时用更密的网格(比如linspace(a,b,1000)),画散点用搜索历史history,两者共用同一个hold on的坐标系。
5. 二次开发与改造思路:把这套骨架用起来
5.1 改造目标函数适配自己的问题
这份源码最大的价值是骨架,目标函数换成你自己的就行。比如你要调一个 PID 控制器的三个参数,目标函数就是「给定参数下系统的超调量 + 调节时间」,输入是三维向量[Kp, Ki, Kd],输出是标量性能指标。把objectiveFunc.m替换掉,main.m里改一下lb、ub、steps,就能直接跑。注意参数范围要设合理,别让仿真跑飞。
5.2 加一个两阶段搜索策略
前面提过粗扫加细扫,具体实现就是在main.m里调两次核心函数:
% 第一阶段:粗扫 [xCoarse, ~] = exhaustiveSearchND(f, lb, ub, [1, 1]); % 第二阶段:在粗解附近细扫 lbFine = xCoarse - 1; ubFine = xCoarse + 1; [xFine, fFine] = exhaustiveSearchND(f, lbFine, ubFine, [0.05, 0.05]);这样既保证了全局覆盖,又能在最优区域拿到高精度,计算量还可控。我一般会把两阶段的步长比设成 20:1 左右,粗扫步长是细扫的 20 倍。
5.3 并行加速
如果装了 Parallel Computing Toolbox,把多维搜索里的for循环换成parfor就能并行:
parfor k = 1:n fVals(k) = f(X(k, :)); end注意parfor里不能有依赖上一次迭代的变量,这里每次迭代独立求值,正好适合。加速比大概等于核数,四核能快三倍多。没装工具箱的话,用parfeval也行,但代码复杂一些。
5.4 记录搜索历史用于分析
一维版本已经返回了history,多维版本也可以照葫芦画瓢加一个。把每次求值的点和函数值存下来,跑完之后可以分析「搜索了多少个点才找到最优」「最优解附近函数值变化陡不陡」。这些信息对判断结果可信度很有帮助。我习惯在exhaustiveSearchND里加一个可选的returnHistory参数,默认false,需要的时候打开。
5.5 一个容易忽略的细节:函数句柄的捕获
改造目标函数时,如果函数里需要用到外部变量,记得用匿名函数捕获:
targetValue = 3.5; f = @(x) (x - targetValue).^2;这样targetValue的值在定义f的时候就固定了,后面改它不影响f。如果直接在函数文件里用全局变量,调试起来会很头疼。这个坑我踩过不止一次,后来养成习惯,所有外部参数都通过匿名函数捕获,绝不用全局变量。
从那以后我每次改目标函数,都强制走一遍「粗扫验证 + 细扫确认 + 边界检查」三步,再也没出现过结果对不上的情况。希望帮到你。
本文还有配套的精品资源,点击获取