本文是对论文[1]的复现。
由于遗传算法的资料网上已经有很多描述,这里就不进行赘述。本文主要说明几个关键过程。
1.编码
由于FJSP问题具有两个决策过程(机器选择和工序排序),所以使用双向量编码。其中ms编码用来描述机器选择的过程,os编码用来描述工序排序的过程。
假如有三个作业0,1,2,每个作业都有两个工序0,1。则所有工序为
{
O
0
,
0
,
O
0
,
1
,
O
1
,
0
,
O
1
,
1
,
O
2
,
0
,
O
2
,
1
}
\{O_{0,0},O_{0,1},O_{1,0},O_{1,1},O_{2,0},O_{2,1}\}
{O0,0,O0,1,O1,0,O1,1,O2,0,O2,1}。这些工序可选择的机器均为编号为0,1,2的机器。
ms编码用一个列表表示。比如
{
0
,
2
,
1
,
0
,
2
,
1
}
\{0,2,1,0,2,1\}
{0,2,1,0,2,1},该列表中的每个元素分别为
{
O
0
,
0
,
O
0
,
1
,
O
1
,
0
,
O
1
,
1
,
O
2
,
0
,
O
2
,
1
}
\{O_{0,0},O_{0,1},O_{1,0},O_{1,1},O_{2,0},O_{2,1}\}
{O0,0,O0,1,O1,0,O1,1,O2,0,O2,1}所选择的机器。比如索引为2的位置元素是1,表示工序
O
1
,
0
O_{1,0}
O1,0选择了机器1。
os编码也用一个列表表示,由一组工件的作业编号组成,编号出现的次数对应了该作业在当前位置应该被加工的工序。比如
{
2
,
1
,
0
,
0
,
2
,
1
}
\{2,1,0,0,2,1\}
{2,1,0,0,2,1}表示加工顺序为
{
O
2
,
0
,
O
1
,
0
,
O
0
,
0
,
O
0
,
1
,
O
2
,
1
,
O
1
,
1
}
\{O_{2,0},O_{1,0},O_{0,0},O_{0,1},O_{2,1},O_{1,1}\}
{O2,0,O1,0,O0,0,O0,1,O2,1,O1,1}。
ms编码和os编码共同描述了一个完整的调度计划。可以通过对这两个编码进行解码得到一个调度安排。以下是本文的遗传算法的解码过程的代码:
/**
* 用于将一个个体的编码变成一个调度计划
*/
public ScheduleResult getScheduleResult(int[] msCode, int[] osCode, DataManager dataManager){
//对每个工序出现的次数进行计数
int[] operationOrderCount=new int[dataManager.getJobNum()];
//机器
ArrayList<Machine> machines=new ArrayList<>(dataManager.getMachineNum());
for (int i = 0; i < dataManager.getMachineNum(); i++) {
machines.add(new Machine(i,new ArrayList<>()));
}
//作业
ArrayList<Job> jobs=new ArrayList<>(dataManager.getJobNum());
for (int i = 0; i < dataManager.getJobNum(); i++) {
jobs.add(new Job(i,new ArrayList<>()));
}
for (int jobNumber : osCode) {
//获得工序编号
String operationCode = jobNumber + "-" + operationOrderCount[jobNumber];
//该工序选择的机器的编号
int csCodeIndex = dataManager.getOperationCodeAndIndexMap().get(operationCode);
int machineNumber = msCode[csCodeIndex];
//该工序在机器上的处理时间
int processTime = dataManager.getOperationProcessTimeMap().get(operationCode + "-" + machineNumber);
Machine machine = machines.get(machineNumber);
Job job = jobs.get(jobNumber);
//判断作业紧前工序和机器紧前工序的完工时间哪个最晚
int latestCompleteTime = Math.max(machine.getCompleteTime(), job.getCompleteTime());
//封装一个工序
Operation operation = new Operation(jobNumber, operationOrderCount[jobNumber], operationCode,
latestCompleteTime, latestCompleteTime + processTime,machineNumber,processTime);
//更新机器序列和机器的最大完工时间
machine.getOperations().add(operation);
machine.setCompleteTime(operation.getCompleteTime());
//更新作业序列和作业的最大完工时间
job.getOperations().add(operation);
job.setCompleteTime(operation.getCompleteTime());
//作业工序次数计数
operationOrderCount[jobNumber]++;
}
//获得该调度计划的最大完工时间(makespan)
int makespan=machines.stream().max(Comparator.comparing(Machine::getCompleteTime)).orElseThrow().getCompleteTime();
return new ScheduleResult(machines,jobs,makespan);
}
2.选择
采用精英选择和锦标赛选择。首先使用精英选择策略选出一定数量的个体,再用锦标赛选择补足选择余额。
3.交叉和变异
ms编码的交叉很简单,采用两点交叉操作,即每次随机产生两个交叉点,将父代个体编码中不同片段的元素依次分配给子代个体,产生两个包含了父代信息的新个体。由于工序在 ms 编码中对应的位置保持不变,两点交叉得到的结果一定是可行的。

主要有两种交叉算子,POX和JOX。将两个父代分别称为P1和P2,两个子代分别称为实现过程分别如下:
POX:
- 作业集合 J = { J 1 , . . . , J n } J=\{J_1,...,J_n\} J={J1,...,Jn}随机分为Jobset1和Jobset2。
- P1属于Jobset1的所有元素移除,添加到O1的对应位置,P2属于Jobset1的所有元素移除,添加到O2的对应位置。
- P2剩余位置的元素依次添加到O1还未被添加的位置,P1剩余位置的元素依次添加到O2还未被添加的位置。
JBX:
- 作业集合 J = { J 1 , . . . , J n } J=\{J_1,...,J_n\} J={J1,...,Jn}随机分为Jobset1和Jobset2。
- P1中属于Jobset1的任何元素都被添加到O1中的相同位置,并在P1中删除;P2中任何属于Jobset2的元素都被添加到O2中的相同位置,并在P2中删除。
- P2中剩余的元素被附加到O1序列中剩余的空位置;P1中剩余的元素被附加到O2序列中剩余的空位置。

/**
* ms编码交叉
*/
public void msx(int operationNum,int[] p1msCode,int[] p2msCode,int[] o1msCode,int[] o2msCode){
int random1 = random.nextInt(operationNum - 1);
int random2 = random.nextInt(operationNum - random1) + random1;
for (int i = 0; i < operationNum; i++) {
//ms交叉
if (random1 <= i && i < random2) {
//第二段,p1给o2,p2给o1
o1msCode[i] = p2msCode[i];
o2msCode[i] = p1msCode[i];
} else {
//第一段或第三段,p1给o1,p2给o2
o1msCode[i] = p1msCode[i];
o2msCode[i] = p2msCode[i];
}
}
}
/**
* os编码交叉算子——POX
*/
public void pox(int operationNum,int[] p1osCode,int[] p2osCode,int[] o1osCode,int[] o2osCode,
Set<Integer> jobPartOne,Set<Integer> jobPartTwo){
//第一次遍历完成之后,o1、o2未被使用的位置
ArrayList<Integer> o1osIndexNotUse = new ArrayList<>(), o2osIndexNotUse = new ArrayList<>();
for (int i = 0; i < operationNum; i++) {
//os部分交叉
if (jobPartOne.contains(p1osCode[i])) {
o1osCode[i] = p1osCode[i];
} else {
o1osIndexNotUse.add(i);
}
if (jobPartOne.contains(p2osCode[i])) {
o2osCode[i] = p2osCode[i];
} else {
o2osIndexNotUse.add(i);
}
}
//完成os的交叉
int index2 = 0, index1 = 0;
for (int i = 0; i < operationNum; i++) {
if (jobPartTwo.contains(p1osCode[i])) {
o2osCode[o2osIndexNotUse.get(index2)] = p1osCode[i];
index2++;
}
if (jobPartTwo.contains(p2osCode[i])) {
o1osCode[o1osIndexNotUse.get(index1)] = p2osCode[i];
index1++;
}
}
}
/**
* os编码交叉算子——JBX
*/
public void jbx(int operationNum,int[] p1osCode,int[] p2osCode,int[] o1osCode,int[] o2osCode,
Set<Integer> jobPartOne,Set<Integer> jobPartTwo){
//第一次遍历完成之后,o1、o2未被使用的位置
ArrayList<Integer> o1osIndexNotUse = new ArrayList<>(), o2osIndexNotUse = new ArrayList<>();
for (int i = 0; i < operationNum; i++) {
//os部分交叉
if (jobPartOne.contains(p1osCode[i])) {
o1osCode[i] = p1osCode[i];
} else {
o1osIndexNotUse.add(i);
}
if (jobPartTwo.contains(p2osCode[i])) {
o2osCode[i] = p2osCode[i];
} else {
o2osIndexNotUse.add(i);
}
}
//完成os的交叉
int index2 = 0, index1 = 0;
for (int i = 0; i < operationNum; i++) {
if (jobPartOne.contains(p1osCode[i])) {
o2osCode[o2osIndexNotUse.get(index2)] = p1osCode[i];
index2++;
}
if (jobPartTwo.contains(p2osCode[i])) {
o1osCode[o1osIndexNotUse.get(index1)] = p2osCode[i];
index1++;
}
}
}
4.完整源码
完整代码请进入仓库查看
https://gitee.com/xcy-ghl/tradition_fjsp.git
https://gitee.com/xcy-ghl/tradition_fjsp.git
5.实现效果和可视化
参数设置为:个体3000个,精英选择1050个,交叉概率0.95,变异概率0.1,迭代200代,50代无更优解结束算法
使用测试数据BRData进行测试:
| 算例名称 | 计算结果(makespan) | 计算时间(ms) |
|---|---|---|
| MK01 | 42 | 6086 |
| MK02 | 28 | 15459 |
| MK03 | 204 | 27065 |
| MK04 | 67 | 36921 |
| MK05 | 175 | 52248 |
| MK06 | 71 | 70791 |
| MK07 | 144 | 85989 |
| MK08 | 524 | 106828 |
| MK09 | 320 | 134959 |
| MK10 | 237 | 164320 |
另外,遗传算法非常适合并行处理。在解码和交叉的过程中,可以开启多条线程同时进行解码和两个个体的交叉以提升算法速度。
可视化代码请见我的另一篇文章:
【车间调度】车间调度甘特图可视化(Java源码)
MK09的可视化效果:

参考文献
[1] X. Li, L. Gao. An effective hybrid genetic algorithm and tabu search for flexible job shop scheduling problem. International Journal of Production Economics, 2016, 174: 93-110
本文介绍了一种基于遗传算法解决柔性作业车间调度问题的方法。通过双向量编码(ms和os)表示调度计划,并详细阐述了解码过程、选择策略、交叉变异操作等遗传算法的关键步骤。
500

被折叠的 条评论
为什么被折叠?



