简介:这套MATLAB源程序面向从事智能优化算法、工业工程与生产调度研究的高校学生和工程人员,围绕改进鲸鱼算法(IWOA)求解双目标柔性车间调度问题,配套一篇论文说明算法思路与实验。代码在基本鲸鱼算法上融合动态参数调整、混沌映射、遗传算子与多策略搜索,用于权衡总完成时间、能耗或碳排放等目标,并可通过仿真绘制帕累托前沿,观察不同方案的折衷关系。压缩包共17个文件,约11.4MB,其中16个.m脚本承担算法主流程、目标计算、初始种群、局部搜索、数据转换与结果绘图等任务,另含1个PDF论文便于对照理解模型和实验设置。已有2238人学习下载,适合需要复现实验、改造算法或补充双目标调度案例的读者参考。
1. 从一个排产现场说起:柔性车间调度为什么难,以及这份 MATLAB 源码能帮上什么忙
车间里十几台机器、几十个工件,每个工件要按固定顺序走几道工序,同一道工序换一台机器加工时间就变——这就是柔性车间调度(FJSP)最朴素的形态。它的难处不在建模,而在于“选机器”和“排顺序”这两个决策互相牵扯:机器一换,工序时间变,甘特图整张都要重排。标准鲸鱼算法(WOA)拿来解这种离散组合问题,维度一高就容易早熟,这份 IWOA 改进鲸鱼算法源码就是冲着这个痛点去的,配套的论文把改进点和对比实验一并交代清楚,MATLAB 代码可以直接跑、直接改。它适合两类人:一是课程设计或毕设要交双目标调度结果的学生,二是产线上真要做排产验证、想先拿个能跑的基线再谈落地的开发者。下面按算法骨架、编码解码、跑通步骤、踩坑排查、二次开发五段来拆。
2. 算法骨架拆解:IWOA 相对标准鲸鱼算法动了几处刀
标准 WOA 的结构其实不复杂,三条行为线:包围猎物、螺旋气泡网、随机搜索。三者的切换靠参数 A 的绝对值,|A|<1 就围着当前最优个体收缩,|A|≥1 就随便挑一个个体做全局探索。问题在于,FJSP 是离散的,位置向量本身没有连续意义,而且双目标下“最优个体”往往不止一个,直接照搬连续版的更新公式,结果通常是一半种群早早塌到同一个局部解上。这份源码的改进思路基本沿着“初始化—收敛因子—扰动—档案”四条线展开,属于这一类工作里比较主流的做法。
2.1 标准 WOA 的三段式行为与它为什么会失效
先把标准版的数学形式摆出来,后面才看得懂改了什么。包围猎物用的是D = |C·X* - X|,位置更新X(t+1) = X* - A·D,其中A = 2a·r1 - a、C = 2r2,a从 2 线性降到 0。螺旋更新是X(t+1) = D'·e^(bl)·cos(2πl) + X*,b一般取 1 左右控制螺旋形状。随机搜索阶段则把X*换成一个随机个体。
放到 FJSP 上,第一处失效是离散映射:位置向量要经过某种编码才能变成工序序列,连续的加减乘除作用在编码上,很容易产生非法解,比如同一个工件的工序被安排到它自己的前一道工序之前。第二处失效是收敛因子线性递减:前期探索和后期开发各占一半,但调度问题需要的是前期大幅度铺开搜索面、后期快速收紧,线性过渡在中段浪费了大量迭代。第三处失效是单最优引导:双目标下 Pareto 前沿是一整条线,只盯着一个 gbest,种群多样性会掉得很快。
抓住这三条,再看改进点就不会觉得是拍脑袋堆公式了。
2.2 混沌初始化与非线性收敛因子
初始化这一步,随机均匀分布是最省事的做法,但在高维调度空间里,随机点的覆盖质量不稳定,换个种子结果能差出 5%~10%,这种玄学波动后面调参会很痛苦。源码里用的是混沌映射生成初始种群,常见做法是 Tent 映射或 Logistic 映射,前者遍历性更好、后者实现更简单,两者都可以。
function X = chaoticInit(N, dim, lb, ub, mode) % 混沌初始化:先生成 (0,1) 区间的混沌序列,再线性映射到 [lb, ub] % N 种群规模 % dim 染色体维度(FJSP 里通常是 2×总工序数) % mode 'tent' 或 'logistic' z = zeros(N, dim); z(1, :) = rand(1, dim); % 混沌初值,避开 0 和 0.5 这些不动点 for i = 2:N for d = 1:dim if strcmp(mode, 'tent') mu = 1.99; % 越接近 2 遍历性越好,取 2 会退化 z(i, d) = (mu * z(i-1, d)) * (z(i-1, d) < 0.5) + ... (mu * (1 - z(i-1, d))) * (z(i-1, d) >= 0.5); else mu = 4.0; z(i, d) = mu * z(i-1, d) * (1 - z(i-1, d)); end end end X = repmat(lb, N, 1) + z .* repmat(ub - lb, N, 1); endmu这个参数值得单独说一句:Tent 映射在mu=2时会退化成周期序列,实际用一般取 1.99 或者 1.999,代码里写成 1.99 是留了余量。这一段输出的X只是连续空间里的初始点,真正喂给解码器之前还要做一次离散化,见第 3 章。
收敛因子是第二个改动点,把线性改成非线性:
function a = convFactor(t, T, mode) % 非线性收敛因子,t 当前迭代,T 最大迭代 switch mode case 'square' % 前期慢降、后期快降 a = 2 - 2 * (t / T)^2; case 'cos' % 余弦型,两端平缓中间陡 a = 2 * cos((pi / 2) * (t / T)); case 'exp' % 指数型,前期长尾探索 a = 2 * exp(-3 * (t / T)^1.5); otherwise a = 2 - 2 * (t / T); % 标准线性,拿来对比用 end end三种模式我都试过,square在中小规模算例上收敛最快,cos在前沿分布均匀度上略好一点。论文里通常会附一张不同收敛因子策略的对比曲线,跑复现的时候记得把mode换成'linear'再跑一遍当基线,不然看不出改进到底贡献了多少。
2.3 自适应权重与 Lévy 扰动跳出局部最优
第三个改动是给位置更新加惯性权重,让优秀个体在后期少被扰动,差个体多被推一把:
w = wMax - (wMax - wMin) * (t / T); % 典型取值 wMax=0.9, wMin=0.4 pop(i, :) = w .* pop(i, :) + (1 - w) .* (gbest - A .* D);Lévy 飞行那条线是用在停滞个体上的,不是每一代对全体做。判断停滞一般看连续若干代适应度没有改善,或者个体的适应度偏离种群均值超过某个阈值。这么设计的目的是:扰动是有代价的,全域乱飞会破坏已经收敛得不错的解结构。
function Xn = levyPerturb(X, beta, lb, ub) % beta 常取 1.5,Lévy 步长由两个正态分布之比生成 dim = size(X, 2); sigma = (gamma(1+beta) * sin(pi*beta/2) / ... (gamma((1+beta)/2) * beta * 2^((beta-1)/2)))^(1/beta); u = randn(1, dim) * sigma; v = randn(1, dim); step = u ./ (abs(v).^(1/beta)); Xn = X + 0.01 * step .* X; % 0.01 是步长缩放,太大会直接把解炸飞 Xn = min(max(Xn, lb), ub); end那个 0.01 的缩放系数是关键参数。我见过直接乘 1.0 的写法,结果每代都在做全局重置,收敛曲线抖成锯齿状,看着像“算法在探索”,实际是压根没在收敛。
2.4 双目标处理与外部档案维护
双目标不能简单加权了事——权重一固定,Pareto 前沿的凹部区域基本采不到。这份源码用的是“加权构造适应度引导搜索 + 外部档案保存非支配解”的组合:搜索阶段需要一个标量来比较大小,就用归一化加权;但每一代结束都把非支配解放进档案,最后从档案里输出前沿。
function archive = updateArchive(archive, pop, fit, maxSize) % 合并旧档案与新种群,做非支配筛选,超限则按拥挤度裁剪 cand = [archive; pop]; cf = [archiveFit; fit]; % 适应度同步合并 n = size(cand, 1); dominated = false(n, 1); for i = 1:n for j = 1:n if i == j, continue; end if all(cf(j,:) <= cf(i,:)) && any(cf(j,:) < cf(i,:)) dominated(i) = true; % i 被 j 支配 break; end end end archive = cand(~dominated, :); archiveFit = cf(~dominated, :); if size(archive, 1) > maxSize cd = crowdingDistance(archiveFit); [~, idx] = sort(cd, 'descend'); archive = archive(idx(1:maxSize), :); archiveFit = archiveFit(idx(1:maxSize), :); end endmaxSize一般取 50~100,太小前沿会稀疏,太大每一代的非支配比较开销会明显上升。crowdingDistance就是标准的拥挤度距离,按每个目标排序后取相邻解的目标差归一化求和。这块代码不长但很容易写错,尤其是合并时忘了同步archiveFit,导致档案里的解和目标值对不上——这个坑我自己也踩过,后面第 5 章会展开。
3. 柔性车间调度的编码与解码:两段式染色体怎么落到甘特图
算法是壳,编码解码才是这个项目真正的肉。FJSP 有三组决策:每道工序选哪台机器、工序之间谁先谁后、以及插入到机器的哪个空档。前两个靠染色体表达,第三个靠解码器决定。绝大多数“跑出来结果不对”的问题,最后都出在解码环节而不是算法本身。
3.1 OS 与 MS 两段编码的物理含义
源码用的是两段式编码:OS(Operation Sequence)段和 MS(Machine Selection)段,各长 N,N 是总工序数。
OS 段里存的是工件号,不是工序号。规则是:某个工件号第 k 次出现,就代表该工件的第 k 道工序。比如工件 1 有 3 道工序,那 OS 段里数字 1 出现三次,第一次出现就是工序 1-1,第二次是 1-2,第三次是 1-3。这个设计的好处是任何排列都合法,不需要额外修复。
MS 段每一位对应 OS 段同位置的工序,存的是这台工序选中的机器编号。这里有个极易翻车的语义分歧:存“全局机器编号”还是“该工序候选机器集里的序号”。两种都有人用,但绝不能混。源码里用的是全局编号,好处是调试时看甘特图能直接对上号,坏处是交叉变异后必须做一次越界检查。
| 段 | 长度 | 取值含义 | 合法性要求 |
|---|---|---|---|
| OS | N(总工序数) | 工件编号,第 k 次出现代表该工件第 k 道工序 | 每个工件出现次数 = 其工序数 |
| MS | N | 该工序选用的全局机器编号 | 必须在该工序的可加工机器集合内 |
基因初始化时,OS 段按工件工序数展开成[1 1 1 2 2 3 3 3 ...]再打乱,MS 段每道工序从它的候选机器里随机挑一台。这么构造出来的初始解 100% 可行,省掉了后面写修复算子的麻烦。
3.2 插入式解码:一个工序到底插在哪个空隙
解码的任务是把 OS + MS 翻译成每道工序的开始时间和结束时间。这里有两种做法:顺序解码(工序只能排在机器已有任务的末尾)和插入式解码(工序可以塞进机器时间轴上的第一个足够大的空隙)。前者实现简单但解的质量差一大截,同一组染色体,插入式解码经常能省下 5%~15% 的 makespan。这份源码用的是插入式。
function [st, en, mOf] = decode(OS, MS, PT, nJob) % OS 1×N 工序序列(工件号) % MS 1×N 对应工序选中的机器 % PT nJob×maxOp×nMach 加工时间张量,0 表示不可加工 % st/en 每道工序的开始/结束时刻,按 k 排序,与 OS 顺序一致 N = numel(OS); st = zeros(1, N); en = zeros(1, N); mOf = zeros(1, N); jobCnt = zeros(1, nJob); % 各工件已完成工序数 jobEnd = zeros(1, nJob); % 各工件前一道工序的完工时刻(紧前约束) occ = cell(1, 100); % occ{m} 是机器 m 上已占用区间,按开始时刻升序 for k = 1:N j = OS(k); jobCnt(j) = jobCnt(j) + 1; o = jobCnt(j); % 这是工件的第几道工序 m = MS(k); p = PT(j, o, m); ready = jobEnd(j); % 不能早于前道工序完工 [s, pos] = earliestGap(occ{m}, p, ready); e = s + p; st(k) = s; en(k) = e; mOf(k) = m; occ{m} = insertInterval(occ{m}, [s e], pos); jobEnd(j) = e; end endearliestGap的逻辑就三行核心:从ready时刻起,依次检查机器上每个已占用区间,看它开始时刻和当前游标之间的间隔够不够放p;不够就把游标推到该区间结束时刻。
function [s, pos] = earliestGap(iv, p, ready) % iv n×2 已占用区间,按开始时刻升序 % p 待排工序加工时长 % ready 最早可开工时刻(前道工序完工时间) s = ready; pos = 1; for i = 1:size(iv, 1) if iv(i, 1) - s >= p return; % 第 i 段之前就放得下,直接返回 end if iv(i, 2) > s s = iv(i, 2); % 跳过这段占用,游标右移 end pos = i + 1; end end注意if iv(i,2) > s这个判断,少了它,游标可能被往回推,甘特图上就会出现工序重叠。这是我从一次通宵调试里换来的血泪经验:图能画出来、目标值也算得出来,就是看着别扭,一查全是时间轴倒流。
3.3 双目标函数:makespan 与机器总负荷怎么算怎么写
两个目标分别是最大完工时间Cmax = max(en)和总机器负荷Wtot = sum(en - st)。前者管交期,后者管设备占用和能耗,工程上这两个指标经常打架——把活儿全堆到一台快机器上,Cmax好看但负荷全压在一台上,换型、维护、能耗都会出问题。
function [f1, f2] = objectives(st, en, nMach) f1 = max(en); % 最大完工时间 f2 = sum(en - st); % 总机器负荷 end搜索阶段需要一个标量来比大小,就用归一化加权:
function f = scalarize(f1, f2, ref, w) % ref 是从初始种群统计出来的 [min max],用来做量纲对齐 n1 = (f1 - ref(1,1)) / (ref(1,2) - ref(1,1) + eps); n2 = (f2 - ref(2,1)) / (ref(2,2) - ref(2,1) + eps); f = w(1) * n1 + w(2) * n2; endeps那一下不能省,某些算例初始种群恰好Cmax全一样,分母为 0 会出NaN,然后整个种群一夜之间变成NaN,收敛曲线直接消失——这个现象第一次遇到挺唬人的。归一化的参考值建议用初始种群统计,而不是固定常数,换算例时不用改代码。
4. 把程序跑起来:MATLAB 入口、算例格式与参数配置
前面的算法和编码都是零件,这一章说怎么把这堆零件拧起来跑出图。这份源码的组织方式是典型的“主脚本 + 若干函数文件 + 算例目录 + 论文 PDF”,没有用类,也没有依赖工具箱,好处是拿走就能跑,坏处是全局变量能省则省、参数全写在main.m顶部,改之前最好先通读一遍。
4.1 目录结构与主入口脚本
先看目录,找得到东西才好改:
IWOA_FJSP/ ├── main.m % 主入口:读算例、设参数、跑算法、出图 ├── iwoa.m % IWOA 主循环 ├── chaoticInit.m % 混沌初始化 ├── convFactor.m % 非线性收敛因子 ├── levyPerturb.m % Lévy 扰动 ├── decode.m % 两段式染色体解码 ├── earliestGap.m % 插入式解码的空隙查找 ├── objectives.m % 双目标计算 ├── scalarize.m % 加权标量化 ├── updateArchive.m % 外部档案维护 ├── loadCase.m % 算例文件解析 ├── plotGantt.m % 甘特图绘制 ├── plotConvergence.m % 收敛曲线 ├── exdata/ % 算例目录,case01.fjs ~ case10.fjs └── paper/ % 配套论文main.m大致长这样,参数全在开头集中,改起来直观:
clc; clear; close all; rng(2024); % 固定随机种子,方便复现论文曲线 addpath(genpath(pwd)); caseFile = 'exdata/case01.fjs'; [PT, nJob, nMach, maxOp] = loadCase(caseFile); N = 60; % 种群规模 T = 200; % 最大迭代次数 w = [0.7 0.3]; % 两个目标的权重 archMax= 80; % 外部档案上限 aMode = 'square'; % 收敛因子模式:square / cos / exp / linear [gbest, curve, archive] = iwoa(PT, nJob, nMach, maxOp, N, T, w, archMax, aMode); [st, en, mOf] = decode(gbest.OS, gbest.MS, PT, nJob); fprintf('Cmax = %d, Wtot = %d\n', max(en), sum(en - st)); plotGantt(st, en, mOf, nJob, nMach); plotConvergence(curve);rng(2024)这行别删。鲸鱼算法本身带随机性,同一份代码同参数跑两次结果差个 3%~8% 是常态,不固定种子就根本没法判断“改了之后是变好了还是运气好”。我一般会在对比实验里跑 10~20 次取均值和标准差,单次结果只能当参考。
4.2 算例文件格式与 loadCase 的解析要点
.fjs文件是纯文本,格式很朴素:第一行两个数,工件数nJob和机器数nMach;接下来nJob行,每行开头是该工件的工序数,后面跟着每道工序的「可选机器数、机器号、加工时间」三元组重复出现。
3 4 2 2 1 3 1 4 3 1 3 2 5 4 2 2 3 4 4 6第一行说 3 个工件 4 台机器;第二行说工件 1 有 2 道工序,第 1 道可在机器 2(耗时 1)或机器 3(耗时 4)加工,第 2 道只能在机器 1(耗时 4)。注意不同工件的工序数往往不一样,这是解析最容易翻车的地方。
function [PT, nJob, nMach, maxOp] = loadCase(fname) % 逐行读取,避免用 textscan 一次性拉平后分不清行的边界 fid = fopen(fname, 'r'); head = fgetl(fid); v = sscanf(head, '%d'); nJob = v(1); nMach = v(2); lines = {}; while ~feof(fid) ln = strtrim(fgetl(fid)); if isempty(ln), continue; end lines{end+1} = ln; end fclose(fid); opsPerJob = zeros(1, nJob); for j = 1:nJob v = sscanf(lines{j}, '%d'); opsPerJob(j) = v(1); end maxOp = max(opsPerJob); PT = zeros(nJob, maxOp, nMach); % 0 表示这道工序在该机器上不可加工 for j = 1:nJob v = sscanf(lines{j}, '%d'); ptr = 2; for o = 1:opsPerJob(j) k = v(ptr); ptr = ptr + 1; % 该工序的可选机器数 for q = 1:k m = v(ptr); t = v(ptr+1); ptr = ptr + 2; PT(j, o, m) = t; end end end end用一个三维张量PT(j,o,m)存加工时间,索引直接、调试友好,代价是规模大的算例内存占用高一点。中小规模(几十个工件、十几台机器)完全够用,真上到几百工件再考虑稀疏存储。
4.3 参数怎么配:种群、迭代与收敛因子的取舍
| 参数 | 变量名 | 常用取值 | 调大之后 | 调小之后 |
|---|---|---|---|---|
| 种群规模 | N | 40~100 | 多样性好,单代耗时长 | 收敛快,易早熟 |
| 最大迭代 | T | 100~300 | 解质量提升,边际递减明显 | 跑得快,前沿不完整 |
| 目标权重 | w | [0.6 0.4]~[0.8 0.2] | 偏向Cmax | 偏向机器负荷 |
| 档案上限 | archMax | 50~100 | 前沿更密,比较开销大 | 前沿稀疏 |
| Lévy 缩放 | 0.01 | 0.005~0.05 | 扰动强,可能破坏好解 | 扰动弱,跳不出局部 |
N和T的乘积大致决定了总评价次数,这个数在和其他算法做对比时必须对齐,不然比出来的东西没有意义。我一般是固定N=60,用T去控制总预算,让各算法在同一评价次数下比较。
4.4 结果怎么看:收敛曲线与甘特图
甘特图看三件事:横轴是时间,纵轴是机器,每个色块是一道工序。一眼扫过去,如果同一台机器上色块有重叠,说明解码的时间轴算错了;如果某个工件的工序在时间上前后颠倒,说明 OS 段的工件号出现次数和工序数对不上。收敛曲线则看两段:前 30 代下降快不快(初始化和探索能力),后 50 代还动不动(开发能力和是否早熟)。
function plotGantt(st, en, mOf, nJob, nMach) figure('Color', 'w'); hold on; cmap = lines(nJob); for k = 1:numel(st) j = mod(k-1, nJob) + 1; % 仅用于配色,与 OS 无强绑定 fill([st(k) en(k) en(k) st(k)], ... [mOf(k)-0.35 mOf(k)-0.35 mOf(k)+0.35 mOf(k)+0.35], cmap(j,:)); end set(gca, 'YTick', 1:nMach, 'YDir', 'reverse'); xlabel('时间'); ylabel('机器'); grid on; box on; end配色那行只是让图能看,真要按工件着色得把 OS 一起传进来,源码里plotGantt的完整版本是带这个参数的,我这里只留了骨架。图能出、目标值能对上、跟论文里报的数字在一个量级,基本就算跑通了。
5. 常见问题排查:跑不出论文那张图的时候先查这几处
下面这几条都是复现这类调度代码时高频遇到的真问题,按“现象 → 原因 → 解决”的顺序写,对照着查能省不少时间。
5.1 现象:结果比论文里差一截,怎么调参都追不上
原因:八成不是参数问题,而是解码方式不一致。论文里用的是插入式解码,代码里如果写成了顺序解码(工序只能追加到机器末尾),同一组染色体的Cmax会系统性偏大。第二种可能是随机数没固定,单次结果本来就在波动区间内。
解决:在decode.m里打断点,挑一道工序,手工核对它有没有被塞进前面的空隙。再确认main.m顶部的rng有没有被注释掉。最后跑 10 次看均值,跟论文报的均值比,别拿单次最优去比。
5.2 现象:甘特图上同一台机器的色块重叠,或者工序跑到时间 0 之前
原因:earliestGap里游标推进的写法有问题。典型错误是遍历占用区间时忘了比较区间结束时刻和当前游标,导致游标被往回拨;另一种是紧前约束没参与计算,ready传了 0。
解决:在decode里每排完一道工序就断言st(k) >= jobEnd(j)且该机器上不存在与之重叠的区间。断言打开跑一遍,出问题第一时间就能定位到是哪一道工序、哪台机器。
5.3 现象:算法跑到二三十代就不动了,收敛曲线一条直线
原因:种群多样性丢光。可能是所有个体都塌到同一个局部解,也可能是加权归一化时分母为 0 导致整代适应度变成NaN,min失效后gbest卡死不动。
解决:先在每代打印种群适应度的标准差,标准差掉到 1e-6 量级就是塌了,这时候把 Lévy 扰动的触发阈值调松一点,或者加大wMax。同时检查scalarize.m里分母有没有加eps。
5.4 现象:换一个算例就报“索引超出数组范围”
原因:算例解析出了问题。不同算例的工件工序数不一样,如果PT的第二维按第一个工件写死,后面的工件工序一多就越界。另一种是.fjs文件里有空行或者行尾多余空格,sscanf读出来的长度和预期不符。
解决:loadCase里先扫一遍所有工件取最大工序数再分配张量(代码见 4.2),读文件时用strtrim去掉首尾空白并跳过空行。读取完顺手校验一下sum(PT(:)>0)是否等于文件里声明的可选机器总数。
5.5 现象:MS 段看着没问题,但解的质量突然崩了
原因:MS 段的语义变了。有的是改代码时把“全局机器编号”改成了“候选机器集内的序号”,或者反过来,两者混用后工序被分到了根本不能加工它的机器上,PT查到 0,加工时间变 0,Cmax看着很小但完全不可行。
解决:统一语义,并且在decode里加一句硬校验:assert(PT(j,o,m) > 0)。这一句加上去,非法解当场就炸,不会带着错误结果跑完整轮再让你怀疑人生。
提示:这几条排查最好在改任何算法代码之前先做一遍,把解码的正确性锁死,后面调参才有意义。解码错了,算法改得再花哨也只是在错误的基准上抖动。
6. 二次开发的改造路线:换目标、接数据、做对比
拿到手最常做的三件事:加一个目标、换成自己的数据、把对比实验补齐。
加目标(比如能耗或拖期惩罚)时,改动集中在三处:objectives.m多返回一个f3,scalarize.m的权重向量扩到三维,updateArchive.m里的支配判断从二维循环改成按行比较(写死两维的地方要一并改)。能耗这类目标一般跟机器负荷正相关但不完全等价,得单独按机器功率和空转时间建模,别直接拿Wtot顶替。
换数据时,只要把loadCase的输出对齐成PT(nJob, maxOp, nMach)张量,其余模块一行不用动。历史数据在 Excel 里的话,写个转换脚本先导出成.fjs文本,比在 MATLAB 里直接读表格省事得多。
| 改造点 | 涉及函数 | 改动量 | 容易漏的地方 |
|---|---|---|---|
| 加第三目标 | objectives/scalarize/updateArchive | 中 | 支配判断写死二维 |
| 换成 Pareto 输出 | updateArchive/main | 中 | 档案与适应度数组不同步 |
| 接入自有数据 | loadCase | 小 | 工序数不一致导致张量越界 |
| 换收敛因子策略 | convFactor | 小 | 对比实验里忘跑基线 |
| 加局部搜索 | iwoa主循环末尾 | 大 | 局部搜索后没做编码修复 |
跟其他算法对比时,评价次数必须对齐,N×T相等是底线;每个算例至少跑 10 次,报均值和标准差;指标建议同时给Cmax最优值、均值和超体积(HV),只报最优值说服力不够。
有个习惯是那次甘特图重叠事件之后养成的:动任何跟时间轴相关的代码之前,先在decode末尾挂一段断言,把紧前约束和机器不重叠这两条硬规则跑一遍,断言不过就不往下走。这几十行代码看着笨,但省下的调试时间比写它多得多。希望帮到你。
本文还有配套的精品资源,点击获取