【车间调度】遗传算法求解柔性作业车间调度问题(Java源码)

本文介绍了一种基于遗传算法解决柔性作业车间调度问题的方法。通过双向量编码(ms和os)表示调度计划,并详细阐述了解码过程、选择策略、交叉变异操作等遗传算法的关键步骤。

本文是对论文[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)
MK01426086
MK022815459
MK0320427065
MK046736921
MK0517552248
MK067170791
MK0714485989
MK08524106828
MK09320134959
MK10237164320

另外,遗传算法非常适合并行处理。在解码和交叉的过程中,可以开启多条线程同时进行解码和两个个体的交叉以提升算法速度。

可视化代码请见我的另一篇文章:
【车间调度】车间调度甘特图可视化(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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值