MATLAB实现节约算法与禁忌搜索求解LRP问题:从模型到代码全解析
2026/9/18 19:57:40 网站建设 项目流程

简介:面向本科毕业设计中的物流选址-路径(LRP)问题,提供基于Matlab实现的节约算法与禁忌搜索算法源码及配套说明,适合计算机、自动化、交通运输等专业学生进行课程设计、毕设或算法对比。压缩包共16个文件,以12个.m脚本为主,覆盖初始解构造、节约合并、禁忌搜索主循环、距离计算与绘图展示等模块;另有3个.asv自动备份文件与1个README说明文档,整体仅13KB,结构紧凑、便于后续按需修改与功能扩展。目前已有148人学习下载,项目代码经作者调试运行成功,答辩评分96分。下载后可运行示例脚本快速体验,通过对比两种算法模块的求解结果,理解搜索策略差异;也可调整初始解、距离计算等关键函数以适配自有数据,对毕设二次开发和算法学习都有直接帮助。

1. LRP 问题为什么值得用节约算法和禁忌搜索算法去解

本科毕业设计题目里最经久不衰的组合优化选题之一,就是基于 matlab 实现节约算法和禁忌搜索算法的 LRP 问题。LRP 是 Location-Routing Problem 的缩写,要同时决定配送中心开在哪、每个中心派几台车、车按什么顺序拜访哪些客户;选址和配路两个子问题单独都是 NP-hard,叠在一起之后精确算法在 50 个客户规模基本跑不动,这正是两代启发式算法长期占据教科书和实践的原因。节约算法负责在给定中心集合下把客户快速拼成低代价路径,禁忌搜索负责在中心开关和客户归属之间做系统化翻找。下面按建模、初始解、搜索、验证四步,把可复现的 MATLAB 源代码逐段拆开,毕业生可以照着组织文档说明,做物流仿真的工程师也能直接改参数复用。

2. LRP 问题的数学模型与 MATLAB 里的数据组织

2.1 为什么选址和路径两个决策层不能分开解

LRP 最迷惑人的地方在于它看起来像"先选址、再配路"两步走。常见的拆分做法是先把客户按最近距离分给配送中心,再对每个中心单独跑 VRP,但这样得到的解在大多数算例上都会明显偏贵:客户归完中心之后,路径会在空间上互相穿插,单车走出来的折线距离远大于把客户重新分组后各自成环的距离。反过来先配路再落地选址更不成立,路径依赖的中心位置根本还不存在。

所以在建模阶段就要承认,选址变量和路径变量共享同一个目标函数,这是 LRP 和普通 VRP 的根本区别,也是毕业设计文档说明里最值得写清楚的第一页。算法的分工因此也很明确:节约算法只优化行驶成本,开放中心的固定成本必须靠禁忌搜索的开关中心邻域去平衡。理解这一层,才能解释为什么标题要把两个算法放在一起,而不是随便拿一个贪心收尾。

2.2 LRP 的 0-1 整数规划模型与约束含义

书面模型用教科书紧凑版就够了,变量含义比符号重要。记 I 为候选中心集合,J 为客户集合,f_i 为中心 i 的固定开放成本,q_j 为客户 j 的需求,c_jk 为从 j 到 k 的行驶距离,capV 为车载容量,cap_i 为中心容量。目标函数写成两项:

min Z = Σ(i∈I) f_i · y_i + Σ(i∈I) Σ(j,k∈J∪{i}) c_jk · x_ijk

约束条件分别是:每个客户恰好被一条路径访问一次;路径满足流量守恒,即车辆进入某点后必须从该点离开;任意路径载重不超过 capV;任一中心服务的客户总需求不超过 cap_i × y_i,中心不开就不能服务客户。

对毕业设计来说,并不需要真的去解这个混合整数规划,但模型挂在手边有三个实际用途:一是说清 calCost 代价函数里算的到底是什么;二是做小规模算例时用枚举或者 intlinprog 给启发式算法对照一个精确界;三是论文里画变量关系图时有依据。我一般会保留"固定成本 + 行驶成本"这一项结构,后面两种算法的接口都对着它写。

2.3 prob 结构体、距离矩阵与随机算例生成

MATLAB 里处理这类问题最顺手的做法是定义一个 prob 结构体,把坐标、需求、容量、距离矩阵一次性放进去。半径 100 的平面随机算例生成代码如下。

rng(2024); % 固定随机种子,保证可复现 prob.nCust = 30; % 客户数量 prob.custXY = randi([0 100], prob.nCust, 2); prob.demand = randi([2 10], prob.nCust, 1); prob.nDepot = 5; % 候选配送中心数量 prob.depotXY = [20 20; 80 20; 20 80; 80 80; 50 50]; prob.depotCost = [150 160 140 170 120]; % 中心固定开放成本 prob.depotCap = [120 120 110 130 140]; % 中心容量 prob.vehCap = 40; % 车辆容量 prob.distCust = pdist2(prob.custXY, prob.custXY); % 客户间距离 n x n prob.distDepot = pdist2(prob.depotXY, prob.custXY); % 中心到客户距离 m x n save('lrp_instance.mat', 'prob'); % 直接落盘,方便文档说明引用

pdist2 一步算出全连接距离矩阵,后续节约值计算全部基于这两个矩阵,不用每次现算。距离矩阵是对称的,而且欧氏距离满足三角不等式,这是节约算法能用的前提。随机种子固定下来之后,整个毕业设计的所有实验结果都可以复现,这是评审老师最看重的点之一。

提示:如果客户坐标是经纬度,pdist2 直接算欧氏距离会严重低估实际里程,要换成 haversine 公式;平面坐标算例则完全不用管这个问题。

3. 节约算法在 MATLAB 里的实现与容量约束处理

3.1 节约值公式与一个四客户手算例子

Clarke-Wright 节约算法的核心是 s(i,j) = d(0,i) + d(0,j) − d(i,j),其中 0 代表当前配送中心。含义很直观:客户 i、j 各自单独跑一趟成本是 2·d(0,i) + 2·d(0,j),合成一条路径变成 d(0,i) + d(i,j) + d(j,0),省下的正好是 d(0,i) + d(0,j) − d(i,j)。对 LRP 而言,"0"必须换成具体配送中心,所以节约值要按中心逐个算,这是它和纯 VRP 实现的最大差别。

用一个四客户例子手算一遍,能省掉很多调试时的困惑。中心在原点 O(0,0),客户 A(5,0)、B(0,6)、C(8,8)、D(9,1),需求分别是 6、4、7、5,车容量 18。所有客户对按节约值降序排列如下。

客户对节约值当前端点 ?合并后载重决策
C-D13.30都是12合并 [C,D]
A-D9.94都是18合并 [A,D,C]
B-C9.06C 已非端点跳过
A-C7.77C 已非端点跳过
B-D4.76D 已非端点跳过
A-B3.19都是22 > 18超载,跳过

最终得到两条路径 [A,D,C] 和 [B],总成本 39.50,对比不合并的 62.74 节约了 23.24。注意 A-D 合并时 D 是 [C,D] 的端点,路径翻转后得到 [A,D,C],而不是把 A 插到任意位置——节约算法只允许"端点接端点",这是实现时最容易写错的地方。

3.2 buildRoutesBySavings:单中心的节约算法函数

function routes = buildRoutesBySavings(custDist, depotDist, demand, vehCap) % custDist : n x n 客户间距离矩阵 % depotDist: 1 x n 配送中心到各客户的距离 % 返回 cell 数组,每个元素是一条客户路径 n = length(demand); routes = num2cell(1:n); % 初始:每位客户独立一条路径 routeOf = 1:n; % 客户 -> 所属路径编号 active = true(1, n); % 路径是否还在合并池中 load_ = demand(:)'; [I, J] = ndgrid(1:n, 1:n); up = I < J; iL = I(up); jL = J(up); sav = depotDist(iL) + depotDist(jL) - custDist(sub2ind([n n], iL, jL)); [~, ord] = sort(sav, 'descend'); % 核心:节约值降序 for t = 1:length(ord) i = iL(ord(t)); j = jL(ord(t)); ri = routeOf(i); rj = routeOf(j); if ~active(ri) || ~active(rj) || ri == rj, continue; end if load_(ri) + load_(rj) > vehCap, continue; end Ri = routes{ri}; Rj = routes{rj}; iP = find(Ri == i); jP = find(Rj == j); if ~(ismember(iP, [1 numel(Ri)]) && ismember(jP, [1 numel(Rj)])), continue; end if iP ~= numel(Ri), Ri = fliplr(Ri); end % 翻转让 i 到右端 if jP ~= 1, Rj = fliplr(Rj); end % 翻转让 j 到左端 routes{ri} = [Ri Rj]; active(rj) = false; routeOf(routes{ri}) = ri; % 整条链重新登记归属 load_(ri) = load_(ri) + load_(rj); end routes = routes(active); end

routeOf 和 active 两个数组配合,避免在循环里频繁删除 cell 元素导致索引错乱,这是 MATLAB 实现节约算法时最值得留意的工程细节。端点检查用ismember(iP, [1 numel(Ri)]),保证只合并两条路径的端部;fliplr 翻转后统一从 i 的右端接 j 的左端,四种接法塌缩成一种,代码量少一半。

性能上,枚举所有客户对并排序是 O(n² log n),n 在 200 以内直接用没有问题;超过这个规模再考虑按空间网格预剪枝。毕业设计 30~80 个客户的算例,这个函数就是最终形态。

3.3 容量感知的最近中心分配:多中心初始解

LRP 的初始解不能用"纯最近中心"分配,因为最便宜的中心会被先塞满,禁忌搜索后面要花大量迭代把客户搬走。我一般按容量优先做贪心分配,得到可行解再逐中心跑节约算法。

function assign = nearestDepotAssign(prob) [~, ord] = sort(prob.distDepot, 1); % 每个客户按中心距离升序 depotLoad = zeros(1, prob.nDepot); assign = zeros(1, prob.nCust); for k = 1:prob.nCust for r = 1:prob.nDepot d = ord(r, k); if depotLoad(d) + prob.demand(k) <= prob.depotCap(d) assign(k) = d; depotLoad(d) = depotLoad(d) + prob.demand(k); break; end end end end

ord 的第二维是客户,第一维是中心距离排名,这样每个客户都从最近的中心开始试,容量不够才顺延到下一家。如果全部中心都放不下,说明 depotCap 设置过小,属于算例问题而不是算法问题,在文档说明里要单独标注。

3.4 代价函数 calCost:把路径成本折算回 LRP 目标

初始解和禁忌搜索共用一个代价函数。对每个开放中心,取出属于它的客户,用节约算法重排路径,再把路径首尾到中心的距离和中心固定成本全部累加。

function cost = calCost(prob, assign, open) cost = sum(prob.depotCost(open)); % 固定开放成本 for d = find(open) idx = find(assign == d); if isempty(idx), continue; end subDist = prob.distCust(idx, idx); % 局部客户间距离 subDepot = prob.distDepot(d, idx); % 本中心到这些客户 rt = buildRoutesBySavings(subDist, subDepot, prob.demand(idx), prob.vehCap); for t = 1:numel(rt) r = rt{t}; c = subDepot(r(1)) + subDepot(r(end)); % 首尾往返 for k = 1:numel(r)-1 c = c + subDist(r(k), r(k+1)); % 内部弧长 end cost = cost + c; end end end

每次 calCost 都对开放中心内重新跑一遍节约算法,这是"朴素但正确"的实现。30 个客户规模下,一次 calCost 在 1ms 量级,禁忌搜索 200 代也就几万次调用,完全扛得住。工程上更讲究的做法是只对受影响的中心做增量重算,但这个优化留给文档说明的"改进方向"章节写更合适。

4. 禁忌搜索算法的邻域、禁忌表与主循环代码

4.1 解的编码和邻域操作怎么选

禁忌搜索不是另一种路径构造法,而是"在节约算法给的初始解附近系统性地翻找"。解编码直接用 assign 向量,客户 k 的归属中心是 assign(k),路径形态不单独编码,每次评估时由节约算法现场重建。这样编解码成本低,邻域操作只需要动归属关系,不需要维护复杂的路径数据结构。

邻域操作常见有三类:客户改派、客户交换、中心开关。客户改派把某个客户从当前中心移到另一个开放中心;客户交换是两个中心各取一个客户互换;中心开关尝试关闭一个开放中心或者打开一个关闭的中心。毕业设计规模下,全邻域枚举的开销可以接受,我一般只保留改派 + 开关两类,交换操作对解质量的提升有限,却让邻域规模翻一倍。文档说明里可以把这个取舍写成一个对比实验。

4.2 禁忌表、藐视准则和终止条件的工程设定

禁忌表用一个 n x m 的矩阵 tabu(k,d) 记录"客户 k 从当前中心移出的动作还剩几代禁止",中心开关动作单独用一维 tabuDepot 记录。两个表每代整体减 1,新动作写入 opt.tabuLen。这个设计比"记录整个解"的笨办法省内存,也更容易解释。

藐视准则要做,否则容易把所有突破解都挡在门外:当某个被禁忌的移动产生的新代价小于当前历史最优时,解除禁忌放行。终止条件用最大迭代代数 maxIter,配一个"连续无改进代数"提前退出。提前退出条件在 30 客户算例上能省掉三分之一的无意义迭代。

4.3 tabuSearchLRP 主循环代码与参数表

function [bestAssign, bestOpen, bestCost, trace] = tabuSearchLRP(prob, initAssign, opt) % opt.tabuLen : 禁忌长度(代数) % opt.maxIter : 最大迭代代数 n = prob.nCust; m = prob.nDepot; curAssign = initAssign(:)'; curOpen = false(1, m); curOpen(unique(curAssign)) = true; curCost = calCost(prob, curAssign, curOpen); bestAssign = curAssign; bestOpen = curOpen; bestCost = curCost; tabu = zeros(n, m); tabuDepot = zeros(1, m); trace = nan(opt.maxIter, 1); for it = 1:opt.maxIter nbrAssign = []; nbrCost = inf; hint = 0; % 邻域1: 客户改派到其他开放中心 for d = find(curOpen) for k = 1:n if d == curAssign(k), continue; end tmp = curAssign; tmp(k) = d; c = calCost(prob, tmp, curOpen); if tabu(k, curAssign(k)) > 0 && c >= bestCost, continue; end if c < nbrCost, nbrCost = c; nbrAssign = tmp; hint = d; end end end % 邻域2: 中心开关 for d = 1:m tmpOpen = curOpen; tmp = curAssign; if curOpen(d) tmpOpen(d) = false; tmp = reassignToNearest(prob, curAssign, tmpOpen); if isempty(tmp), continue; end else tmpOpen(d) = true; end c = calCost(prob, tmp, tmpOpen); if tabuDepot(d) > 0 && c >= bestCost, continue; end if c < nbrCost, nbrCost = c; nbrAssign = tmp; hint = -d; end end if isempty(nbrAssign), break; end % 执行移动并更新禁忌表 if hint > 0 k = find(nbrAssign ~= curAssign); tabu(k, curAssign(k)) = opt.tabuLen; % 禁止立刻改回原中心 else tabuDepot(-hint) = opt.tabuLen; % 禁止立刻反向开关 end tabu = max(tabu - 1, 0); tabuDepot = max(tabuDepot - 1, 0); curAssign = nbrAssign; curOpen = false(1, m); curOpen(unique(curAssign)) = true; curCost = calCost(prob, curAssign, curOpen); if curCost < bestCost bestCost = curCost; bestAssign = curAssign; bestOpen = curOpen; end trace(it) = curCost; end end

改派动作的关键注释在最后一行:禁忌的不是"把 k 移到 d",而是"把 k 移回原来的中心 curAssign(k)",这样才真正封住了上一步的直接回退。中心开关的禁忌同理,关掉一个中心后,短期内不允许把它重新打开,逼着算法去探索别的组合。

参数的经验取值范围整理成下表,30 客户左右的开随机算例可以直接用中列数值起步。

参数建议范围30 客户常用取值说明
tabuLen5 ~ 1510太短会原地循环,太长限制邻域
maxIter100 ~ 500200后期收益快速递减
提前退出代数30 ~ 6040无改进达到该值即停
藐视准则开/关开启关闭时容易陷在次优解

reassignToNearest 是配套函数:关闭中心 d 后,把原属于它的客户按距离顺序改派到其他开放中心,任一客户放不下就返回空数组,表示该关闭操作不可行。

5. 验证节约算法与禁忌搜索结果的三步检查法

5.1 用收敛曲线判断算法是在搜索还是在摸鱼

主循环里 trace 已经记录了每代当前解代价,画出来是第一张必出图。禁忌搜索的典型收敛曲线是阶梯状下降:平台期对应改派邻域内的局部调整,突然跳降对应一次成功的中心关闭或打开引发的路径整体重构。如果曲线从头到尾是一条直线,说明初始解太差或者禁忌长度把邻域锁死了;如果曲线是锯齿状但 200 代后和初始解一样,多半是藐视准则没生效。

figure; plot(find(~isnan(trace)), trace(~isnan(trace)), 'o-'); xlabel('迭代代数'); ylabel('当前解总成本');

判断标准很简单:前 20% 迭代内成本必须显著低于初始解,后续每次跳降之间不应该出现成本回升超过 5% 的情况。实际工程里,LRP 收敛曲线带一点回摆是正常的,因为关闭中心后客户改派要花几代才能重新排顺。

5.2 小规模穷举对照和 tabuLen 敏感性记录

验证提交结果可靠性的第二个手段是"穷举中心组合对照"。候选中心不超过 5 个时,中心开闭组合只有 2^m 种,对每种组合用第 3 章的节约算法求出该组合下最优路径代价,取全局最小作为参照。禁忌搜索的最终解不应该比这个参照差太多——如果差很多,说明初始分配或禁忌参数有问题。

tabuLen 敏感性实验是文档说明的标准素材,用 5 个随机种子分别跑 tabuLen = 5, 10, 15, 20,记录最好成本和运行时间。

tabuLen最好成本均值(示意)平均运行秒数(示意)
521684.1
1021144.3
1521324.6
2021454.9

表格里的数字是示意性的,不同算例结论不同,但记录格式可以直接照搬。实验做完把数据存成 result_xxx.mat,随源代码一起提交,评审老师就能自己复现。

5.3 结果落盘与文档说明的组织

最后一步是把最优解画成图:每个开放中心一种颜色,画出客户点、路径折线、中心方块,坐标轴等比例。这张配送路线图加上收敛曲线图,正好构成文档说明的核心插图。别忘了把 bestAssign、bestOpen、bestCost、trace 和算例参数一起存进 .mat 文件,命名用日期加客户数加实验编号,例如 result_20250103_n30_len10.mat。源代码里每个函数头部写清输入输出和依赖,calCost 依赖 buildRoutesBySavings 这一点必须在文档的函数调用关系图里标出来。再加上这章里的三步验证记录,这套基于 matlab 的节约算法和禁忌搜索算法 LRP 源代码就真正闭环了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询