排课V2.0 GA约束体系完整解读:从硬约束到软约束的技术实现
排课V2.0 GA约束体系完整解读:从硬约束到软约束的技术实现
作者:某中学 | 技术实现:tiangolo | 2026年3月
排课V2.0 GA约束体系完整解读:从硬约束到软约束的技术实现
作者:tiangolo
日期:2026-04-03
脱敏:某中学
前言
排课是典型的约束满足问题(CSP)。当学校规模扩大(18班、63教师、161条课程需求),人工排课几乎不可能同时满足所有约束。遗传算法(GA)通过"惩罚机制"将硬约束转化为适应度函数的强制项,是工业级排课系统的标准方案。
本文深入解析排课V2.0的GA约束体系设计,包含:
一、约束分类体系
排课V2.0将约束分为三层:
层次约束类型惩罚系数说明 硬约束体育不排前3节×5000必须满足,否则课表不可用 硬约束同教师不撞课×5000必须满足,多班并发核心约束 硬约束科目互斥×500如体育与语文不能同天 软约束连堂课优化×5语数英尽量连排,提升教学效果 软约束时段偏好×2主科上午,体美下午 课时约束周课时偏差×1各科课时尽量接近目标为什么硬约束用这么大惩罚系数?
GA的适应度函数是"越大越好"。假设个体A违反硬约束但软约束满分,适应度 = 0 + 100 = 100;如果硬约束惩罚系数只有10,个体B满足硬约束但软约束差,适应度 = -10 + 80 = 70 → 算法会错误选择A。
惩罚系数 ≥ 10³ 确保:任何违反硬约束的个体,在进化中必然被淘汰。
二、适应度函数数学建模
基础公式
fitness = w₁×H_硬约束 + w₂×H_软约束 + w₃×H_课时 + H_时段奖励
各项展开:
硬约束惩罚:
H_体育 = Σ(每节体育课排在前3节) × 5000
H_教师 = Σ(同一教师同时上两个班) × 5000
H_互斥 = Σ(同班同天出现互斥科目对) × 500
课时惩罚:
H_课时 = Σ|某科目实际排课节数 - 目标周课时|
软约束奖励(越大越好):
R_连堂 = 连堂课对数 × 8 # 每增加一对连堂,+8分
R_时段 = Σ时段权重(period) # 上午权重高,奖励大
完整适应度函数
def fitness(self, chrom):
H_pe = self.count_pe_conflicts() # 体育冲突数 × 5000
H_tc = self.count_teacher_conflicts() # 教师冲突数 × 5000
H_me = self.count_mutual_exclusions() # 互斥冲突数 × 500
P_pe = H_pe * 5000
P_tc = H_tc * 5000
P_me = H_me * 500
# 课时偏差(越小越好 → 转为负惩罚)
hour_dev = sum(sum(d.values()) for d in self._hours_by_class(chrom).values())
P_hour = -hour_dev # 偏差越大,适应度越低
# 连堂课奖励(软约束,越多越好)
n_consec = self.count_consecutive_pairs(chrom)
R_consec = n_consec * 8
# 时段偏好奖励
R_period = self._period_score(chrom)
return -(P_pe + P_tc + P_me) + P_hour + R_consec + R_period
适应度曲线解读
在3班并发压力测试中(200代):
Gen 1: fitness = -8444 (大量冲突)
Gen 31: fitness = 3.45(硬约束归零)
Gen 61: fitness = 11.50(开始优化软约束)
Gen120: fitness = 25.40(软约束持续改善)
Gen200: fitness = 57.35(稳定收敛)
关键转折点在Gen31:硬约束归零后,适应度由负转正,说明惩罚机制有效。
三、硬约束实现细节
3.1 体育不排前3节(单班独立轨道)
每班有独立的8节×5天时间槽轨道。染色体中,体育课(tiyu)出现在第0、1、2位(对应第1、2、3节)时,计数+1:
TIYU_NO_PERIODS = {0, 1, 2} # 第1-3节
def count_pe_conflicts(self, chrom):
count = 0
slots = self.slots_per_class # 40节/班
for ci in range(self.n_classes):
offset = ci * slots
for day in range(self.days):
for period in range(self.ppd):
idx = offset + day * self.ppd + period
if chrom[idx] == "tiyu" and period in self.TIYU_NO_PERIODS:
count += 1
return count
3.2 同教师不撞课(多班并发检测)
每班有独立轨道,但同一教师可能教多个班。在同一时间槽,检查该教师是否被两个班同时占用:
def count_teacher_conflicts(self, chrom):
conflicts = 0
for day in range(self.days):
for period in range(self.ppd):
teachers_at_slot = {}
for ci, class_id in enumerate(self.class_ids):
idx = ci * self.slots_per_class + day * self.ppd + period
subj_id = chrom[idx]
teacher_id = self.teacher_subjects.get(subj_id)
if teacher_id:
if teacher_id in teachers_at_slot:
conflicts += 1 # 同一教师同一时间上两个班
teachers_at_slot[teacher_id] = class_id
return conflicts
3.3 科目互斥(同一班级同一天)
某些科目不能排在同一天(如体育课当天不宜安排其他主科考试):
def count_mutual_exclusions(self, chrom):
# mutual_exclusions: List[Tuple[str, str]]
# 例如 [("tiyu", "yuwen")] = 体育与语文不能同天
violations = 0
for ci in range(self.n_classes):
offset = ci * self.slots_per_class
for day in range(self.days):
subjects_today = set()
for period in range(self.ppd):
subj = chrom[offset + day * self.ppd + period]
subjects_today.add(subj)
# 检查互斥对
for (a, b) in self.mutual_exclusions:
if a in subjects_today and b in subjects_today:
violations += 1
return violations
四、软约束实现细节
4.1 连堂课优化(变异操作改造)
连堂课(同一科目连续两节)有助于教学连贯性。在GA变异阶段,主动将孤立节次与相邻节交换:
def _mutate(self, chrom, prob):
for ci in range(self.n_classes):
offset = ci * self.slots_per_class
for day in range(self.days):
for period in range(self.ppd):
if random.random() > prob:
continue
idx = offset + day * self.ppd + period
subj = chrom[idx]
# 判断是否为孤立节(前后节不同科目)
is_single = True
if period > 0 and chrom[idx - 1] == subj:
is_single = False
if period < self.ppd - 1 and chrom[idx + 1] == subj:
is_single = False
if not is_single:
continue
# 尝试与相邻节交换
candidates = []
if period > 0:
candidates.append(idx - 1)
if period < self.ppd - 1:
candidates.append(idx + 1)
if candidates and random.random() < 0.3:
swap_idx = random.choice(candidates)
if chrom[swap_idx] in self.consecutive_subjects:
chrom[idx], chrom[swap_idx] = chrom[swap_idx], chrom[idx]
return chrom
4.2 时段偏好权重
主科(语数英)排在上午(1-4节)效果更好,体美排在下午。GA在适应度函数中奖励时段分配合理的个体:
PERIOD_WEIGHTS = {
0: 1.0, 1: 1.0, # 第1-2节,权重最高
2: 0.9, 3: 0.9, # 第3-4节,权重较高
4: 0.6, 5: 0.6, # 第5-6节
6: 0.5, 7: 0.5 # 第7-8节,下午最低
}
def _period_score(self, chrom):
"""计算时段偏好得分:主科在上午得正分,体美在下午得正分"""
score = 0.0
main_subjects = {"yuwen", "shuxue", "yingyu"} # 语数英
afternoon_subjects = {"tiyu", "meishu", "yinyue", "laoji"} # 体美
for ci in range(self.n_classes):
offset = ci * self.slots_per_class
for day in range(self.days):
for period in range(self.ppd):
subj = chrom[offset + day * self.ppd + period]
w = PERIOD_WEIGHTS.get(period, 0.5)
if subj in main_subjects:
score += w # 主科上午:加分
elif subj in afternoon_subjects:
score -= w * 0.5 # 体美下午:轻微减分
return score
五、实测数据与效果
5.1 多班并发压力测试(200代)
指标Gen 1Gen 31Gen 200变化 适应度-8444+3.45+57.35↗ 大幅提升 体育冲突200 ✅立即收敛 教师冲突200 ✅立即收敛 互斥冲突500 ✅立即收敛 连堂对数5611↗ 持续优化5.2 约束满足率
六、工程实践建议
6.1 惩罚系数调参经验
惩罚系数是GA排课的核心超参数。经验法则:
约束类型系数范围建议值 绝对硬约束(不可违反)10³ ~ 10⁶5000 强约束(几乎不可违反)10² ~ 10³500 软约束(可适当违反)1 ~ 105~8 奖励项(越多越好)正分2~106.2 内存优化
当学校规模较大(>20班)时,染色体数组可能占用大量内存:
# 使用 numpy 而非 Python list,内存节省约10倍
self.population = np.array([...]) # uint8 足矣
# 每50代强制GC,防止内存碎片化
if gen % 50 == 0:
gc.collect()
6.3 早停策略
当硬约束归零后,可提前停止以节省计算资源:
if H_pe == 0 and H_tc == 0 and H_me == 0:
print("硬约束已全部满足,可停止进化")
break
结语
排课V2.0的GA约束体系体现了"硬约束用惩罚过滤,软约束用奖励优化"的经典思想。实测数据表明,3班并发场景下,200代内即可同时满足所有硬约束并显著优化软约束。
下一步方向:
完整源码:https://github.com/xxx/paike-v2
本文为某中学教务教研系统技术博客系列第五篇。相关项目:排课系统 / 成绩分析报表 / 教务自动化
某中学 · 教务教研数字化实践 · 2026
技术栈:Python · NumPy · FastAPI · python-docx

浙公网安备 33010602011771号