网上关于禁忌搜索的描述有很多。这里我就不进行赘述了。
禁忌搜索流程
说一下我自己实现禁忌搜索算法的流程。
- 生成初始解,并将初始解赋值给当前解和最优解
- 对当前解进行局部邻域搜索(搜索当前解的邻居)。搜索过程中,如果一个解存在于禁忌表中,不计算该解的函数值,最后会找到一个局部最优解(并不是找到比当前解更优的解,而是找到当前解的邻居中的最优解)。
- 将局部最优解赋值给当前解。
- (特赦准则)判断禁忌表中的某一个解是否优于当前解?若是,将当前解禁忌,将该解解禁赋给当前解,若否,直接将当前解禁忌(如果此时禁忌表达到禁忌长度,解禁第一个对象,将该解放入禁忌表尾部)
- 判断全局最优是否需要进行更新
- 循环上边过程,达到迭代次数之后退出。
本文进行邻域搜索的过程中,产生邻居解的方式是k-insertion。具体过程可以看以下文章:
【车间调度】论文阅读复现——effective neighbourhood functions for the flexible job shop problem
Java源码
代码的项目结构:
完整代码请进入仓库查看(如果觉得有用的话,留下一个star吧~):
https://gitee.com/xcy-ghl/tradition_fjsp.git
https://gitee.com/xcy-ghl/tradition_fjsp.git
这里只贴出主要代码(k-insertion和禁忌搜索):
/**
* 根据插入信息,将v插入机器k中
* @param insertInformation 插入信息,第一个元素为移除的关键节点的number,第二个元素为关键节点选择的机器k,第三个元素为在机器中选择的位置
* @return 插入完毕的析取图
*/
public DisjunctiveGraph kInsertion(DisjunctiveGraph graph, DataManager dataManager, int[] insertInformation) {
List<DisjunctiveGraphNode> nodes = graph.getNodes();
int vNumber=insertInformation[0];
DisjunctiveGraphNode v = nodes.get(vNumber);
int k=insertInformation[1];
int insertIndex=insertInformation[2];
int jobPreV=insertInformation[3];
int jobNextV=insertInformation[4];
List<Integer> qk = graph.getMachineAndNodeListMap().get(k);
List<Integer> vMachineSeq = graph.getMachineAndNodeListMap().get(v.getMachineNum());
int vIndexInMachine = searchRemoveNodeInMachine(0, vMachineSeq.size() - 1, vMachineSeq, v.getNumber(), nodes);
//在插入之前,需要先将v移除原本的机器
if (vIndexInMachine != 0) {
int machinePreV = vMachineSeq.get(vIndexInMachine - 1);
// System.out.println("机器紧前工序移除前:"+nodes.get(machinePreV));
if (machinePreV != jobPreV) {
if (!nodes.get(machinePreV).getNextNodes().removeIf(nextNode -> nextNode == v.getNumber())) {
throw new IllegalStateException("not expecting to arrive here");
}
if (!v.getPreNodes().removeIf(preNode -> preNode == nodes.get(machinePreV).getNumber())) {
throw new IllegalStateException("not expecting to arrive here");
}
}
// System.out.println("机器紧前工序移除后:"+nodes.get(machinePreV));
}
if (vIndexInMachine != vMachineSeq.size() - 1) {
int machineNextV = vMachineSeq.get(vIndexInMachine + 1);
// System.out.println("机器紧后工序移除前:"+nodes.get(machineNextV));
if (machineNextV != jobNextV) {
if (!nodes.get(machineNextV).getPreNodes().removeIf(preNode -> preNode == v.getNumber())) {
throw new IllegalStateException("not expecting to arrive here");
}
if (!v.getNextNodes().removeIf(nextNode -> nextNode == nodes.get(machineNextV).getNumber())) {
throw new IllegalStateException("not expecting to arrive here");
}
}
// System.out.println("机器紧后工序移除后:"+nodes.get(machineNextV));
}
if (vIndexInMachine != 0 && vIndexInMachine != vMachineSeq.size() - 1) {
DisjunctiveGraphNode machinePreV = nodes.get(vMachineSeq.get(vIndexInMachine - 1));
DisjunctiveGraphNode machineNextV = nodes.get(vMachineSeq.get(vIndexInMachine + 1));
if (machinePreV.getJobNum() != machineNextV.getJobNum() || machinePreV.getNumber() + 1 != machineNextV.getNumber()) {
machinePreV.getNextNodes().add(machineNextV.getNumber());
machineNextV.getPreNodes().add(machinePreV.getNumber());
}
}
vMachineSeq.remove(vIndexInMachine);
//将v插入k中选中的位置,断开insertIndex前后工序的机器弧,并重连v与机器紧前/紧后工序(若有且不是作业紧前紧后工序)机器弧,并重新设置机器、权重
//断开
qk.add(insertIndex, v.getNumber());
if (insertIndex !=0&&insertIndex !=qk.size()-1) {
DisjunctiveGraphNode machinePreNode = nodes.get(qk.get(insertIndex - 1));
DisjunctiveGraphNode machineNextNode = nodes.get(qk.get(insertIndex + 1));
if (machinePreNode.getJobNum() != machineNextNode.getJobNum() || machinePreNode.getNumber() + 1 != machineNextNode.getNumber()) {
//不是相邻的同作业工序,断开它们之间的连接弧
if (!machinePreNode.getNextNodes().removeIf(nextNode -> nodes.get(nextNode).getNumber() == machineNextNode.getNumber())) {
throw new IllegalStateException("not expecting to arrive here");
}
if (!machineNextNode.getPreNodes().removeIf(preNode -> nodes.get(preNode).getNumber() == machinePreNode.getNumber())) {
throw new IllegalStateException("not expecting to arrive here");
}
}
}
//重连
if (insertIndex !=0) {
//前一个工序存在,如果前一个工序跟当前工序不是相邻的同作业工序,进行连接
DisjunctiveGraphNode machinePreNode = nodes.get(qk.get(insertIndex - 1));
if (machinePreNode.getJobNum() != v.getJobNum() || machinePreNode.getNumber() + 1 != v.getNumber()) {
machinePreNode.getNextNodes().add(v.getNumber());
v.getPreNodes().add(machinePreNode.getNumber());
}
}
if (insertIndex !=qk.size()-1) {
//后一个工序存在,如果后一个工序跟当前工序不是相邻的同作业工序,进行连接
DisjunctiveGraphNode machineNextNode = nodes.get(qk.get(insertIndex + 1));
if (machineNextNode.getJobNum() != v.getJobNum() || machineNextNode.getNumber() - 1 != v.getNumber()) {
machineNextNode.getPreNodes().add(v.getNumber());
v.getNextNodes().add(machineNextNode.getNumber());
}
}
v.setMachineNum(k);
Integer newProcessTime = dataManager.getOperationProcessTimeMap().get(v.getOperationCode() + "-" + k);
v.setProcessTime(newProcessTime);
nodes.forEach(node->{
node.setLatestPreNode(null);
node.setStartTime(0);
node.setTailTime(0);
});
//重新计算所有节点的头长度 尾长度、关键路径
calStartTime(nodes.get(0), nodes, new boolean[nodes.size()], false);
calTailTime(nodes.get(nodes.size() - 1), nodes, new boolean[nodes.size()], false);
List<Integer> newCriticalPath = new ArrayList<>();
getCriticalPath(nodes.get(nodes.size() - 1), nodes, newCriticalPath);
graph.setInsertInfo(insertInformation);
graph.setCriticalPath(newCriticalPath);
graph.setMakespan(nodes.get(nodes.size() - 1).getStartTime());
// nodes.forEach(System.out::println);
// System.out.println(graph.getMakespan());
return graph;
}
/**
* 在析取图的关键路径上随机选择一个节点,将其从机器中移除,选择另一个机器k,选择一个位置插入
* @return 数组,第一个元素为移除的关键节点的number,第二个元素为关键节点选择的机器k,第三个元素为在机器中选择的位置
*/
public int[] getInsertInfo(DisjunctiveGraph graph, DataManager dataManager){
List<DisjunctiveGraphNode> nodes = graph.getNodes();
Map<Integer, List<Integer>> jobMap = graph.getJobAndNodeListMap();
//随机选择关键路径上的一个节点
int randomIndex = random.nextInt(graph.getCriticalPath().size());
DisjunctiveGraphNode v = nodes.get(graph.getCriticalPath().get(randomIndex));
List<Integer> vJobSeq = jobMap.get(v.getJobNum());
int vIndexInJob = searchRemoveNodeInJob(0, vJobSeq.size() - 1, vJobSeq, v.getNumber());
int jobPreV = vIndexInJob == 0 ? nodes.get(0).getNumber() : vJobSeq.get(vIndexInJob - 1);
int jobNextV = vIndexInJob == vJobSeq.size() - 1 ? nodes.get(nodes.size() - 1).getNumber() : vJobSeq.get(vIndexInJob + 1);
//工序v在子图中的头长度和尾长度此时为作业紧前工序的头长度+作业紧前工序处理时间,作业紧后工作的尾长度+作业紧后工序处理时间
int vStartTime = nodes.get(jobPreV).getStartTime() + nodes.get(jobPreV).getProcessTime();
int vTailTime = nodes.get(jobNextV).getTailTime() + nodes.get(jobNextV).getProcessTime();
//重新为v随机选择一个机器k
List<Integer> vOptionalMachines = dataManager.getOperationCodeAndMachineListMap().get(v.getOperationCode());
Integer k = vOptionalMachines.get(random.nextInt(vOptionalMachines.size()));
List<Integer> qk = new ArrayList<>(graph.getMachineAndNodeListMap().get(k));
//v刚好选中了原来的机器,qk中要除去v
qk.removeIf(nodeNumber->nodeNumber==v.getNumber());
//用二分查找获得v在k中可插入的范围lk和rk
int leftIndex = 0, rightIndex = qk.size();
if (!qk.isEmpty() && vTailTime < nodes.get(qk.get(0)).getTailTime() + nodes.get(qk.get(0)).getProcessTime()) {
leftIndex = searchLk(0, qk.size() - 1, qk, vTailTime, nodes);
}
if (!qk.isEmpty() && vStartTime < nodes.get(qk.get(qk.size() - 1)).getStartTime() + nodes.get(qk.get(qk.size() - 1)).getProcessTime()) {
rightIndex = searchRk(0, qk.size() - 1, qk, vStartTime, nodes);
}
//选择v在k中插入的位置
int insertIndex;
if (rightIndex < leftIndex) {
//此时lk和rk有交集,可插入的位置为[rightIndex,leftIndex]
insertIndex = random.nextInt(leftIndex - rightIndex + 1) + rightIndex;
} else {
//此时lk和rk无交集,可插入的位置为[leftIndex,rightIndex]
insertIndex = random.nextInt(rightIndex - leftIndex + 1) + leftIndex;
}
return new int[]{v.getNumber(),k,insertIndex,jobPreV,jobNextV};
}
/**
* 在qk中寻找Lk最后一个工序的索引
*
* @return lk最后一个工序索引+1
*/
private int searchLk(int start, int end, List<Integer> qk, int vTailTime, List<DisjunctiveGraphNode> nodes) {
if (start == end) {
int xTailTime = nodes.get(qk.get(start)).getTailTime();
int xProcessTime = nodes.get(qk.get(start)).getProcessTime();
if (vTailTime == xTailTime + xProcessTime) {
//如果找到,lk的要求是tx+px>tv,所以lk最后一道工序的索引是start-1
return start;
} else {
//如果没有找到,当前索引是target最近的左边,刚好是lk的最后一道工序
return start + 1;
}
}
int middle = (int) Math.ceil((start + end) / 2.0);
if (vTailTime <= nodes.get(qk.get(middle)).getTailTime() + nodes.get(qk.get(middle)).getProcessTime()) {
return searchLk(middle, end, qk, vTailTime, nodes);
} else {
return searchLk(start, middle - 1, qk, vTailTime, nodes);
}
}
/**
* 在k中寻找Rk第一个工序的索引
*/
private int searchRk(int start, int end, List<Integer> qk, int vStartTime, List<DisjunctiveGraphNode> nodes) {
if (start == end) {
int xStartTime = nodes.get(qk.get(start)).getStartTime();
int xProcessTime = nodes.get(qk.get(start)).getProcessTime();
if (vStartTime == xStartTime + xProcessTime) {
//如果找到,此时sx+px=sv,但是Rk的第一道工序要求sx+px>sv,所以第一道工序的索引是start+1
return start + 1;
} else {
//没有找到,此时找到的索引是target最近的右边,刚好是Rk的第一道工序
return start;
}
}
int middle = (start + end) / 2;
if (nodes.get(qk.get(middle)).getStartTime() + nodes.get(qk.get(middle)).getProcessTime() >= vStartTime) {
return searchRk(start, middle, qk, vStartTime, nodes);
} else {
return searchRk(middle + 1, end, qk, vStartTime, nodes);
}
}
/**
* 在vJobSeq中找到target的索引
*/
private int searchRemoveNodeInJob(int start, int end, List<Integer> vJobSeq, int target) {
if (start == end) {
if (vJobSeq.get(start) == target) {
return start;
} else {
throw new IllegalStateException("not expecting to arrive here");
}
}
int middle = (start + end) / 2;
if (vJobSeq.get(middle) >= target) {
return searchRemoveNodeInJob(start, middle, vJobSeq, target);
} else {
return searchRemoveNodeInJob(middle + 1, end, vJobSeq, target);
}
}
/**
* 在vMachineSeq中找到target的索引
*/
private int searchRemoveNodeInMachine(int start, int end, List<Integer> vMachineSeq, int target, List<DisjunctiveGraphNode> nodes) {
if (start == end) {
if (vMachineSeq.get(start) == target) {
return start;
} else {
throw new IllegalStateException("not expecting to arrive here");
}
}
int middle = (start + end) / 2;
if (nodes.get(vMachineSeq.get(middle)).getStartTime() >= nodes.get(target).getStartTime()) {
return searchRemoveNodeInMachine(start, middle, vMachineSeq, target, nodes);
} else {
return searchRemoveNodeInMachine(middle + 1, end, vMachineSeq, target, nodes);
}
}
/**
* 根据可行调度计划生成析取图(遗传算法的调度计划结果转化为析取图,以方便进行禁忌搜索)
*/
public DisjunctiveGraph convertScheduleToGraph(ScheduleResult scheduleResult, DataManager dataManager) {
List<DisjunctiveGraphNode> nodes = new ArrayList<>();
//先根据作业优先顺序生成有向图
ArrayList<Job> jobs = scheduleResult.getJobs();
int nodeNumber = 0;
DisjunctiveGraphNode endNode = new DisjunctiveGraphNode("end", dataManager.getOperationNum() + 1, new ArrayList<>(), new ArrayList<>());
endNode.setMachineNum(-1);
endNode.setJobNum(-1);
for (int i = 0; i < jobs.size(); i++) {
if (i == 0) {
//加入虚拟开始节点
DisjunctiveGraphNode startNode = new DisjunctiveGraphNode("start", nodeNumber, new ArrayList<>(), new ArrayList<>());
startNode.setMachineNum(-1);
startNode.setJobNum(-1);
nodes.add(startNode);
nodeNumber++;
}
Job job = jobs.get(i);
for (int j = 0; j < job.getOperations().size(); j++) {
Operation operation = job.getOperations().get(j);
DisjunctiveGraphNode node = new DisjunctiveGraphNode(operation.getCode(), nodeNumber, new ArrayList<>(), new ArrayList<>());
node.setJobNum(operation.getJobNumber());
node.setMachineNum(operation.getMachineNum());
node.setProcessTime(operation.getProcessTime());
if (j == 0) {
//作业的第一道工序的前驱节点必定是虚拟开始节点
node.getPreNodes().add(nodes.get(0).getNumber());
nodes.get(0).getNextNodes().add(node.getNumber());
} else {
DisjunctiveGraphNode preNode = nodes.get(nodes.size() - 1);
preNode.getNextNodes().add(node.getNumber());
node.getPreNodes().add(preNode.getNumber());
}
if (j == job.getOperations().size() - 1) {
//作业的最后一道工序必定连向虚拟结束节点
node.getNextNodes().add(endNode.getNumber());
endNode.getPreNodes().add(node.getNumber());
}
nodes.add(node);
nodeNumber++;
}
}
nodes.add(endNode);
//接着加入机器的析取弧
for (Machine machine : scheduleResult.getMachines()) {
for (int i = 0; i < machine.getOperations().size() - 1; i++) {
Operation operation = machine.getOperations().get(i);
Operation nextOperation = machine.getOperations().get(i + 1);
//得到当前工序和下一个工序对应的节点
DisjunctiveGraphNode node = nodes.get(dataManager.getOperationCodeAndIndexMap().get(operation.getCode()) + 1);
DisjunctiveGraphNode nextNode = nodes.get(dataManager.getOperationCodeAndIndexMap().get(nextOperation.getCode()) + 1);
//当前工序和下一个工序属于同一个作业的前后工序,不用添加机器弧
if (node.getJobNum() != nextNode.getJobNum() || node.getNumber() + 1 != nextNode.getNumber()) {
node.getNextNodes().add(nextNode.getNumber());
nextNode.getPreNodes().add(node.getNumber());
}
}
}
//计算每个节点的头长度和尾长度
calStartTime(nodes.get(0), nodes, new boolean[nodes.size()], false);
calTailTime(nodes.get(nodes.size() - 1), nodes, new boolean[nodes.size()], false);
//获得关键路径
List<Integer> criticalPath = new ArrayList<>();
getCriticalPath(nodes.get(nodes.size() - 1), nodes, criticalPath);
//按照机器号对节点进行分组,分组之后的机器序列需要按照升序排序
Map<Integer, List<Integer>> machineMap = nodes.stream().collect(Collectors.groupingBy(DisjunctiveGraphNode::getMachineNum,
Collectors.mapping(DisjunctiveGraphNode::getNumber, Collectors.toList())));
//析取图中的节点的机器并不全面,可能会漏掉一些机器,需要手动补上
for (int i = 0; i < dataManager.getMachineNum(); i++) {
if (!machineMap.containsKey(i)) {
machineMap.put(i, new ArrayList<>());
}
}
Map<Integer, List<Integer>> jobMap = nodes.stream().collect(Collectors.groupingBy(DisjunctiveGraphNode::getJobNum,
Collectors.mapping(DisjunctiveGraphNode::getNumber, Collectors.toList())));
jobMap.forEach((key, value) -> value.sort(Integer::compareTo));
machineMap.forEach((key, value) -> value.sort(Comparator.comparingInt(o -> nodes.get(o).getStartTime())));
return new DisjunctiveGraph(nodes, machineMap, jobMap, criticalPath, nodes.get(nodes.size() - 1).getStartTime(),new int[]{0});
}
/**
* 计算析取图中每一个节点的头长度
*
* @param node 虚拟开始节点
* @param visited 记录节点被访问的情况
* @param back 该节点是否是前驱节点
*/
private void calStartTime(DisjunctiveGraphNode node, List<DisjunctiveGraphNode> nodes, boolean[] visited, boolean back) {
if (visited[node.getNumber()]) {
//该节点被访问过,返回
return;
}
//访问所有未被访问过的前驱节点,并记录前驱节点之中的最迟完工时间
for (Integer preNode : node.getPreNodes()) {
if (!visited[preNode]) {
calStartTime(nodes.get(preNode), nodes, visited, true);
}
int startTime = nodes.get(preNode).getStartTime() + nodes.get(preNode).getProcessTime();
if (startTime >= node.getStartTime()) {
node.setStartTime(startTime);
node.setLatestPreNode(preNode);
}
}
//所有前驱节点被访问完毕,标记该节点被访问过
visited[node.getNumber()] = true;
//如果此时是前驱节点的搜索过程,返回到原本的节点
if (back) {
return;
}
//访问该节点的所有未被访问过的后继节点
for (Integer nextNode : node.getNextNodes()) {
if (!visited[nextNode]) {
calStartTime(nodes.get(nextNode), nodes, visited, false);
}
}
}
/**
* 计算析取图中每一个节点的尾长度
*
* @param node 虚拟结束节点
* @param visited 记录节点是否被访问过
* @param forward 该节点是否是后继节点
*/
private void calTailTime(DisjunctiveGraphNode node, List<DisjunctiveGraphNode> nodes, boolean[] visited, boolean forward) {
if (visited[node.getNumber()]) {
//该节点被访问过,返回
return;
}
//访问所有未被访问过的后继节点,并记录后继节点之中的最大尾长度
for (Integer nextNode : node.getNextNodes()) {
if (!visited[nextNode]) {
calTailTime(nodes.get(nextNode), nodes, visited, true);
}
node.setTailTime(Math.max(node.getTailTime(), nodes.get(nextNode).getTailTime() + nodes.get(nextNode).getProcessTime()));
}
//所有后继节点被访问完毕,标记该节点被访问过
visited[node.getNumber()] = true;
//如果此时是后继节点的搜索过程,返回到原本的节点
if (forward) {
return;
}
//访问该节点的所有未被访问过的前驱节点
for (Integer preNode : node.getPreNodes()) {
if (!visited[preNode]) {
calTailTime(nodes.get(preNode), nodes, visited, false);
}
}
}
/**
* 获得析取图的关键路径
*
* @param node 第一次输入需要是析取图的虚拟结束节点
* @param criticalPath 传入一个空的列表,该方法执行完毕后列表会按开始时间为序填充关键节点
*/
private void getCriticalPath(DisjunctiveGraphNode node, List<DisjunctiveGraphNode> nodes, List<Integer> criticalPath) {
if (null == node.getLatestPreNode()) {
//到达头节点,为了之后方便,开始节点不要添加入关键路径
return;
}
//进入上一个节点
getCriticalPath(nodes.get(node.getLatestPreNode()), nodes, criticalPath);
if (!"end".equals(node.getOperationCode())) {
//为了之后方便,结束节点不要添加入关键路径
criticalPath.add(node.getNumber());
}
}
实现效果及可视化
参数设置为:禁忌表长50,迭代5000,局部搜索次数70,最大允许无更优解代数500
使用测试数据BRData进行测试:
算例名称 | 计算结果(makespan) | 计算时间(ms) |
---|---|---|
MK01 | 40 | 557 |
MK02 | 28 | 796 |
MK03 | 204 | 851 |
MK04 | 64 | 980 |
MK05 | 183 | 881 |
MK06 | 63 | 2468 |
MK07 | 166 | 993 |
MK08 | 537 | 1835 |
MK09 | 311 | 5001 |
MK10 | 212 | 10194 |
可视化代码请见我的另一篇文章:
【车间调度】车间调度甘特图可视化(Java源码)
算例MK10的可视化效果:
看了一下其他文献,有一些算例的最优解没有求到,可能跟参数设置、计算时间和实现方式(我没加特赦准则)有关。k-insertion有一种计算近似关键路径长度来将时间复杂度从 O ( N ) O(N) O(N)降到 O ( l o g N ) O(logN) O(logN)的方式我也没有应用,所以这个算法还是比较慢的(特别是之后跟遗传算法结合,禁忌搜索一次就要几百毫秒实在是太慢了)。之后有空再优化一下。