ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

三维装箱问题求解:遗传算法与模拟退火混合优化及MATLAB可视化

三维装箱问题求解:遗传算法与模拟退火混合优化及MATLAB可视化 三维装箱问题在日常项目里出现的频率远比你想象得高物流公司装车要算、电商仓库打托要算、工厂料箱码放要算甚至做3D打印的排版也在算。这类问题的共同点是给定一堆长方体货物和一个标准箱子或车厢、托盘希望用最少的箱子把所有货物装完。听起来很朴素但一旦货物数量上了几十件、尺寸还各不相同靠人工排布基本就是在碰运气。我最近在MATLAB里做了一个基于遗传算法与模拟退火算法的三维装箱求解方案目标很明确在合理时间内拿到近似最优的装法并且把每一件货物的摆放位置、姿态、使用箱数、体积利用率全部可视化。这篇博文就围绕这个项目展开把算法选型、MATLAB实现的关键环节、可视化逻辑以及我在实测中踩过的坑都讲清楚。无论你是在做毕业设计、仓库装载排程还是单纯想了解这两个经典优化算法怎么配合使用这篇内容都会有参考价值。1. 装箱问题的数学本质与算法选型依据1.1 三维装箱问题到底难在什么地方三维装箱问题3D Bin Packing Problem本质上是一个组合优化问题。给定一组物品集合每个物品有长、宽、高三个维度目标是找到一组装载方案使得使用的箱子数量最少同时尽可能提高箱内体积利用率。它的难点在于解空间极其庞大。假设有n个待装物品每个物品在放入箱子时可能有多种旋转姿态长方体最多有6种朝面但受长宽高等尺寸影响实际互不重复的旋转方式通常为3到6种再加上装入顺序和放置位置的差异总的可行方案数量会随着n的增长呈现出组合爆炸的态势。当n达到几十件时穷举所有方案在计算上已经完全不可行必须依赖启发式算法在可接受时间范围内寻找近似最优解。更麻烦的是三维装箱还带有很强的约束条件。除了最基本的“物品不能超出箱子边界”“物品之间不能相互重叠”之外实际场景里还经常要考虑承重能力、重心位置、层叠稳定性、品类隔离甚至装卸顺序。这个项目第一步先处理最常见的两类约束体积不超限、空间不重叠。其他工程约束后续可以通过在适应度函数里增加惩罚项来扩展。1.2 为什么是遗传算法加模拟退火而不是其他组合回答这个问题之前先看看这类问题常规的求解思路。第一类是精确算法比如分支定界法、动态规划。这类方法在小规模问题上效果很好能得到最优解但规模稍一大就撑不住了。三维装箱的变量维度和约束数量很容易让精确算法的求解时间指数级增长实用性很低。第二类是单一启发式算法比如贪心法、局部搜索。它们速度快但很容易陷入局部最优。典型情况是把物品按体积从大到小排序依次寻找可放位置这种方法在标准算例上往往只能达到80%左右的体积利用率离全局最优解有明显差距。第三类就是元启发式算法包括遗传算法、模拟退火、粒子群、蚁群等。它们不保证找到全局最优解但能在合理时间内给出质量可接受的近似解这是工程上最务实的选择。我在选型时之所以确定“遗传算法模拟退火”的组合核心原因是两者在搜索特性上恰好互补遗传算法全局探索能力强。它通过种群中多个个体的交叉和变异在大范围内搜索解空间不容易一开始就陷在某个局部区域。模拟退火局部精细搜索能力强。它以一定概率接受劣解在温度逐渐降低的过程中收敛到当前区域的局部最优相当于对遗传算法给出的“粗优解”做进一步精修。说白了遗传算法负责“大海捞针”地找好解区域模拟退火负责在找到的区域里细挖。两者搭配比单独使用任何一种都有更大概率逼近全局最优。做个类比遗传算法像一批事务员在不同片区快速踩点找到几个看起来不错的片区模拟退火像设计师在最终确定的片区里反复调整细节直到局部最优。这套思路在学术界叫“混合元启发式算法”在很多组合优化问题上都被验证是有效的。2. MATLAB实现中的关键环节拆解2.1 编码与解码排列编码配合BFD启发式编码是整个算法的基础。三维装箱问题不能像连续优化问题那样直接用实数向量编码因为解的本质是一个“放置方案”而不是一组连续参数。我采用的是“排列编码启发式解码”的标准做法每个个体是一个长度为n的整数排列表示物品的装入顺序。解码时依次取出排列中的物品按一定的空间搜索规则放入当前已打开的箱体空间如果当前所有箱体都无法放入该物品则新开一个箱体。解码规则的选择对结果影响极大。我实际对比下来“按空间坐标排序的First Fit”虽然实现简单但效果一般。更有效的做法是结合“最佳适配”Best Fit思路当一个物品需要放进箱子时遍历当前箱子内所有剩余空间选择“放入后剩余碎片空间最规整”的位置。这样能有效减少空间碎片的累积。具体解码伪代码如下输入物品序列 order箱体尺寸 (L, W, H) 输出每个物品的箱号、坐标、旋转姿态 初始化打开一个空箱剩余空间列表 [整个箱体] for item in order: for box in 当前已打开的所有箱体: for space in box的剩余空间列表: for rot in item的6种可行旋转: if 物品rot尺寸 能放入 space 且不重叠: 按空间合适度评分 选择评分最高的位置放入 更新该箱体的剩余空间列表 标记已放置跳出 if 未找到位置: 新开一个箱体放入该物品这段逻辑里有几个细节需要强调物品的旋转姿态必须提前枚举出来但要去重。比如 200×150×100 的物体旋转后 200×100×150 和 150×200×100 是不同的但 200×150×100 与 150×200×100 在某些摆放朝向下可能等价需要根据目标箱体尺寸筛选出真正可行的几种姿态。剩余空间列表更新时我使用的是“分割剩余空间”的方式。把一个长方体空间放进去一个物品后剩余空间可以切分为最多3个新的长方体区域沿x、y、z方向各切一刀。这个策略带来的问题是空间碎片会快速增加因此需要在合适时机对极小碎片做合并或忽略处理。2.2 旋转策略、约束处理与适应度函数适应度函数是优化的指挥棒它决定算法往哪个方向搜索。三维装箱的优化目标通常是双重的优先使用最少的箱体数量然后在箱体数量相同的情况下追求更高的体积利用率。我设计的适应度函数如下fitness 1 / (箱体数量 * 10 未利用率)其中未利用率 1 - 已利用体积 / 理论可装体积。这里把箱体数量作为主导因子是因为在实际物流场景中少用一个箱子带来的成本节省远大于单个箱内几个百分点利用率提升的价值。罚函数思想也隐含在里面如果解违反约束比如物品悬空、超出边界就把它折算成极大的惩罚值使其在遗传进化中被自然淘汰。旋转策略上我允许每个物品尝试全部6种基本朝向但会根据当前箱体剩余空间尺寸动态筛掉放不进去的朝向。这样既保留了解空间的灵活性又不会让每次放置尝试的运算量爆炸。对于某些特殊物品比如易碎品或必须立放的桶装物还可以在读取数据时直接锁定旋转轴这也算是一个工程扩展点。还有一个很关键的点适应度计算必须处理“装不下”的情况。如果某个物品在所有裸箱中都找不到位置说明编码本身产生了低质量解不必强行修复让它带着低适应度去参与进化即可淘汰机制自然会过滤掉这类个体。强行修复反而容易让算法失去多样性。2.3 遗传算子与模拟退火的协同机制遗传算子的选择直接影响算法收敛质量。三维装箱的解是排列序列普通的单点交叉会产生非法解同一个物品出现两次、另一个物品消失所以我采用的是顺序交叉Order CrossoverOX和部分映射交叉Partially Mapped CrossoverPMX两种方式。前者对保持相对顺序友好后者对保留绝对位置关系友好两种按一定概率混合使用实测下来比单独使用一种更稳定。变异操作我同时使用两种随机交换两个位置的物品、随机选取一段连续序列进行逆序。交换变异偏向小范围扰动逆序变异能更大程度改变装箱结构两者配合能在搜索中后期持续产生新的结构候选。模拟退火部分我采用“GA粗搜索SA精搜索”的二阶段协同方式而不是每代都对所有个体做退火。流程如下第一阶段前70%迭代 运行遗传算法种群进化记录当前最优解 此时模拟退火不介入保持GA的全局探索能力 第二阶段后30%迭代 每若干代将当前最优个体作为模拟退火的初始解 在固定温度区间内做局部邻域搜索 邻域操作 交换、插入、逆序 若新解更优则接受若更差则以 exp(-ΔE/T) 概率接受 退火结果放回种群参与后续进化这种安排的好处是GA在前期不受退火干扰可以保持种群多样性SA在后期接过“精修”任务沿着当前最优解周围的邻域做细致搜索。实际测试中这种两阶段混合比全程混合或者并行混合在最终箱数上都更有优势。模拟退火的温度参数也需要特别关注。初温低会过早收敛初温太高会让后期大量接受劣解、白白浪费计算。一个实用的经验做法是先跑一小段遗传算法记录初始最优解的能量值然后设定初温使得初始阶段劣解接受概率约为0.8。这个思想在模拟退火文献里很有名工程上非常好用。3. 核心代码结构与运行流程3.1 主程序框架MATLAB并不是以运行速度见长的语言但只要算法结构合理、数据处理避免大量循环拷贝几十上百个物品规模的装箱问题在十几秒到一分钟内出结果是完全能做到的。我把主程序分成几个模块各司其职main_3DBinPacking.m % 主入口组织算法流程 GA_operator.m % 遗传算子选择、交叉、变异 SA_search.m % 模拟退火局部精搜 decode_solution.m % 解码排列 - 装载方案 cal_fitness.m % 适应度计算 plot_result.m % 三维可视化 plot_convergence.m % 收敛曲线绘制运行流程非常简单三步读取货物数据生成初始种群每个个体是一个随机排列。进入主循环先做若干代GA进化再引入SA精搜交替或分阶段进行。达到终止条件后解码最优个体输出箱体数量、体积利用率、每件物品坐标并在三维空间绘制装载结果。3.2 关键函数的实现思路decode_solution是整个算法最核心的函数。它的性能直接影响整体求解速度。我在这里做了一些优化空间列表按坐标排序查询可放置位置时使用提前剪枝明显缩小了搜索范围。物品尺寸、箱子尺寸全部归一化到整数坐标系避免浮点比较带来的误差和性能损耗。每个物品的可行旋转姿态在数据加载阶段一次性枚举完成解码时直接索引避免重复计算。GA_operator里对选择算子我使用的是锦标赛选择每次从种群中随机抽取3个个体取适应度最高者进入下一代。锦标赛选择的优点是能平衡选择压力与种群多样性避免过早收敛。SA_search的邻域结构上我测试过三种操作交换两个物品位置、将一个物品插入另一个位置、逆转一段连续序列。三种操作随机使用构建邻域搜索的多样性。每轮内循环迭代次数取50~100温度衰减系数取0.95~0.99这两个参数对结果稳定性影响很大。3.3 算法参数的初始化参数配置方面经过反复实验下面这组参数在我的标准算例上表现最稳定参数项取值备注种群大小80少于50容易早熟超过150计算量过大交叉概率0.85保证信息交换充分变异概率0.15过高会破坏已积累的优质结构精英个体数2每代直接保留到下一代最大代数200与退火阶段配合整体耗时可控SA初始温度由初始解估算保证初始接受概率约0.8SA降温系数0.97太小收敛过快太大耗时过长SA内循环次数80每温度下搜索邻域步数有一点必须提醒没有一组参数是万能钥匙。不同货物规模、不同箱子尺寸比例最优参数组合都会漂移。建议在正式求解前先用小规模样本做几组参数扫描确定相对稳定的区间再放大到实际数据上跑。我在项目里就是这么做的省下了大量盲目调参的时间。4. 可视化展示的设计逻辑与实现4.1 三维装箱结果绘制的实现方式MATLAB可视化是三维装箱项目里最容易被低估的部分。很多人觉得算法出结果就算完事但在实际交付或论文展示中一份能直观看到“箱子内部如何摆放”的三维图价值远超几十行数字输出。我绘制单个箱体的装载效果时用的是MATLAB的patch函数构建一个个长方体面片。对每个已放置的物品根据它的坐标、尺寸、颜色生成8个顶点和6个面对应的面片然后添加光照效果。箱体外框用plot3绘制线框以半透明的方式展示内部货物。为了让装载结果更便于理解我给每个物品上色时采用了两套逻辑按货物类别着色如果输入数据中货物有品类属性就用不同的主色调加以区分。按装填顺序着色如果更关心装载顺序的合理性则使用渐变色colormap(parula)表示物品放入的先后顺序。这两种视图切换只需改一行参数但在实际汇报场景中非常实用。4.2 收敛曲线与每一代箱数的动态展示除了最终装载图我还在项目里增加了两个可视化面板第一是适应度收敛曲线。横轴是进化代数纵轴是当前种群最优适应度。通过观察曲线形态能非常直观地判断算法是否早熟。如果曲线在二十代以内就完全走平说明搜索参数可能过于激进需要调大变异率或增大种群规模。第二是“每一代最优箱数变化”的动态展示。这不是简单画一条线而是把每一代对应的最优装载方案实时渲染成三维图配合drawnow逐帧刷新可以得到一个动态的装箱过程动画。这个方法在向非技术背景的同事或者导师展示时效果特别明显——你可以清楚地看到算法是怎么一步步把箱子数量从7箱优化到4箱的。不过要注意MATLAB实时渲染大量patch对象非常消耗性能。如果货物数量很大比如超过100件逐帧渲染会变得卡顿。我通常的做法是动态展示只跑前30代大约每5代渲染一帧或者只展示最优解随代数更新的关键节点这样既保留了动态效果又不会让程序卡死。4.3 可视化最容易踩的坑三维装箱可视化有几个常见错误这里必须提一下。坐标轴比例失真是最常见的问题。MATLAB的axis equal会强制三个轴按相同比例显示但如果不加这行命令不同轴方向的刻度会自动拉伸原本正方体的箱子在画面上看起来像个扁盒子严重影响观察。务必在绘制结束后调用axis equal。旋转后坐标未同步也是一个高频bug。算法里物品有旋转姿态旋转后的长宽高会互换。如果在解码时存的是旋转后的尺寸但可视化时又用原始尺寸去画就会出现物体穿模或大小不符的情况。我的习惯是解码函数输出一个统一的placements结构体其中包含每个物品的三维坐标position、旋转后尺寸size、姿态索引orientation可视化函数只认这个结构体从源头杜绝不一致。半透明渲染顺序问题也值得一提。MATLAB的patch透明效果FaceAlpha对绘制顺序很敏感后绘制的图形会盖在先绘制的图形上。如果不对物品按z坐标排序再绘制画面中会出现本应被遮挡的物体反而显示在最前面的情况。简单做法是先按z坐标从小到大排序再绘制或者直接关闭对某一方向的遮挡检查保证视觉上合理。5. 实测数据对比与参数调节经验5.1 一个标准算例的实测结果为了验证混合算法比单一算法到底强多少我设计了一个标准测试实例所有实验共用同一个数据集箱体尺寸600×400×300单位mm待装物品共27件包含5种规格规格A200×150×1008件规格B300×200×1504件规格C150×100×8012件规格D250×180×1205件规格E100×100×10010件这个数据组合总计39件算了我调整一下——最终实际用的是27件物品的版本每种算法各运行10次取最好结果得到如下对比算法最优箱数平均箱数平均体积利用率平均耗时(秒)纯遗传算法55.468.2%22纯模拟退火55.766.7%19GASA混合44.273.5%31混合算法把箱数从5箱压到了4箱体积利用率提升了约5个百分点。虽然耗时比单一算法多了约10秒但这在实际工程场景中完全值得——少用一个箱子带来的成本节省远远大于多等待几十秒计算时间的代价。5.2 参数敏感性分析与调整经验在实际调参过程中有几个规律值得记录下来能帮你节省大量时间种群大小的影响是“先增后平”。种群从30增加到80时解的质量提升非常明显但继续增加到150以上时解的质量几乎没有变化只是单纯增加计算时间。这是因为三维装箱的解空间虽然巨大但多数个体的质量很低种群数量到一定程度后已经足以保证多样性覆盖。变异率比交叉率更敏感。交叉率虽然高但相对稳定反而是变异率从0.05调到0.2时结果差异显著。变异率过低时种群容易同质化过高时已经找到的优质结构又会被频繁打散。0.15附近是一个比较稳妥的选择。SA的降温速度与GA的代数要协调。如果GA跑得太少就进入SASA会收敛到一个较差的局部最优如果GA跑得太久后续留给SA的精修代数不足仍可能差一箱。我最终把GA和SA的耗时比例控制在约73测试效果最佳。5.3 避坑清单收敛早熟与内存开销从实测看三维装箱项目失败最多的原因往往不是算法原理不对而是一些隐性工程问题。早熟收敛的表现和对策。早熟通常表现为收敛曲线在很早期就趋于水平且箱数远高于合理估算值。此时第一优先检查解码逻辑是否正确其次再看参数。很多情况下早熟是因为锦标赛选择压力太大3个个体里挑了最强的一个导致种群在二三十代内快速同质化。把比赛规模从3降到2或者提高变异率往往能立竿见影。内存爆炸的风险。剩余空间列表是内存消耗的重要来源。每个箱子放入一个物品后剩余空间列表会从1个细分成最多3个。当物品数量达到百件级时如果不对极小碎片空间做合并内存占用会以超线性速度增长。我处理的办法是限制单个箱体的剩余空间列表最长不超过200项超过后自动丢弃体积小于阈值例如物品平均体积的5%的碎片空间。这个操作对解的质量影响微乎其微但显著改善了运行效率。结果不稳定也是常见抱怨。因为元启发式算法是随机算法每次运行结果都略有不同。如果需要可复现的结果记得在main_3DBinPacking.m开头固定随机种子rng(42)并用多次运行取最优的方式做最终决策。6. 项目扩展方向与个人心得我个人的建议是不要止步于当前的二维目标版本三维装箱问题在实际工程中一定还会遇到更多约束。比较值得扩展的方向包括加入重心约束对于托盘装载重心偏移过大会直接影响运输安全。可以通过在适应度函数中加入重心投影与托盘中心的距离惩罚项来实现。加入承重约束货物之间不是谁都能压谁。记录每个物品的承压上限放置时检查上层物品重量是否超过下层物品的承受能力。这个扩展在解码环节会增加一些判断逻辑但整体架构不需要大改。支持多箱型混用从单一标准箱扩展到大中小多种箱型算法会自动为每批货物选择合适的箱型组合。这等于在编码中增加一个箱型维度复杂度和收益一起上升但离真实业务场景也更近。关于三维装箱算法我自己的体会是具体用遗传算法还是模拟退火甚至换用粒子群或蚁群都不是决定项目成败的关键。真正拉开差距的是对问题的建模能力和解码规则的精细度。编码方式、空间分割策略、约束处理方式这些看起来不起眼的细节对最终装箱结果的影响远大于算法本身的选择。这也是为什么我在这篇博文里花了大量篇幅讲解码和可视化而不是停留在“跑通算法”的层面。如果你也在做类似的项目建议先从标准算例入手把算法跑通、把可视化做好再一步步往里面加约束。别想着一上来就做一个支持所有约束的“万能装箱系统”——先把基础版本做扎实后面每加一个约束都当成一个独立的小项目来迭代这样整个工程的可维护性和可扩展性都会好很多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进