问题描述
最近需要设计一个基于python的排课算法。已知条件:每门课程(共25门课)对应的知识范围(多个)和老师(20位老师)的知识范围(多个),每天上五次课,一星期五天。约束条件是:每个课程必须分配给一个插槽(上课时间)。 每个插槽都需要分配一个导师。将导师和模块分配到同一插槽意味着辅导员将教该模块。 只有在导师的专业知识主题包括该模块涵盖的每个主题时,该教员才能教该模块。 辅导老师最多只能教两个不同的模块。 导师不能在一天内教多个模块。最终需求:得到一个可行的排课列表。
关键问题点
如何保证每个模块分配的老师都涵盖该课程范围;如何保证每天老师上课次数和每周上课次数满足约束条件;由于每门课对应上课老师可能是一个也可能是多个,要是在前面的课程安排中出现了某个老师,而下一个课程只能选择该老师时,这个时候老师就可能违反教课程的次数。
算法选择思路
关于老师满足课程的涵盖范围这个点很好满足,主要的问题是如何解决老师上课次数的冲突问题。在这个问题中,以我们人工处理思想为例,在排课问题中如果遇到老师A冲突,首先想到的是该位置能不能换一个老师,如果不能够换,则向前找到老师A出现的位置,在此处将老师A换成其他老师。这样就可以保证后面老师A能够顺利安排了。这一处理思路完美的契合了回溯算法的思想,故选用回溯算法处理该问题。
回溯算法简单介绍
回溯算法的基本原理在此不做介绍,如果没有基础的同学(在代码的世界里,我们都是学生),建议去力扣上去查看全排列问题求解https://leetcode-cn.com/problems/permutations/和八皇后求解算法https://leetcode-cn.com/problems/n-queens/,在给出的答案中都有很详细的介绍。简单思路就是遍历每一个节点,如果当前选择满足约束条件,保存当前节点至路径,进入下一个节点。如不满足约束条件,选择另一个老师。当这一门课的每一个老师都不满足时,返回上一层节点,选择其他老师。这个思路课我们人工处理的思路很类似,但如果数据量较多时,只能依靠计算机来解决了。
1.回溯函数主要的三个内容:
①路径:也就是已经做出的选择。
路径在本题中对应的是已经选择的课程,比如星期一的课程已经选择,则星期一的课程就是路径。剩下的未排课的星期就是选择列表。
②选择列表:也就是你当前可以做的选择。
③结束条件:也就是到达决策树底层,无法再做选择的条件。
本题的结束条件是当每一天的课程被排满,即安排了25节课则结束
2.回溯算法的结构:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
3.编写选择函数
用来判断该老师是否与前面安排的课表发生冲突。思路是判断当前老师最多只能教两个不同的模块。导师不能在一天内教多个模块。
排课问题解决思路(重点来了!!!)
1.解决老师和课程匹配问题
在这个步骤中,按照给出的课程顺序排列课程对应可上课的老师列表,即解决了老师和课程匹配问题,也简化了模型。我们只需要对得到的老师列表进行判断即可。
#创建一个列表来储存每一个课程可以选择的老师,按课程顺序排序
module_tutor = []
for module in self.moduleList:
# 创建一个列表保存当前课程所能参与的老师
teacher = []
for tutor in self.tutorList:
#判断课程是否在老师的范围内
if set(module.topics).issubset(set(tutor.expertise)):
#加入老师名单
teacher.append(tutor)
#加入当前课程的老师名单
module_tutor.append(teacher)
2.回溯算法编译
在步骤1中得到了依据课程顺序排列的老师顺序,形如[ [ tutor1,tutor2],[tutor3,tutor4],……],课表顺序为[class1,class2,……],class1对应的可上课的老师为 [tutor1,tutor2]。这个时候我们就按照得到的可上课老师列表的顺序来排课,主要考虑的是老师教课次数的冲突。
①冲突函数的编写
#判断一个老师一天只上一节课和一周最多只上两节课,用于在排课是判断该老师是否与前面的课程冲突。输入一段排好的课程,输入一个待排课的老师
def conflict(tut_result,tut):
n = len(tut_result)%5
return False if tut_result.count(tut)<2 and tut not in tut_result[-n:] else True
②判断生成的课程表是否满足约束条件(我们只需要得到一个可用列表即输出,回溯函数则是遍历所有结果),当我们得到一个解时,如满足条件,便输出,并结束回溯函数)
#该函数用来判断排课方式是否满足约束条件,输入排好的课程,输出True of False.判断条件,同一天老师不能上两次,一周老师不能超过两次。
def if_Ok(A):
B = dict.fromkeys(A)
for i in B.keys():
if A.count(i) > 2:
return False
for i in range(0, len(A), 5):
if len(dict.fromkeys(A[i:i + 5])) != 5:
return False
return True
③回溯函数的编写
# 回溯函数,用于选择一种合适的课程排序,输入课程可供选择的老师,输出排好的课程
def backtrack(module_tutor, tut_result):
#回溯函数结束条件
if len(module_tutor) == len(tut_result):
return
for i in module_tutor[len(tut_result)]:
#判断是否冲突
if not conflict(tut_result, i):
tut_result.append(i)
A.append(i)
backtrack(module_tutor, tut_result)
#得到一个可行的课程排序,退出函数,输出结果
if len(A) == len(module_tutor) and if_Ok(A):
break
tut_result.pop()
A.pop()
return A
# 创建一个列表A保存排好的课程表,
A = []
A=backtrack(module_tutor, [])
3.将课程加入排序内容
由于排好的只是老师的顺序,我们再把课程加入即可。如:老师列表 [teacher1,teacher2,……],初始给出课程列表 [class1,class2,……],最终结果 [ [class1,teacher1],[class2,teacher2],……],其中[class1,teacher1]代表将该课安排在周一第一节课,以此类推。
总结
整个排课系统包括三个问题,这是第一个问题,在接触这些问题之前并不了解回溯算法和启发式算法(后面问题会用到),花了五天时间现学现用给解决掉,如果有不对的地方,请大神指出。在后面的排课问题中,有给出聘请每个老师需要花费的费用和一些费用制定规则,求如何安排课表使得整个排课所需费用最小。使用启发式算法中的遗传算法成功求出,后续如果有时间会写出来。转载请说明出处!