这个项目运用到了遗传算法, 但是是改进版本(变异), 接下来我们分析一下算法流程
首先导入开课任务, 这一批课程有哪些, 老师是谁, 一个班有多少人等等
根据排课计划, 开始排课
将这些排课数据进行编码, 课程划分为固定时间的和不固定时间的, 输出染色体
染色体格式如下
示例: 2 01 20200101 10041 100051 04 14
数据表示例
id | semester | grade_no | class_no | course_no | course_name | teacher_no | realname | courseAttr | studentNum | weeks_sum | weeks_number | isFix | class_time | deleted | create_time | update_time |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
63 | 2019-2020-1 | 01 | 20200201 | 100005 | 高二语文必修5 | 10019 | 汪莉莉 | 01 | 45 | 20 | 6 | 1 | null | 0 | 2020-06-11 10:25:39 | null |
64 | 2019-2020-1 | 01 | 20200201 | 100017 | 高二化学选修2 | 10050 | 黄三毛 | 02 | 45 | 20 | 4 | 1 | null | 0 | 2020-06-11 10:25:40 | null |
65 | 2019-2020-1 | 01 | 20200201 | 100029 | 高二生物必修3:稳态与环境 | 10031 | 张小龙 | 02 | 45 | 20 | 4 | 1 | null | 0 | 2020-06-11 10:25:40 | null |
11 | 2019-2020-1 | 01 | 20200101 | 100066 | 物理实验 | 10025 | 张德良 | 03 | 40 | 20 | 2 | 2 | 04 | 0 | 2020-06-11 10:25:36 | null |
23 | 2019-2020-1 | 01 | 20200102 | 100066 | 物理实验 | 10025 | 张德良 | 03 | 40 | 20 | 2 | 2 | 09 | 0 | 2020-06-11 10:25:37 | null |
10 | 2019-2020-1 | 01 | 20200101 | 100051 | 体育课 | 10041 | 张杰 | 04 | 40 | 20 | 2 | 2 | 14 | 0 | 2020-06-11 10:25:36 | null |
接下来, 给不固定的课程编码随机分配时间(上一步不固定时间的课堂时间编码默认为 00
)
将分配好时间的基因编码以班级分类成为以班级的个体(Map<String, List<String>>
, 以班级id为key,班级课程编码list为value)
开始进化
迭代 50 次, 每一次都进行 选择, 交叉, 变异, 然后消除冲突继续进化
private Map<String, List<String>> geneticEvolution(Map<String, List<String>> individualMap) {
// 遗传代数
int generation = ConstantInfo.GENERATION;
List<String> resultGeneList;
for (int i = 0; i < generation; ++i) {
// 1、选择、交叉individualMap:按班级分的课表
individualMap = hybridization(individualMap);
// 2,3、变异
resultGeneList = geneMutation(collectGene(individualMap));
// 4,5、将消除冲突后的个体再分班进入下一次进化
individualMap = transformIndividual(conflictResolution(resultGeneList));
}
return individualMap;
}
在选择的过程中, 我们需要比较适应度值
private Map<String, List<String>> hybridization(Map<String, List<String>> individualMap) {
// 对每一个班级的基因编码片段进行交叉
for (String classNo : individualMap.keySet()) {
// 得到每一个班级对应的基因编码
List<String> individualList = individualMap.get(classNo);
// 保存上一代
List<String> oldIndividualList = individualList;
// 交叉生成新个体,得到新生代
individualList = selectGene(individualList);
// 计算并对比子父代的适应度值,高的留下进行下一代遗传
if (ClassUtil.calculatExpectedValue(individualList) >= ClassUtil.calculatExpectedValue(oldIndividualList)) {
individualMap.put(classNo, individualList);
} else {
individualMap.put(classNo, oldIndividualList);
}
}
return individualMap;
}
这里的适应度值主要通过课程的期望值获得, 期望值指的是每种课程在每种时间开展的适合度, 越适合期望值越高
public static double calculatExpectedValue(List<String> individualList) {
double K1 = 0.3; // 主要课所占权重
double K2 = 0.1; // 次要课所占权重
double K3 = 0.1; // 体育课所占权重
double K4 = 0.3; // 实验课所占权重
double K5 = 0.2; // 课程离散程度所占权重
int F1 = 0; // 主要课程期望总值
int F2 = 0; // 次要课程期望总值
int F3 = 0; // 体育课期望总值
int F4 = 0; // 实验课期望总值
int F5; // 课程离散程度期望总值
double Fx; // 总适应度值
// 开始计算每一个个体的适应度
for (String gene : individualList) {
// 获得课程属性
String courseAttr = cutGene(ConstantInfo.COURSE_ATTR, gene);
// 获得该课程的开课时间
String classTime = cutGene(ConstantInfo.CLASS_TIME, gene);
if (courseAttr.equals(ConstantInfo.MAIN_COURSE)) {
F1 = F1 + calculateMainExpect(classTime);
} else if (courseAttr.equals(ConstantInfo.SECONDARY_COURSE)) {
F2 = F2 + calculateSecondaryExpect(classTime);
} else if (courseAttr.equals(ConstantInfo.PHYSICAL_COURSE)) {
F3 = F3 + calculatePhysicalExpect(classTime);
} else {
F4 = F4 + calculateExperimentExpect(classTime);
}
}
// 计算期望值
F5 = calculateDiscreteExpect(individualList);
// 总适应度
Fx = K1 * F1 + K2 * F2 + K3 * F3 + K4 * F4 + K5 * F5;
return Fx; // 整个种群的适应度值
}
我们以主要课的期望计算来分析
private static int calculateMainExpect(String classTime) {
// 主要课程期望值为10时的时间片值,放在第一节课
String[] tenExpectValue = {"01", "06", "11", "16", "21"};
// 主要课程期望值为8时的时间片值
String[] eightExpectValue = {"02", "07", "12", "17", "22"};
// 主要课程期望值为4时的时间片值
String[] fourExpectValue = {"03", "08", "13", "18", "23"};
// 主要课程期望值为2时的时间片值
String[] twoExpectValue = {"04", "09", "14", "19", "24"};
// 主要课程期望值为0时的时间片值
//String [] zeroExpectValue = {"05","10","15","20","25"};
if (ArrayUtils.contains(tenExpectValue, classTime)) {
return 10;
} else if (ArrayUtils.contains(eightExpectValue, classTime)) {
return 10;
} else if (ArrayUtils.contains(fourExpectValue, classTime)) {
return 4;
} else if (ArrayUtils.contains(twoExpectValue, classTime)) {
return 2;
} else {
return 0;
}
}
当课程在第一节课的时候, 其期望是最高的(一周5天, 每天5节课)
期望中, 有一个 F5
值是最复杂的, 这个值是课程离散度期望值
private static int calculateDiscreteExpect(List<String> individualList) {
// 离散程度期望值
int F5 = 0;
// 返回每个班级的对应课程下面的排序上课时间
Map<String, List<String>> classTimeMap = courseGrouping(individualList);
for (List<String> classTimeList : classTimeMap.values()) {
if (classTimeList.size() > 1) {
for (int i = 0; i < classTimeList.size() - 1; ++i) {
// 计算一门课上课的时间差
int temp = Integer.parseInt(classTimeList.get(++i)) - Integer.parseInt(classTimeList.get(i - 1));
F5 = F5 + judgingDiscreteValues(temp);
}
}
}
return F5;
}
首先, courseGrouping 计算出该班级每门课排课的时间片, 并且进行排序
根据每门课程之间相差的时间, 进行期望计算
/**
* 判断两课程的时间差在哪个区间
* 并返回对应的期望值
*/
private static int judgingDiscreteValues(int temp) {
int[] tenExpectValue = {5, 6, 7, 8}; // 期望值为10时两课之间的时间差
int[] sixExpectValue = {4, 9, 10, 11, 12, 13}; // 期望值为6时两课之间的时间差
int[] fourExpectValue = {3, 14, 15, 16, 17, 18}; // 期望值为4时两课之间的时间差
int[] twoExpectValue = {2, 19, 20, 21, 22, 23}; // 期望值为2时两课之间的时间差
//int [] zeroExpectValue = {1,24};//期望值为0时两课之间的时间差
if (ArrayUtils.contains(tenExpectValue, temp)) {
return 10;
} else if (ArrayUtils.contains(sixExpectValue, temp)) {
return 6;
} else if (ArrayUtils.contains(fourExpectValue, temp)) {
return 4;
} else if (ArrayUtils.contains(twoExpectValue, temp)) {
return 2;
} else {
return 0;
}
}
最后进行加权, 得到适应度值, 值越大, 这个效果越好
// 总适应度
Fx = K1 * F1 + K2 * F2 + K3 * F3 + K4 * F4 + K5 * F5;
选择是一个班级的课程(通常是20节课), 交换上课时间
变异是让所有课程(所有班级课程的总和)的随机一些课程时间发生改变
上述两个操作均未考虑冲突
接下来考虑冲突问题
冲突在不考虑教室的情况下, 主要是一个老师不能再同一时间上多门课, 一旦出现这个问题, 随机一个时间, 再次进行冲突检查
/**
* 冲突消除,同一个讲师同一时间上多门课。解决:重新分配一个时间,直到所有的基因编码中
* 不再存在上课时间冲突为止
* 因素:讲师-课程-时间-教室
* @param resultGeneList 所有个体集合
* @return
*/
private List<String> conflictResolution(List<String> resultGeneList) {
int conflictTimes = 0;
eitx:
for (int i = 0; i < resultGeneList.size(); i++) {
// 得到集合中每一条基因编码的编码信息
String gene = resultGeneList.get(i);
String teacherNo = ClassUtil.cutGene(ConstantInfo.TEACHER_NO, gene);
String classTime = ClassUtil.cutGene(ConstantInfo.CLASS_TIME, gene);
String classNo = ClassUtil.cutGene(ConstantInfo.CLASS_NO, gene);
for (int j = i + 1; j < resultGeneList.size(); j++) {
// 再找剩余的基因编码对比
String tempGene = resultGeneList.get(j);
String tempTeacherNo = ClassUtil.cutGene(ConstantInfo.TEACHER_NO, tempGene);
String tempClassTime = ClassUtil.cutGene(ConstantInfo.CLASS_TIME, tempGene);
String tempClassNo = ClassUtil.cutGene(ConstantInfo.CLASS_NO, tempGene);
// 冲突检测
if (classTime.equals(tempClassTime)) {
if (classNo.equals(tempClassNo) || teacherNo.equals(tempTeacherNo)) {
System.out.println("出现冲突情况");
conflictTimes ++;
String newClassTime = ClassUtil.randomTime(gene, resultGeneList);
String newGene = gene.substring(0, 24) + newClassTime;
resultGeneList = replace(resultGeneList, gene, newGene);
i = -1;
continue eitx; // 外循环
}
}
}
}
System.out.println("冲突发生次数:" + conflictTimes);
return resultGeneList;
}
解决冲突后, 继续进化, 直到50轮结束
接下来考虑教室问题
/**
* 开始给进化完的基因编码分配教室,即在原来的编码中加上教室编号,6
* @param individualMap
* @return
*/
private List<String> finalResult(Map<String, List<String>> individualMap) {
// 存放编上教室编号的完整基因编码
List<String> resultList = new ArrayList<>();
// 将map集合中的基因编码再次全部混合
List<String> resultGeneList = collectGene(individualMap);
String classroomNo = "";
// 得到课程任务的年级列表 01 02 03
List<String> gradeList = classTaskDao.selectByColumnName(ConstantInfo.GRADE_NO);
// 将基因编码按照年级分配
Map<String, List<String>> gradeMap = collectGeneByGrade(resultGeneList, gradeList);
// 这里需要根据安排教学区域时选的教学楼进行安排课程
for (String gradeNo : gradeMap.keySet()) {
// 如果一个年级设置多个教学楼,则需要使用List ===============>>>>>>>
List<String> teachBuildNoList = teachBuildInfoDao.selectTeachBuildList(gradeNo);
// 得到不同年级的课程基因编码
List<String> gradeGeneList = gradeMap.get(gradeNo);
// 年级设置多个教学楼栋的情况
QueryWrapper wrapper = new QueryWrapper();
wrapper.in("teachbuild_no", teachBuildNoList);
List<Classroom> classroomList2 = classroomDao.selectList(wrapper);
for (String gene : gradeGeneList) {
// 分配教室
classroomNo = issueClassroom(gene, classroomList2, resultList);
// 基因编码中加入教室编号,至此所有基因信息编码完成,得到染色体
gene = gene + classroomNo;
// 将最终的编码加入集合中
resultList.add(gene);
}
}
// 完整的基因编码,即分配有教室的
return resultList;
}
分配教室的策略是这样的, 首先判断课程类型, 每种课程类型有着不一样的可用教室
private String issueClassroom(String gene, List<Classroom> classroomList, List<String> resultList) {
// 处理特殊课程,实验课,体育课
// 体育课
List<Classroom> sportBuilding = classroomDao.selectByTeachbuildNo("12");
// 实验课
List<Classroom> experimentBuilding = classroomDao.selectByTeachbuildNo("08");
// 获得班级编号
String classNo = ClassUtil.cutGene(ConstantInfo.CLASS_NO, gene);
// 得到该班级的学生人数
int studentNum = classInfoDao.selectStuNum(classNo);
// 得到课程属性
String courseAttr = ClassUtil.cutGene(ConstantInfo.COURSE_ATTR, gene);
if (courseAttr.equals(ConstantInfo.EXPERIMENT_COURSE)) {
// 03为实验课
return chooseClassroom(studentNum, gene, experimentBuilding, resultList);
} else if (courseAttr.equals(ConstantInfo.PHYSICAL_COURSE)) {
// 04为体育课
return chooseClassroom(studentNum, gene, sportBuilding, resultList);
} else {
// 剩下主要课程、次要课程都放在普通的教室
// 如果还有其他课程另外加判断课程属性,暂时设定4种:主要,次要,实验,体育。音乐舞蹈那些不算
return chooseClassroom(studentNum, gene, classroomList, resultList);
}
}
接下来, 根据不同的课程类型, 按人数随机分配教室, 同时还要判断该教室是否在同一时间有别的班级使用了
private String chooseClassroom(int studentNum, String gene, List<Classroom> classroomList, List<String> resultList) {
// 使用随机分配教室的方式,只要可以放下所有学生即可满足条件
int min = 0;
int max = classroomList.size() - 1;
// 用于随机选取教室
int temp = min + (int)(Math.random() * (max + 1 - min));
Classroom classroom = classroomList.get(temp);
// 判断是否满足条件
if (judgeClassroom(studentNum, gene, classroom, resultList)) {
// 该教室满足条件
return classroom.getClassroomNo();
} else {
// 不满足,继续找教室
return chooseClassroom(studentNum, gene, classroomList, resultList);
}
}
// 判断教室是否符合上课班级所需
// 即:不同属性的课要放在对应属性的教室上课
private Boolean judgeClassroom(int studentNum, String gene, Classroom classroom, List<String> resultList) {
String courseAttr = ClassUtil.cutGene(ConstantInfo.COURSE_ATTR, gene);
// 只要是语数英物化生政史地这些课程都是放在普通教室上课
if (courseAttr.equals(ConstantInfo.MAIN_COURSE) || courseAttr.equals(ConstantInfo.SECONDARY_COURSE)) {
// 找到普通教室,普通教室的属性都是01
if (classroom.getAttr().equals("01")) {
// 判断上课人数与教室容量
if (classroom.getCapacity() >= studentNum) {
// 还要判断该教室是否在同一时间有别的班级使用了
return isFree(gene, resultList, classroom);
} else {
// 教室容量不够
return false;
}
} else {
return false;
}
} else {
// 剩余的课程应该要放在相对应的教室上课
if (ClassUtil.cutGene(ConstantInfo.COURSE_ATTR, gene).equals(classroom.getAttr())) {
// 判断人数
if (classroom.getCapacity() >= studentNum) {
// 判断该教室上课时间是否重复
return isFree(gene, resultList, classroom);
} else {
return false;
}
} else {
return false;
}
}
}
private Boolean isFree(String gene, List<String> resultList, Classroom classroom) {
// 如果resultList为空说明还没有教室被分配,直接返回true
if (resultList.size() == 0) {
return true;
} else {
for (String resultGene : resultList) {
// 如果当前教室在之前分配了则需要去判断时间是否有冲突
if (ClassUtil.cutGene(ConstantInfo.CLASSROOM_NO, resultGene).equals(classroom.getClassroomNo())) {
// 判断时间是否一样,一样则表示有冲突
if (ClassUtil.cutGene(ConstantInfo.CLASS_TIME, gene).equals(ClassUtil.cutGene(ConstantInfo.CLASS_TIME, resultGene))) {
return false;
}
}
}
}
return true;
}
最后就是写入数据库, 大工告成。
需要项目远程部署与项目讲解的请联系扣扣: 2402668109