只要做过物流网络优化的朋友应该都有同一种体会纸上谈兵的最短路径算法放进真实业务里往往不堪一击。多式联运尤其明显——一批货从内陆枢纽到沿海港口中间可能换公路、铁路、水路每一种方式的运输时间不是一个固定数而是随天气、拥堵、班期波动的随机值换装节点还有等待成本和延误风险。这种不确定性让“找一条最便宜路径”变成了“找一条在各种波动下都能稳住的路径”。我前阵子用MATLAB做了一套不确定多式联运路径优化仿真分别用AFO人工花优化、GA遗传算法、PSO粒子群算法求解同一个问题再和MATLAB自带的全局优化搜索器做结果对比。整个过程我会把模型、代码、对比细节都拆开讲清楚如果你正在做类似的算法实验、写论文或者准备物流系统的决策模块这篇分享可以直接给你参考。1. 不确定多式联运问题到底“不确定”在哪模型变量与约束拆解1.1 运输方式链路的表示节点、弧段与换装先聊建模。多式联运的物理网络可以抽象成一张有向图节点是货源地、中转枢纽和目的地边是各个节点之间可以通行的运输弧段。与普通最短路径问题的区别在于每一条弧段上都可能同时存在多种运输方式公路、铁路、水路各有各的时间、成本和容量属性而且货物在节点处从一种方式换到另一种方式还要额外付出换装成本和换装时间。决策变量就有两组。一组是“路段方式选择”比如 (x_{ij}^{m}1) 表示从节点 i 到节点 j 选择运输方式 m另一组是“换装选择”比如 (y_i^{mn}1) 表示在节点 i 处从方式 m 换到方式 n。目标函数通常是总成本最小或者总时间最短更多时候是做一个加权目标把成本和时间压成一个综合费用。真正让问题变复杂的是约束。每个节点有容量限制某些枢纽只能承接指定的换装方式货物可能有时间窗必须在某个时段内到达运输公司还会限制全程换装次数因为每换一次装就增加货损和延迟风险。这些约束叠加在一起解空间一下就膨胀了。1.2 不确定性的来源与处理方法不确定性主要来自两块运输时间和运输成本。公路运输受路况和天气影响铁路运输受班期和调度影响水路运输受水位和港口拥堵影响这些都不是一个固定值。更麻烦的是不确定参数之间还可能相关比如暴雨同时拉高公路时间和仓储成本。处理这种问题学术界常见的思路有三种。随机规划把参数看成服从某个概率分布的随机变量优化目标变成期望值鲁棒优化把参数看成一个不确定集合找的是最坏情况下也能接受的解模糊规划则用隶属度函数描述人的经验判断。但在实际写MATLAB代码时我推荐先用蒙特卡洛模拟配合智能算法。具体做法是预先从历史数据拟合出每个弧段时间和成本的分布在评价一个方案时抽样多次用这些场景的平均目标值作为适应度。这样做最直观也最容易和GA、PSO、AFO这些启发式算法结合。如果想让解更稳健还可以在目标函数里引入保守程度参数。比如评价某个方案时不取平均值而是取“最差的20%场景的平均值”这样得到的解会主动避开那些极端波动下特别吃亏的路线。这个思路在业务上非常实用因为实际决策者关心的往往不是“平均情况”而是“万一出状况能不能兜住”。2. AFO、GA、PSO的求解逻辑对比为什么需要用三种算法互相验证2.1 遗传算法整数编码、交叉变异与不可行解修复GA大家比较熟在多式联运里最自然的做法是整数编码一条染色体就是一条运输方案的路段方式向量每个基因位取值为运输方式编号。比如一条路径有7段弧染色体可能是 [1, 1, 3, 2, 2, 3, 1]代表第1段公路、第2段公路、第3段水路以此类推。GA的核心是选择、交叉、变异。交叉我用两点交叉随机选两个交叉点交换两个父代中间段的基因片段。变异则是随机选一个基因位把它替换成当前弧段上其他可用的运输方式。这里有第一个容易忽略的坑交叉和变异之后可能出现“某段弧实际上不允许走某种方式”的情况。处理办法有两种一种是在生成初始种群和杂交时只从该弧段的可用方式集合里选另一种是对不可行个体加惩罚。我更推荐前一种这样可以大大减少无效搜索。适应度函数里要处理时间窗和换装次数。如果方案超过时间窗不能直接扔掉因为扔掉会损失多样性。我会在目标函数中加一个惩罚项惩罚系数一开始设为目标量级的10%左右如果实验中发现不可行解比例太高就逐步加大。这个系数需要反复试太松会得到不可行解太紧会让算法只敢选保守方案错过更优解。2.2 粒子群算法离散问题中的位置更新策略PSO原本是为连续优化设计的直接用标准的速度-位置更新公式处理离散的方式编号会出问题因为两个“方式编号”相加减没有意义。我在项目里常用的离散PSO做法是把粒子位置定义成一个整数向量 (X_i)每个维度仍然是路段方式编号速度向量 (V_i) 的每一维表示“该维度发生改变的概率”通过sigmoid函数映射到0到1之间。更新时先按标准PSO公式算速度[ V_{i,d}^{t1}wV_{i,d}^{t}c_1 r_1(P_{i,d}-X_{i,d})c_2 r_2(G_d-X_{i,d}) ]然后对每一维生成一个0到1之间的随机数如果它小于sigmoid(V)就把该维的运输方式改成当前最优方式否则保持原值。需要注意这里的减法和乘法不是数值相减而是“是否一致”的比较逻辑。惯性权重w我用线性递减从0.9降到0.4前中期保持全局搜索能力后期加强局部收敛。c1和c2通常都取1.5左右。在多式联运问题里PSO的一个弱点是容易早熟尤其是当某个局部最优的适应度远好于邻域解时粒子会迅速靠拢。所以后来我在速度更新里加了一点随机扰动让粒子有一定概率翻转到非最优方式效果比原版稳定不少。2.3 AFO算法的群体行为机制AFO这个名字在不同文献里对应的全称不完全一样我这里按代码里实际实现的版本理解是Artificial Flora Optimization人工花优化算法。它模拟的是花朵群体通过传粉和萌发来寻找最优生长位置的过程整体框架和GA、PSO一样都是“种群迭代”但解的更新逻辑很不一样。AFO里每个个体是一朵花位置就是一个候选解。每轮迭代时花朵会通过两种方式产生新位置一种是大范围传粉受全局最优位置引导负责全局勘探另一种是局部萌发在自己的小邻域内变异负责局部开发。花朵根据新位置的适应度决定是否移动相当于一个带精英保留的随机搜索过程。用在多式联运问题上编码方式可以和GA完全一致这样三个算法能共用同一个目标函数对比才公平。AFO的一个突出优势是参数少核心就是传粉概率和邻域变异幅度。我实际跑下来它在中等规模网络上的稳定性比GA和PSO都要好不太容易出现“某一代突然崩掉”的情况。2.4 为什么要三种算法一起跑单算法跑出来的最优解你永远不知道是全局最优还是碰巧好。组合优化问题的局部最优特别多换一个随机种子结果可能差一大截。我在这个项目里坚持用三种算法对比核心目的有三个第一看它们最终收敛到的目标值是否接近如果三个算法都指向同一个区间那么这个解的可信度会高很多第二看各自的收敛速度和稳定性这能帮助判断哪个算法更适合这个规模的场景第三为后续写论文或评审时提供更充分的实验依据单算法的实验说服力确实不够。3. MATLAB建模仿真的完整流程从数据表设计到算法循环3.1 网络数据与不确定参数的生成我在代码里把数据生成独立成一个脚本方便换网络实例。先固定随机种子再生成基础期望矩阵和波动范围矩阵。下面这段是一个简化的例子rng(42); N 8; % 节点数 M 3; % 运输方式数1公路2铁路3水路 % 期望运输时间矩阵baseTime(i,j,m) baseTime rand(N, N, M) .* 5 1; % 波动比例矩阵rangeTime(i,j,m) rangeTime rand(N, N, M) .* 0.4 0.05; % 运输成本矩阵 baseCost rand(N, N, M) .* 10 2; % 允许矩阵值为1表示i到j可用方式m0表示不可用 allow randi([0, 1], N, N, M); % 强制起点到终点必有可行路径这里人工设置一条链 for i 1:N-1 allow(i, i1, :) 1; endbaseTime里的每个数值是期望时间真实运输时间我这样模拟(T_{ij}^{m}baseTime_{ij}^{m}\times(1rangeTime_{ij}^{m}\times randn))。成本也类似。用这个方式可以把时间波动和成本波动叠加到方案评价中。这里有一个非常重要的点如果每次评价方案时都用randn现场抽样那么同一个方案两次评估的目标值可能完全不同算法会分不清“解变好了”还是“随机扰动变小了”。所以我在实验开始时预先生成一组扰动场景比如一百个时间扰动矩阵评价函数每次从中抽一组固定的场景来算平均值。这样不同算法、不同个体之间比较的基础是一致的。3.2 目标函数与约束的实现目标函数是三个算法的核心交汇点。我建议把所有约束都封装在一个函数里输出一个标量适应度算法只需要负责最小化它。示例骨架如下function f planCost(x, Data) % x : 路段方式序列比如 [1 1 3 2 3] % Data : 包含所有基础数据、允许矩阵、时间窗、惩罚系数等 totalTime 0; totalCost 0; prevMode x(1); for i 1:length(x) segmentTime sampleTime(Data.baseTime(i, i1, x(i)), ... Data.rangeTime(i, i1, x(i)), Data.scenario); totalTime totalTime segmentTime; totalCost totalCost Data.baseCost(i, i1, x(i)); if i 1 x(i) ~ prevMode % 换装时间与换装成本 totalTime totalTime Data.transitTime(prevMode, x(i)); totalCost totalCost Data.transitCost(prevMode, x(i)); prevMode x(i); end end % 时间窗惩罚 if totalTime Data.TimeWindow totalCost totalCost Data.penalty * (totalTime - Data.TimeWindow); end % 加权目标 f Data.wCost * totalCost Data.wTime * totalTime; end每次换装前还要检查换装允许矩阵如果某种换装组合不允许直接把目标函数设为一个很大的数。这样做虽然粗暴但能让算法慢慢避开这些不可行区域。3.3 三种算法的循环结构三种算法我都写成了独立函数输入都是“评价次数上限”和“数据对象”输出最优解和收敛曲线。这样做的最大好处是公平所有算法都在同样的评价预算下运行没有谁多跑几千次。GA部分我用锦标赛选择每轮随机抽两个个体保留适应度好的然后按概率交叉变异。PSO部分按离散粒子群方式更新位置。AFO部分则是生成全局授粉候选解和局部萌发候选解择优保留。下面是一个AFO主循环的伪代码框架for t 1:maxIter for i 1:popSize % 全局授粉候选解以一定概率转向全局最优 candidate1 globalPollination(X(i,:), Gbest, probPoll); % 局部萌发候选解在邻域内随机变异 candidate2 localGermination(X(i,:), neighborRange); % 择优更新 if planCost(candidate1, Data) planCost(X(i,:), Data) X(i,:) candidate1; end if planCost(candidate2, Data) planCost(X(i,:), Data) X(i,:) candidate2; end % 更新全局最优 end endAFO的传粉概率我一般设置在0.4到0.6之间邻域变异幅度随迭代次数逐渐减小前期让它多探索后期让它精细搜索。这个过程不需要太多参数却能保持很强的跳出局部最优的能力。3.4 一个容易踩的坑随机数与不确定性的耦合这是我这次实验里最深刻的教训。第一版代码里我直接在planCost里调用randn实时采样来模拟不确定性跑出来的收敛曲线上下乱跳三种算法的排名每次都不一样。后来检查才发现GA某一次表现好完全是因为它碰巧抽到了一组很小的扰动值并不是算法本身更强。解决方案很简单在实验开始前把每个弧段的波动因子一次性生成好放在Data.scenario里。评价函数每次计算时从这批预生成场景中随机抽取若干个求平均。比如有100个场景每个方案抽取20个场景的平均值这样既能保留随机性又能让评价结果在一个可接受的方差范围内。对比实验里更要小心三种算法必须使用同一份场景集合否则所谓对比只是统计噪音的对比。统一场景后我发现GA和PSO的差距明显缩小AFO的稳定性优势反而更突出了。4. 与MATLAB自带全局优化搜索器的对比实验设计、指标与结论4.1 MATLAB全局优化搜索器是什么GlobalSearch的适用边界MATLAB自带了一个全局优化工具箱里面有GlobalSearch和MultiStart这两个全局搜索器。它们的思路是从多个起始点出发各自调用局部优化求解器试图覆盖更多局部最优区域最终返回最好的那个解。听起来很厉害但要注意它的适用边界。这套工具是为连续变量和混合整数非线性规划准备的而多式联运的决策变量是离散的运输方式编号属于典型的组合优化问题。硬要让它求解要么把整数变量做二进制展开要么用连续变量近似这会让问题的维度和非线性程度急剧上升搜索器经常在一个不可行区域附近反复尝试。不过我还是建议做这个对比理由很简单第一很多人潜意识里认为“软件官方自带的优化器一定比普通启发式算法靠谱”我需要用实验验证这个假设第二全局搜索器的结果可以作为一个独立的参考基线帮助评估GA、PSO和AFO的解质量。4.2 对比实验设计相同评价次数、相同约束为了让对比尽量公平我给所有算法设定相同的评价次数上限比如两万次。数据实例用同一个随机场景集合也相同目标函数完全一致。每个算法独立运行20次记录最优值、平均值、标准差、可行解比例和平均耗时。下表是我在模拟项目上得到的一组数据形态数值仅用于演示实际结果会随着网络规模和扰动强度变化算法最优总费用平均总费用标准差可行率平均耗时(s)AFO156.3160.23.898%12.4GA158.7164.56.295%14.1PSO155.9162.15.096%11.8GlobalSearch172.6189.018.780%22.6最直观的结论是全局搜索器在这个离散组合问题上的可行率只有80%标准差也明显偏大它在某些初始点下可以找到还不错的解但换一个初始点就容易陷入不可行区域。GA和PSO各有千秋AFO在标准差和可行率上表现更好平均耗时也更短。4.3 结果解读与对比结论从收敛曲线看GA的特点是前期收敛很快所有个体快速向一个较优区域靠拢但后期容易停滞因为种群多样性下降交叉很难产生超出当前范围的新解。PSO在迭代中期表现稳定但偶尔会集体陷进一个局部最优需要靠惯性权重的随机扰动才能跳出来。AFO则因为同时保留了全局授粉和局部萌发两条路径在后期还能维持一定的探索能力所以标准差最低。GlobalSearch的表现最不稳定但对这个结论要客观看待它原本不是为离散变量设计的强行对比本身对它不公平。可换个角度想做算法对比研究的价值恰恰在这里——你要向别人证明自己用的启发式算法有价值找一个并不擅长的官方工具当对手是没意义的要对比就对比大家都认可的基线和实际业务里真正会被采用的工具。我在这次实验里得到的最终判断是如果问题规模小、变量少GlobalSearch可以做个快速验证但如果网络节点一多、扰动一强用自带搜索器做决策模块的核心求解器是行不通的还是得靠GA、PSO、AFO这种专为离散组合问题设计的智能算法。5. 附带的MATLAB代码如何跑通与扩展使用指南与避坑经验5.1 代码包结构与运行入口代码包的核心文件包括main.m主入口、data_gen.m数据生成、planCost.m目标函数、ga_solver.m、pso_solver.m、afo_solver.m、global_compare.m调用MATLAB自带搜索器的对比脚本。入口文件main.m的工作流程是先生成数据再调用三个算法函数分别求解最后画收敛曲线并输出对比表。运行条件很简单MATLAB基础环境即可只有global_compare.m那一部分需要全局优化工具箱如果你没安装可以跳过这个脚本不影响前三个算法的运行。我建议第一次运行时先保持默认参数把main.m完整跑一遍确认环境没问题后再逐步修改数据。如果你用的是较新的MATLAB版本全局搜索器一类的函数名可能会略有变化遇到“Undefined function”报错时优先检查工具箱是否安装而不是急着改代码。5.2 调参建议与实验扩展方向参数表是我反复实验后比较稳的起点参数建议值说明种群规模50~80太大会拖慢速度太小容易早熟最大迭代次数200看收敛曲线是否还有下降趋势GA交叉概率0.85组合优化里交叉是主要搜索手段GA变异概率0.10变异过少会损失多样性PSO惯性权重0.9线性递减到0.4前期探索、后期收敛PSO学习因子c1c21.5经典取值效果稳定AFO传粉概率0.5控制全局搜索与局部搜索的占比场景采样数20越大评价越稳但耗时增加如果你要扩展问题规模比如从8个节点扩到30个节点建议先小规模跑通再加维度。直接上大网络会碰到两个问题一是评价单次方案的时间变长二是GA和PSO的收敛速度显著下降这时候AFO的参数优势会更明显。5.3 一个典型的报错与修复最后分享一个我在不同版本MATLAB上遇到的报错当我在ga函数里加了整数约束时提示说“Integer constraints are only supported for...”。这是因为MATLAB自带的ga在启用整数约束时会切换到专用混合整数求解器对目标函数的平滑性要求更高。多式联运问题里连换装成本都有不连续性经常导致求解失败或结果极差。解决办法是绕开MATLAB自带的ga整数接口改用我前面说的自写整数编码方式。自写算法虽然代码多几百行但对目标函数没有附加要求什么惩罚函数、不规则约束都能塞进去自由度大很多。这也是为什么我在这套代码里没有直接依赖ga、particleswarm这类工具箱函数来求解多式联运只把它们放在GlobalSearch对比脚本里。写到最后分享一个我的个人判断。对我自己来说最影响仿真可信度的不是算法本身而是不确定场景的生成方式。你选的分布、采样数、是否固定种子几乎决定了三种算法的相对排名。所以如果你只是想把代码跑完出个图那很快但如果想让结果经得起评审或业务验证我建议把时间多花在数据生成和场景设计上先做确定性版本再逐步加入不确定性看到每一步的变化。这套MATLAB代码是验证模型和对比算法的工具你可以直接改数据、改权重、扩节点把它变成自己的版本。