柔性作业车间调度(FJSP)这几年在企业数字化和智能制造的项目里出现的频率越来越高,原因是生产线柔性的需求一上来,传统作业车间调度的模型就扛不住了。做这个方向的研究或者实际落地,最绕不开的一步就是“用启发式算法去搜一个高质量调度解”。最近我花了不少时间把蛇鹫优化算法(Secretary Bird Optimization Algorithm,SBOA)应用到了FJSP上,写了一套Matlab代码,折腾过程中踩了一些坑,也沉淀了一些能直接用的经验和代码套路。这篇文章就把整个思路、原理、编码设计、Matlab实现、实验调参和问题排查从头到尾理一遍,希望能帮到正在做FJSP论文复现或实际项目选型的读者,尤其是那些想在标准测试实例上快速拿到对比结果的同学。
这类项目看起来是个算法应用,实际上真正花时间的部分往往不是算法本身,而是问题建模和编码解码。SBOA作为一个最近几年提出的新型元启发式算法,机制相对新颖,处理连续优化问题表现不错,但FJSP是个典型的离散组合优化问题,直接套用位置更新公式是行不通的,必须把算法搜索机制和FJSP的解空间表达结合起来设计。下面我按照“问题→算法→建模→实现→实验→排错”这条主线,把整个项目拆开讲。
1. FJSP到底难在哪:从车间的真实约束说起
1.1 为什么说FJSP是作业车间调度的升级版
传统作业车间调度问题(JSP)里,每一道工序的加工机器是固定的,调度员只需要决定同一台机器上多个工序的执行顺序。而柔性作业车间调度问题(FJSP)打破了这台机器固定的限制,允许每道工序在多台可选机器上加工,而且不同机器的加工时间还不一样。多了一个“机器选择”维度之后,问题复杂度大幅攀升,这类问题已经被证明是NP-hard问题,规模稍微一上来,精确解法就没戏了,只能靠智能优化算法在合理时间内逼近优质解。
对于实际生产环境,柔性车间到处都能看到。比如一条生产线上的车削、铣削、磨削可能由几台通用设备完成,同样的孔可以由数控铣床加工,也可以由加工中心完成。柔性带来的好处是设备利用率高、排产灵活度高,但排产难度也成倍增加。
FJSP的决策任务拆开来看其实就两件事:第一,为每一道工序选择一台合适的加工机器;第二,在每台机器上安排所有工序的执行顺序。这两个决策互相耦合,机器选得不好,后面顺序怎么排都可能是瓶颈机器过载;顺序排得不好,再好的机器分配也会变成设备等待浪费时间。
1.2 数学建模:怎么用一个模型描述整个车间
把FJSP写成数学模型,是做任何算法设计和代码实现之前的必备功课。虽然代码里不一定直接求解这个模型,但模型的约束和决策变量必须完整映射到编码里。
假设有n个工件,总共有N个工件需要加工,每个工件J_i由一道或多道工序O_{i,j}组成,工序之间有严格的前后顺序约束。车间里有m台机器M_k,每道工序可以在一台或多台机器上加工,对应不同加工时间p_{i,j,k}。如果某台机器不能加工这道工序,就把这个时间设置为无穷大。
目标函数通常选最大完工时间(makespan),也就是从第一个工序开始加工到所有工序全部完成的时刻,记作C_max。约束条件主要包括:
- 工序顺序约束:同一工件的下一道工序,必须等上一道工序完成之后才能开始。
- 机器唯一性约束:同一台机器在同一时刻只能加工一个工序。
- 加工时间非负和决策变量的0-1约束。
决策变量是两个0-1变量:x_{ijk}表示工序O_{ij}是否在机器k上加工,y_{ijk, hlk}表示工序O_{ijk}和O_{hlk}是否在同一台机器k上存在先后关系。
整个数学模型写下来大概有几十行,但核心逻辑相当紧凑,就是“选机器、定顺序、保约束、极小化C_max”。写代码的时候,这些约束就变成了解码函数里的“机器时间表”和“完工时间计算”,后续我会详细说。
1.3 精确解法的天花板与启发式算法的用武之地
遇到FJSP,很多初学者第一反应是用CPLEX、GUROBI这类求解器去跑整数规划模型。说实话,中小规模实例、比如Kacem系列的8x8、10x10,求解器确实能在几秒或几十秒内找到最优解。但一旦到了Brandimarte系列Mk01到Mk10这种规模,求解器的求解时间会迅速膨胀,普通电脑可能跑几个小时都出不了高质量可行解。
所以业界和研究圈的主流做法,是用元启发式算法在可接受时间内找近似最优解。从早期的遗传算法、粒子群优化、模拟退火,到后来的灰狼优化、鲸鱼算法、麻雀搜索,再到最近几年出现的各种新型算法,本质上大家都在做同一件事:在一套精心设计的编码规则和搜索机制下,尽快逼近已知最优解或下界。
SBOA进入这个领域,理由也很直接:它发表时间相对较晚,很多对比实验中还没有被充分研究过,容易出创新点;而且它的搜索结构带有明显的“探索—开发—局部扰动”三段式,天然比较适合处理需要平衡全局搜索和局部精调的离散优化问题。
2. 蛇鹫优化算法:捕蛇者的猎杀策略与搜索逻辑
2.1 蛇鹫的习性是怎么变成算法机制的
蛇鹫是一种生活在非洲草原的大型猛禽,捕蛇非常厉害。它捕猎分两个阶段:先是长途跋涉在草原上搜索蛇的踪迹,这对应算法里的全局探索阶段;发现目标之后,它会发起攻击,或者直接从空中俯冲下来扑杀,这对应算法里的局部开发阶段。
更关键的是,蛇在被攻击时不会老老实实就范,会做三种逃生反应:伪装自己、快速逃跑、突然倒地装死。蛇鹫针对这三种情况有三种应对策略。这种“猎物逃跑—捕猎者应对”的对抗机制,放到算法里就变成了额外的扰动算子,用来防止种群过早陷入局部最优。
这种从捕食行为中抽象出来的模式,和灰狼优化、飞蛾扑火这类算法的设计思路一脉相承,都是把自然界里的对抗或协作行为转化成数学更新规则。
2.2 两阶段搜索框架:先大范围找,再集中突破
SBOA的位置更新大致分成探索和开发两个阶段,两个阶段不是随机切换,而是根据迭代进度按概率切分。迭代初期以探索为主,保证种群在搜索空间里大面积铺开;随着迭代推进,搜索逐渐转向开发,种群开始围绕当前最优区域精细搜索。
探索阶段的位置更新公式,本质上是借鉴了差分进化里的变异思想。文献里的典型写法是:当前解加上向量差分,再接一个随机扰动项。这个向量差分可以提供方向信息,随机扰动可以提供脱离当前区域的动力,从而在搜索空间里不断生成新位置。
开发阶段则更接近“围绕优秀个体加速收敛”的写法。新的位置往往是在当前最优解附近生成,配上由迭代进度控制的步长收缩因子,让算法在后期能逐渐集中在最优解周围“深挖”。
三阶段逃生应对策略则是在特定条件下触发,对当前解做扰动,再和原解比较,择优保留。SBOA的关键优势也在这里:它在开发阶段的内层还嵌套了一层扰动机制,通俗地讲,就是既不耽误往最优区域冲刺,又不断试探周围有没有更好的位置,这一点在求解FJSP这种多峰严重的离散问题时很有价值。
2.3 SBOA与FJSP的结合点为什么成立
FJSP的解空间是离散的、组合爆炸的,直接拿SBOA的连续位置更新公式去更新工序序列和机器序列是不可行的,绝大多数连续元启发式算法做离散优化,都需要“连续编码+离散映射”的中间层。
用SBOA求解FJSP可行的核心思路是:算法产生的“位置向量”被视为一种实数编码的决策向量,每个维度对应调度问题中的一个决策变量。比如工序排序向量用连续数值表示,数值的相对大小决定工序的先后次序;机器分配向量用连续数值映射到合法机器编号。这种映射方式在遗传算法、粒子群的离散改造里已经很成熟,SBOA只是把位置更新从速度和位置交替,换成了自己的一套捕猎更新,其余离散化映射完全复用。
更直白地说,SBOA求解FJSP的过程可以拆成“算法搜索机制负责生成新位置”和“离散映射+解码负责评估新位置好坏”两个模块。前半段SBOA只管更新实数向量;后半段FJSP的解码函数负责把实数向量恢复成可行调度并算出makespan。这种松耦合架构,让我在调试时能单独排查是算法发散还是解码出错,效率提升非常明显。
2.4 核心参数与FJSP场景下的取值建议
SBOA算法里真正需要手动设置的参数不多,主要就是种群规模N和最大迭代次数T。这和遗传算法相比,省掉了交叉率、变异率、选择压等一堆参数,对没有多少调参经验的新手非常友好。
不过需要注意,SBOA内部有一组控制探索向开发切换的概率阈值。复现原算法时,这些阈值通常写死在代码里,但在FJSP这种离散问题里,我实际测试后的经验是:过早切换会让种群快速丧失多样性,后基乏力;过晚切换又会浪费大量迭代次数在无效探索上。工程上更稳妥的做法,是在原始阈值基础上做一个线性过渡,让切换更平滑。
FJSP场景下,我常用的参数组合是:种群规模50到100、最大迭代次数300到500。对于Mk01这种几十道工序的小型实例,50个个体跑300代已经足够收敛;对于工序数上百的实例,建议把种群提到80到100,迭代次数提到500以上。
3. 编码与解码:整个项目的命门所在
3.1 双层编码:工序排序向量 + 机器分配向量
FJSP的一个完整解必须同时表达“机器是谁”和“顺序是什么”两个信息。我用双层编码来解决这个问题:用工序排序向量(Operation Sequence,OS)和机器分配向量(Machine Assignment,MA)拼接成一个完整个体。
工序排序向量是一个整数序列,总长度等于所有工件的工序总数之和。每个工件编号在序列中出现的次数,等于该工件的工序数量。解码时从左往右扫描这个序列,第k次遇到编号i,就对应工件i的第k道工序。如果工件1有3道工序,那么序列里的1出现3次,第一次遇到表示O11,第二次表示O12,第三次表示O13。
机器分配向量长度与工序排序向量相同,每个位置上的数字表示对应工序选择哪台机器。但这里有个编码细节要特别注意:同一个工序在不同个体里的机器编号是随工序位置对齐的,所以两个向量的位置必须一一对应,解码时才能准确知道某个工序被分配到哪台机器。
这种双层编码的好处是工序顺序和机器选择可以分别演化,在SBOA的更新中也能分开处理,不会因为机器变了导致工序序列失去可行性。代价是每个个体占用的存储空间稍大一点,但在Matlab矩阵实现里完全不构成问题。
3.2 解码的核心:插入式调度最有性价比
解码过程是FJSP实现里最容易被写错的模块。我把解码分成两种策略:追加式解码和插入式解码。
追加式解码的逻辑最简单:按照工序排序向量依次取出工序,找到机器分配向量指定机器,然后把该工序追加到这台机器当前最后一个工序的后面,开始时间就是前一工序的完工时间。但这种策略有个明显问题:机器上的空闲时间窗被白白浪费了。
插入式解码会扫描该机器的时间表,查找每一段空闲窗口,如果空闲窗口的起始时间不早于前道工序的完工时间,且窗口长度足够容纳当前工序的加工时间,就把工序插入到这个窗口里。插入式解码能让makespan显著下降,尤其是工序数较多、机器时间表比较碎的时候,收益非常明显。
实际写插入式解码时,我把机器时间表设计成两个数组:完工时间数组和空闲窗口数组。空闲窗口每隔一段时间更新一次,插入工序后需要把被占用的窗口切分或删除,再重新排序。这部分代码逻辑不复杂,但边界条件较多,一定要反复测试。
3.3 初始种群不能全随机:机器选择的混合策略
初始种群的质量直接决定SBOA最终解质量的天花板。如果初始种群全是纯随机机器选择,大概率会生成一些机器负荷极不均衡的个体,算法要花大量迭代去纠正,收敛速度很慢。
我在初始化机器分配向量时,混合了三种策略:全局选择(Global Selection,GS)、局部选择(Local Selection,LS)和随机选择(Random Selection,RS)。全局选择会统计全车间所有工序在各台机器上的累计负荷,优先选择负荷最低的机器,均衡性好;局部选择只针对当前工序,在可加工机器里选加工时间最短的,单个工序最优;随机选择用平均分布产生随机机器编号,保证种群多样性。
三种策略按比例混合生成每个个体的机器向量,比如GS占一部分、LS占一部分、RS占一部分。实测下来这种混合初始化能明显抬升初始种群的平均质量,同时保留充足多样性,SBOA后期的搜索压力小了很多。
工序排序向量的初始化倒是简单得多,只需要保证每个工件编号出现次数等于其工序数,然后随机打乱顺序即可。为了保证初始种群里有部分优秀个体,也可以在部分个体里使用最早完工时间优先、最长剩余加工时间优先等启发式规则,再补充一部分纯随机个体。
3.4 任意一个环节出问题,整个解都报废
编码解码模块需要格外小心一个点:合法性和可行性。解码时如果机器分配向量给某个工序分配了一台不可加工机器,加工时间在数据矩阵里是Inf,那么这台机器的时刻表就会出现一个巨大的加工时间,makespan爆表,搜索过程直接失真。
我的处理方式是在编码阶段就做约束检查:机器分配向量的每个位置,只能是该工序可选机器集合中的编号。如果SBOA更新产生了越界数字,就把它修复到合法范围内;如果更新后机器编号不再属于可选集合,就随机替换成一台可选机器。这个修复步骤写在位置更新和数据转换之间,用专门的归一化函数批量处理。
工序排序向量天然合法,因为更新过程不改变编号出现次数,只改变排列顺序。但要注意,我用连续数值间接编码工序顺序时,连续值排序映射到工序编号后,绝不能出现同一位置重复取出同一工件的不同工序的情况,这些逻辑细节在实现时都要单独写单元测试函数验证。
4. Matlab代码实现:从主循环到甘特图
4.1 程序架构怎么搭才不容易乱
写Matlab代码求解FJSP,我建议按模块划分文件,不要全部堆在一个脚本里。我的目录结构大致是:主脚本main.m、数据加载模块loadInstance.m、种群初始化模块initPop.m、解码模块decode.m、SBOA主体模块SBOA.m、绘图模块plotGantt.m,以及一个临时文件夹用来放实例数据文件。
主脚本main.m的职责非常纯粹:加载实例数据、设置算法参数、调用初始化、进入迭代循环、输出结果。所有复杂计算全部下沉到函数模块里,这样单步调试的时候,一句一句看主循环的流程,很容易定位问题出在哪一层。
一个值得复制的好习惯是:在main.m里加一个总计时器,把初始化耗时、单次迭代耗时、解码耗时分别记录下来。运行一次实验,打开命令行一眼就能看出瓶颈在算法更新还是在解码计算,这对后续性能优化特别重要。
4.2 核心函数逐个说明,附带实际可用的代码骨架
数据加载阶段,我习惯把实例数据存成三部分:工件工序数量数组、每个工序的可选机器列表、每个工序在每台机器上的加工时间表。在Matlab里用嵌套元胞数组存储最灵活。比如time{i}{j}(k)取出来就是第i个工件第j道工序在第k台机器上的加工时间,如果机器k不可加工该工序,值就是Inf。
initPop.m的核心是生成工序序列和机器序列。单说机器向量生成,GS策略的伪代码思路是:先初始化一个全局机器负荷数组,从头到尾遍历所有工序,每选完一台机器就把该机器负荷累加一次,后续选择时优先找累计负荷最小的机器。LS策略只需要在当前工序的可加工机器里找最小时间。RS策略则直接用randi在机器编号里随机挑。
decode.m是整个程序里最核心的函数,我对每个个体都会执行完整的时间推进。基本过程是:
function [makespan, machine_table, job_completion] = decode(operation_seq, machine_seq, job_info, time_info)先把机器完工时间初始化为全零,再初始化每台机器的空闲时间窗口列表。遍历操作序列,找到当前工序及其前道工序的完工时间,得到该工序允许的最早开始时间。然后取机器分配向量中的机器编号,去扫描这台机器的空闲窗口,查找能容纳工序加工时间的窗口,插入并更新窗口列表。全部工序安排完后,makespan等于所有机器完工时间的最大值。
SBOA主体模块的每一次迭代中,位置更新之后会立刻做边界约束与机器合法性修复,然后调用decode评估新个体,保住父代精英,用锦标赛式或直接比较的方式选出下一代。这部分代码调用的顺序一旦颠倒,比如先评估后修复,会导致解码结果失真,必须严格遵循“更新→修复→评估→选择”的流程。
plotGantt.m画甘特图相对独立,遍历机器表的每台机器,把每个工序按开始时间和结束时间画成矩形条,用不同颜色区分工件,纵轴是机器编号,横轴是时间。
4.3 运行参数和实例规格怎么对应起来
我测试时用的实例主要是Brandimarte系列和Kacem系列。Mk01是8个工件、6台机器、总工序数约55的实例;Kacem的8x8实例则是8个工件、8台机器、总工序数27左右。不同规模岗位对参数的要求差异很大,我在main.m里把参数集中在顶部,方便跑实验时统一切换。
针对小规模实例,种群规模40、迭代次数200已经能稳定收敛。针对中等规模的Mk系列,我往往把种群提到80、迭代次数提到400。更大的自定义实例,我通常先用迭代次数300快速试跑一遍,根据收敛曲线决定是否需要加迭代。
参数设置还有个容易忽略的点:独立重复运行次数。元启发式算法是随机的,跑一次不能说明问题,我在实验里统一设置每个实例独立运行20次,记录每次的最优makespan,再求平均值和标准差。这样出来的数据,写论文或做方案对比才有说服力。
4.4 从一次运行到一组实验:数据记录脚本
跑实验时,只靠命令行输出很难做系统对比,我习惯把结果统一记录到一个结构体数组里。每跑一个实例,就把实例名、最优makespan、平均makespan、标准差、平均运行时间和最优解的工序序列存下来,最后用save保存成mat文件。
处理多个实例时,我会写一个runExperiments.m脚本,里面用一个for循环遍历所有实例文件,每个实例重复运行多次。结果清晰之后,再统一汇总成表格,方便后续分析。这个方法对于论文里“各算法在标准实例上的对比结果”部分特别好用,数据直接就能填进表格。
5. 实验设计与参数调优:让数据说话,别拍脑袋
5.1 标准测试实例的选择思路
做FJSP研究,使用公开标准实例几乎是学术共识。因为不同论文、不同算法之间的对比必须建立在同一套测试集上,否则数据没有可比性。Brandimarte系列是最常用的数据集之一,从Mk01到Mk10,涵盖了不同工件数、机器数和柔性程度。Kacem系列则包括4x5、8x8、10x10等小规模实例,非常适合快速验证算法逻辑是否跑通。
我建议新手先用Kacem的小实例做功能验证,比如Kacem 8x8实例的已知最优解是14或15左右,一旦算法能求出接近的数字,说明编码解码和算法主体基本正确。再上Mk01这类难度适中的实例做算法对比。
5.2 评价指标不止C_max一个
虽然目标函数是makespan,但评价算法优劣,指标不能只看最优解一个值。我常用的指标包括:
- 最佳makespan:多次运行中最优的解。
- 平均makespan:反映算法整体稳定性。
- 标准差:波动越小说明算法越稳定。
- 相对偏差(RPD):与已知最优解或下界的偏差百分比。
- 平均计算时间:评估算法效率。
RPD计算公式是:(best_found - best_known) / best_known * 100%。对于没有已知最优解的实例,可以用下界替代。论文里比较多个算法时,表格通常列出各算法在每个实例上的最佳值、平均值和标准差,一目了然。
5.3 SBOA和常见算法放一起比,结果怎么看
对比实验我一般选择遗传算法(GA)、粒子群优化(PSO)和灰狼优化(GWO)作为基准算法。所有算法使用完全相同的编码解码函数、相同的初始种群生成方式、相同的评估函数,这样唯一变量就是搜索机制本身,对比才算公平。
在Kacem 8x8和Mk01实例上,SBOA的收敛曲线表现比较突出,往往在迭代早期就能快速逼近较优区域。相比GA需要调交叉变异率、PSO需要调惯性权重和学习因子,SBOA几乎不需要太多参数干预,开箱即用的特性让我省了不少调参时间。
但SBOA也有一些短板,在部分实例上后期收敛速度会放缓,种群多样性下降后,最优解精度不如GA配合局部搜索。实际做法是在SBOA迭代后期叠加一个“精英局部搜索”环节,比如对当前全局最优解的邻域做若干次扰动和重解码,能显著提升最终解质量。
5.4 参数调优实操心得,直接把结论写出来
受限于篇幅,我不打算把无数次调试过程全列出来,但有三条规律,基本上是换哪个实例都成立的:
第一,种群规模太小容易早熟。种群为20时,Mk05这种中等实例很容易在迭代中段就停滞;提到80后,解质量有明显提升。第二,迭代次数不能太少,SBOA的逃生扰动机制在后半段才发挥主要作用,如果只跑150代,相当于没给它精调的机会。第三,机器初始化里GS比例建议不低于40%。纯随机初始化时,Mk01的最优解大约比混合初始化差5%到8%,这是相当可观的差距。
我还发现,SBOA逃生策略的触发频率对结果很敏感。太频繁会让种群一直处于剧烈扰动状态,难以收敛;完全不触发又会让开发阶段缺少多样性恢复机制。合理做法是按迭代进度递减触发概率,前期触发多一些保证探索,后期减少并让种群稳定开发。
6. Matlab代码常见问题与排查技巧实录
6.1 收敛太快崩在局部最优,怎么判断和解决
很多同学第一次运行完,看收敛曲线发现几百代内从很早就平了,以为算法“收敛成功”,其实是早熟。判断依据很简单:多次独立运行的最优解结果几乎一模一样,且距离已知最优解还有明显差距。
排查时先看初始种群的解质量。如果初始种群平均makespan太高,SBOA一上来就疯狂朝当前最优聚拢,很快就丧失多样性。解决办法是提高GS策略比例,提升初始种群质量基线;同时把逃生策略触发概率调高,给后期更多跳出机会。还有一个粗暴但好用的办法,就是引入“重启机制”,当连续30代最优解没有更新时,随机重新生成一部分个体,插入当前种群。
6.2 解码速度慢,瓶颈往往在空闲窗口扫描
随着迭代次数增加,解码函数被调用的次数越来越多,如果空闲窗口扫描写得低效,整个程序跑起来会非常慢。我调试早期就遇到过一次:Mk10实例,200代、50个个体竟然要跑将近10分钟。
排查后发现是窗口扫描用了全量遍历,每插入一个工序都重新扫描该机器全时间段。优化思路很简单:每台机器维护一个按开始时间排序的空闲窗口数组,插入工序时二分查找第一个可行窗口;插入后只更新受影响部分窗口,不同机器间互不影响。优化之后同样的参数配置,运行时间缩短到两分钟左右。
在Matlab里写循环确实慢,能向量化的尽量向量化。但解码过程本身需要逐工序推进,不太适合完全向量化。折中方案是用MEX编译或把解码函数写成C风格,实在不行就在Matlab里用tic/toc持续监控,优先确认优化收益再动手改。
6.3 解码结果出现不可行解,大概率是机器约束没卡紧
有时候运行完成后检查调度方案,发现某台机器上同时有两个工序在加工,或者某个工件的下一道工序开始时间早于上一道工序结束时间。这类不可行解通常来自两个地方:一是解码函数里更新机器时间表时漏掉了对前道工序完工时间的检查,二是SBOA更新后机器编号出现非法值,而且边界修复没生效。
我加了专门的checkSchedule函数,调度方案生成后会自动做合法性校验,一旦检测到重叠就输出错误信息并终止运行,这样能迅速定位是哪个个体、哪个位置出错。
6.4 甘特图绘制时矩形重叠怎么处理
甘特图是FJSP结果展示里最直观的输出,但程序写不好,很容易画出一堆重叠矩形条,看起来像是调度方案冲突。实际上大部分时候方案合法,只是绘图坐标没算对。
因为同一台机器上的工序都是串行排列的,矩形条纵坐标按机器编号设置,横坐标从开始时间到结束时间,所有矩形在纵向互不重叠,机器之间天然隔离。如果非要在同一行里画出多个并行矩形,那说明解码设计有问题或者绘图机的轴坐标映射出错了。我通常用rectangle函数逐个绘制,颜色按工件编号区分,并在图例里标注工件号,这样导出的图能直接用于论文或项目汇报。
最后再分享一点我自己的实操体会
如果让我重新做一遍这个项目,我会先在纸上把编码、解码、修复、评估四件事的输入输出和对应关系画清楚,再动手写代码。因为FJSP项目里90%的bug不是算法本身太难,而是边界条件不清、数据格式不稳、机器合法性没卡死。SBOA的部分反而设计得比较“干净”,把核心更新公式吃透之后,调试起来比想象中顺利。
最后一个非常实用的小技巧:每次运行实验,把最优解对应的工序序列和机器序列单独保存下来。这组数据是有效的问题解,丢进甘特图可以直接验证方案合理性,写论文时还能反复使用,避免算法跑完结果却无从追溯的尴尬。