main
happyw1nd 2025-01-17 01:40:38 +08:00
parent 89132986a5
commit 4f7c605da8
5 changed files with 442 additions and 106 deletions

372
main.py
View File

@ -4,6 +4,7 @@ from PySide6.QtGui import QFont
from utils import read_excel, save_to_excel
from solve import solve_program
from datetime import datetime
import traceback
class MyWidget(QtWidgets.QWidget):
def __init__(self):
@ -42,6 +43,18 @@ class MyWidget(QtWidgets.QWidget):
self.cond_layout_overall = QtWidgets.QVBoxLayout()
self.group_box_2.setLayout(self.cond_layout_overall)
# 自动模式开关
self._auto_mode = True
self.cond_layout_0 = QtWidgets.QVBoxLayout()
self.auto_switch = QtWidgets.QCheckBox("自动调参模式")
self.auto_switch.setTristate(False) # 禁用三态模式
self.auto_switch.setChecked(True)
self.auto_switch.stateChanged.connect(self.on_switch_toggled)
self.auto_switch.setFont(thin_font)
self.cond_layout_0.addWidget(self.auto_switch)
self.cond_layout_overall.addLayout(self.cond_layout_0)
# 限制条件1
self.cond_layout_1 = QtWidgets.QHBoxLayout()
# 文字1
@ -53,6 +66,7 @@ class MyWidget(QtWidgets.QWidget):
self.line_edit_1_1.setValidator(QtGui.QIntValidator()) # 设置只接受整数
self.line_edit_1_1.setPlaceholderText("无限制")
self.line_edit_1_1.setText("4")
self.line_edit_1_1.setEnabled(False)
# 文字2
self.label_cond_1_2 = QtWidgets.QLabel("", self)
self.label_cond_1_2.setFont(thin_font)
@ -62,6 +76,7 @@ class MyWidget(QtWidgets.QWidget):
self.line_edit_1_2.setValidator(QtGui.QIntValidator()) # 设置只接受整数
self.line_edit_1_2.setPlaceholderText("无限制")
self.line_edit_1_2.setText("8")
self.line_edit_1_2.setEnabled(False)
self.cond_layout_1.addWidget(self.label_cond_1_1)
self.cond_layout_1.addWidget(self.line_edit_1_1)
@ -79,6 +94,7 @@ class MyWidget(QtWidgets.QWidget):
self.line_edit_2_1.setFont(thin_font)
self.line_edit_2_1.setValidator(QtGui.QIntValidator()) # 设置只接受整数
self.line_edit_2_1.setPlaceholderText("无限制")
self.line_edit_2_1.setEnabled(False)
# 文字2
self.label_cond_2_2 = QtWidgets.QLabel("", self)
self.label_cond_2_2.setFont(thin_font)
@ -87,7 +103,8 @@ class MyWidget(QtWidgets.QWidget):
self.line_edit_2_2.setFont(thin_font)
self.line_edit_2_2.setValidator(QtGui.QIntValidator()) # 设置只接受整数
self.line_edit_2_2.setPlaceholderText("无限制")
self.line_edit_2_2.setText("1")
self.line_edit_2_2.setText("2")
self.line_edit_2_2.setEnabled(False)
self.cond_layout_2.addWidget(self.label_cond_2_1)
self.cond_layout_2.addWidget(self.line_edit_2_1)
@ -106,6 +123,7 @@ class MyWidget(QtWidgets.QWidget):
self.line_edit_3_1.setValidator(QtGui.QIntValidator())
self.line_edit_3_1.setPlaceholderText("无限制")
self.line_edit_3_1.setText("1")
self.line_edit_3_1.setEnabled(False)
# 文字2
self.label_cond_3_2 = QtWidgets.QLabel("", self)
self.label_cond_3_2.setFont(thin_font)
@ -114,6 +132,7 @@ class MyWidget(QtWidgets.QWidget):
self.line_edit_3_2.setFont(thin_font)
self.line_edit_3_2.setValidator(QtGui.QIntValidator())
self.line_edit_3_2.setPlaceholderText("无限制")
self.line_edit_3_2.setEnabled(False)
self.cond_layout_3.addWidget(self.label_cond_3_1)
self.cond_layout_3.addWidget(self.line_edit_3_1)
@ -124,14 +143,15 @@ class MyWidget(QtWidgets.QWidget):
# 限制条件4
self.cond_layout_4 = QtWidgets.QHBoxLayout()
# 文字1
self.label_cond_4_1 = QtWidgets.QLabel("每班次小朋友数:", self)
self.label_cond_4_1 = QtWidgets.QLabel("每班次人资小朋友数:", self)
self.label_cond_4_1.setFont(thin_font)
# 数字输入框1
self.line_edit_4_1 = QtWidgets.QLineEdit(self)
self.line_edit_4_1.setFont(thin_font)
self.line_edit_4_1.setValidator(QtGui.QIntValidator())
self.line_edit_4_1.setPlaceholderText("无限制")
self.line_edit_4_1.setText("2")
self.line_edit_4_1.setText("1")
self.line_edit_4_1.setEnabled(False)
# 文字2
self.label_cond_4_2 = QtWidgets.QLabel("", self)
self.label_cond_4_2.setFont(thin_font)
@ -140,6 +160,7 @@ class MyWidget(QtWidgets.QWidget):
self.line_edit_4_2.setFont(thin_font)
self.line_edit_4_2.setValidator(QtGui.QIntValidator())
self.line_edit_4_2.setPlaceholderText("无限制")
self.line_edit_4_2.setEnabled(False)
self.cond_layout_4.addWidget(self.label_cond_4_1)
self.cond_layout_4.addWidget(self.line_edit_4_1)
@ -147,6 +168,34 @@ class MyWidget(QtWidgets.QWidget):
self.cond_layout_4.addWidget(self.line_edit_4_2)
self.cond_layout_overall.addLayout(self.cond_layout_4)
# 限制条件5
self.cond_layout_5 = QtWidgets.QHBoxLayout()
# 文字1
self.label_cond_5_1 = QtWidgets.QLabel("每班次小朋友数:", self)
self.label_cond_5_1.setFont(thin_font)
# 数字输入框1
self.line_edit_5_1 = QtWidgets.QLineEdit(self)
self.line_edit_5_1.setFont(thin_font)
self.line_edit_5_1.setValidator(QtGui.QIntValidator())
self.line_edit_5_1.setPlaceholderText("无限制")
self.line_edit_5_1.setText("2")
self.line_edit_5_1.setEnabled(False)
# 文字2
self.label_cond_5_2 = QtWidgets.QLabel("", self)
self.label_cond_5_2.setFont(thin_font)
# 数字输入框2
self.line_edit_5_2 = QtWidgets.QLineEdit(self)
self.line_edit_5_2.setFont(thin_font)
self.line_edit_5_2.setValidator(QtGui.QIntValidator())
self.line_edit_5_2.setPlaceholderText("无限制")
self.line_edit_5_2.setEnabled(False)
self.cond_layout_5.addWidget(self.label_cond_5_1)
self.cond_layout_5.addWidget(self.line_edit_5_1)
self.cond_layout_5.addWidget(self.label_cond_5_2)
self.cond_layout_5.addWidget(self.line_edit_5_2)
self.cond_layout_overall.addLayout(self.cond_layout_5)
self.main_layout.addWidget(self.group_box_2)
@ -183,63 +232,288 @@ class MyWidget(QtWidgets.QWidget):
self.label_openfile.setText(f"选中文件: {file_path}")
self._excel_dir = file_path
def on_switch_toggled(self, state):
"""处理开关状态的变化事件"""
if state == 0: # 关闭
self._auto_mode = False
self.line_edit_1_1.setEnabled(True)
self.line_edit_1_2.setEnabled(True)
self.line_edit_2_1.setEnabled(True)
self.line_edit_2_2.setEnabled(True)
self.line_edit_3_1.setEnabled(True)
self.line_edit_3_2.setEnabled(True)
self.line_edit_4_1.setEnabled(True)
self.line_edit_4_2.setEnabled(True)
self.line_edit_5_1.setEnabled(True)
self.line_edit_5_2.setEnabled(True)
elif state == 2: # 打开
self._auto_mode = True
self.line_edit_1_1.setEnabled(False)
self.line_edit_1_2.setEnabled(False)
self.line_edit_2_1.setEnabled(False)
self.line_edit_2_2.setEnabled(False)
self.line_edit_3_1.setEnabled(False)
self.line_edit_3_2.setEnabled(False)
self.line_edit_4_1.setEnabled(False)
self.line_edit_4_2.setEnabled(False)
self.line_edit_5_1.setEnabled(False)
self.line_edit_5_2.setEnabled(False)
@QtCore.Slot()
def magic(self):
self.text_solve.append("*******************")
try:
self.text_solve.append(f"******** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ********")
# 读取文件
if self._excel_dir is None:
self.text_solve.append("请先选择文件!")
return
# 读取文件
if self._excel_dir is None:
self.text_solve.append("请先选择文件!")
return
self.text_solve.append("开始排班...")
self.text_solve.append("读取文件中...")
all_data, index_to_name_dict, preference_mat, want_num_array, is_new_array, is_tech_array = read_excel(self._excel_dir)
self.text_solve.append("读取文件成功!")
self.text_solve.append("开始排班...")
self.text_solve.append("读取文件中...")
all_data, index_to_name_dict, preference_mat, want_num_array, is_new_array, is_tech_array, is_hr_array = read_excel(self._excel_dir)
self.text_solve.append("读取文件成功!")
self.text_solve.append("信息统计:")
stu_num = len(index_to_name_dict)
self.text_solve.append(f"\t学生总人数:{stu_num}")
# 计算并打印一些统计信息
self.text_solve.append("信息统计:")
stu_num = len(index_to_name_dict)
self.text_solve.append(f"\t学生总人数:{stu_num}")
class_num = len(preference_mat[0])
self.text_solve.append(f"\t班次总数:{class_num}")
class_num = len(preference_mat[0])
self.text_solve.append(f"\t班次总数:{class_num}")
tech_sum = 0
for i in range(len(preference_mat)):
if not is_new_array[i] and is_tech_array[i]:
tech_sum += want_num_array[i]
self.text_solve.append(f"\t电脑部或电气部的所有老人的意愿班次之和:{tech_sum}")
tech_sum = 0
for i in range(len(preference_mat)):
if not is_new_array[i] and is_tech_array[i]:
tech_sum += want_num_array[i]
self.text_solve.append(f"\t电脑部或电气部的所有老人的意愿班次之和:{tech_sum}")
all_sum = sum(want_num_array)
self.text_solve.append(f"\t所有人的意愿班次之和:{all_sum}")
all_sum = sum(want_num_array)
self.text_solve.append(f"\t所有人的意愿班次之和:{all_sum}")
self.text_solve.append(f"\t平均每班人数:{all_sum/20}")
# 读取限制条件
num_min = int(self.line_edit_1_1.text()) if self.line_edit_1_1.text() else None
num_max = int(self.line_edit_1_2.text()) if self.line_edit_1_2.text() else None
num_tech_min = int(self.line_edit_2_1.text()) if self.line_edit_2_1.text() else None
num_tech_max = int(self.line_edit_2_2.text()) if self.line_edit_2_2.text() else None
num_old_min = int(self.line_edit_3_1.text()) if self.line_edit_3_1.text() else None
num_old_max = int(self.line_edit_3_2.text()) if self.line_edit_3_2.text() else None
num_new_min = int(self.line_edit_4_1.text()) if self.line_edit_4_1.text() else None
num_new_max = int(self.line_edit_4_2.text()) if self.line_edit_4_2.text() else None
min_prefer = 100
for j in range(len(preference_mat[0])):
prefer_num = sum([preference_mat[i][j] for i in range(len(preference_mat))])
min_prefer = min(min_prefer, prefer_num)
self.text_solve.append(f"\t所有班次中最少拥有意愿数:{min_prefer}")
self.text_solve.append("计算最优解中...")
vars = solve_program(preference_mat=preference_mat, want_num_array=want_num_array, is_new_array=is_new_array, is_tech_array=is_tech_array,
num_min=num_min, num_max=num_max, num_tech_min=num_tech_min, num_tech_max=num_tech_max,
num_old_min=num_old_min, num_old_max=num_old_max, num_new_min=num_new_min, num_new_max=num_new_max)
if vars is None:
self.text_solve.append("在目前限制条件下无解!请尝试更改限制条件!")
return
else:
self.text_solve.append("计算最优解成功!")
if self._auto_mode:
self.text_solve.append(f"自动调参开始...")
# 保存结果到 excel
self.text_solve.append("保存结果中...")
time_str = datetime.now().strftime('%Y%m%d_%H%M%S')
save_dir = f"result_{time_str}.xlsx"
if vars is not None:
save_to_excel(vars, all_data, index_to_name_dict, preference_mat, save_dir)
self.text_solve.append(f"保存结果成功!保存路径:{save_dir}")
# 至少?个电脑或电器的老人
tech_old_min_try_list = [1,0]
tech_old_min_num = None
for _tech_min in tech_old_min_try_list:
vars = solve_program(preference_mat=preference_mat,
want_num_array=want_num_array,
is_new_array=is_new_array,
is_tech_array=is_tech_array,
is_hr_array=is_hr_array,
num_min=None, num_max=None,
num_tech_min=_tech_min, num_tech_max=None,
num_old_min=None, num_old_max=None,
num_hr_min=None, num_hr_max=None,
num_new_min=None, num_new_max=None)
if vars:
tech_old_min_num = _tech_min
break
# 至多?个电脑或电器的老人
tech_old_max_try_list = [1,2,3,4,5,6,7,8]
tech_old_max_num = None
for _tech_max in tech_old_max_try_list:
vars = solve_program(preference_mat=preference_mat,
want_num_array=want_num_array,
is_new_array=is_new_array,
is_tech_array=is_tech_array,
is_hr_array=is_hr_array,
num_min=None, num_max=None,
num_tech_min=tech_old_min_num, num_tech_max=_tech_max,
num_old_min=None, num_old_max=None,
num_hr_min=None, num_hr_max=None,
num_new_min=None, num_new_max=None)
if vars:
tech_old_max_num = _tech_max
break
# 每班至少?个老人
num_old_min_try_list = [2,1,0]
num_old_min = None
for _num_old_min in num_old_min_try_list:
vars = solve_program(preference_mat=preference_mat,
want_num_array=want_num_array,
is_new_array=is_new_array,
is_tech_array=is_tech_array,
is_hr_array=is_hr_array,
num_min=None, num_max=None,
num_tech_min=tech_old_min_num, num_tech_max=tech_old_max_num,
num_old_min=_num_old_min, num_old_max=None,
num_hr_min=None, num_hr_max=None,
num_new_min=None, num_new_max=None)
if vars:
num_old_min = _num_old_min
break
# 每班至多?个老人
num_old_max_try_list = [1,2,3,4,5,6,7,8]
num_old_max = None
for _num_old_max in num_old_max_try_list:
vars = solve_program(preference_mat=preference_mat,
want_num_array=want_num_array,
is_new_array=is_new_array,
is_tech_array=is_tech_array,
is_hr_array=is_hr_array,
num_min=None, num_max=None,
num_tech_min=tech_old_min_num, num_tech_max=tech_old_max_num,
num_old_min=num_old_min, num_old_max=_num_old_max,
num_hr_min=None, num_hr_max=None,
num_new_min=None, num_new_max=None)
if vars:
num_old_max = _num_old_max
break
# 每班至少?人
num_min_try_list = [4,3,2,1]
num_min = None
for _num_min in num_min_try_list:
vars = solve_program(preference_mat=preference_mat,
want_num_array=want_num_array,
is_new_array=is_new_array,
is_tech_array=is_tech_array,
is_hr_array=is_hr_array,
num_min=_num_min, num_max=None,
num_tech_min=tech_old_min_num, num_tech_max=tech_old_max_num,
num_old_min=num_old_min, num_old_max=num_old_max,
num_hr_min=None, num_hr_max=None,
num_new_min=None, num_new_max=None)
if vars:
num_min = _num_min
break
# 每班至多?人
num_max_try_list = [8,9,10,11,12,13,14,15]
num_max = None
for _num_max in num_max_try_list:
vars = solve_program(preference_mat=preference_mat,
want_num_array=want_num_array,
is_new_array=is_new_array,
is_tech_array=is_tech_array,
is_hr_array=is_hr_array,
num_min=num_min, num_max=_num_max,
num_tech_min=tech_old_min_num, num_tech_max=tech_old_max_num,
num_old_min=num_old_min, num_old_max=num_old_max,
num_hr_min=None, num_hr_max=None,
num_new_min=None, num_new_max=None)
if vars:
num_max = _num_max
break
# 每班至少?个小朋友
num_new_min_try_list = [1,0]
num_new_min = None
for _num_new_min in num_new_min_try_list:
vars = solve_program(preference_mat=preference_mat,
want_num_array=want_num_array,
is_new_array=is_new_array,
is_tech_array=is_tech_array,
is_hr_array=is_hr_array,
num_min=num_min, num_max=num_max,
num_tech_min=tech_old_min_num, num_tech_max=tech_old_max_num,
num_old_min=num_old_min, num_old_max=num_old_max,
num_hr_min=None, num_hr_max=None,
num_new_min=_num_new_min, num_new_max=None)
if vars:
num_new_min = _num_new_min
break
# 每班至少?个人资小朋友
num_hr_min_try_list = [1,0]
num_hr_min = None
for _num_hr_min in num_hr_min_try_list:
vars = solve_program(preference_mat=preference_mat,
want_num_array=want_num_array,
is_new_array=is_new_array,
is_tech_array=is_tech_array,
is_hr_array=is_hr_array,
num_min=num_min, num_max=num_max,
num_tech_min=tech_old_min_num, num_tech_max=tech_old_max_num,
num_old_min=num_old_min, num_old_max=num_old_max,
num_hr_min=_num_hr_min, num_hr_max=None,
num_new_min=num_new_min, num_new_max=None)
if vars:
num_hr_min = _num_hr_min
break
# 每班至多?个人资小朋友
num_hr_max_try_list = [1,2,3,4,5,6,7,8]
num_hr_max = None
for _num_hr_max in num_hr_max_try_list:
vars = solve_program(preference_mat=preference_mat,
want_num_array=want_num_array,
is_new_array=is_new_array,
is_tech_array=is_tech_array,
is_hr_array=is_hr_array,
num_min=num_min, num_max=num_max,
num_tech_min=tech_old_min_num, num_tech_max=tech_old_max_num,
num_old_min=num_old_min, num_old_max=num_old_max,
num_hr_min=num_hr_min, num_hr_max=_num_hr_max,
num_new_min=num_new_min, num_new_max=None)
if vars:
num_hr_max = _num_hr_max
break
if vars is None:
self.text_solve.append("自动求解失败!请尝试手动调参!")
return
else:
self.text_solve.append("自动计算成功!")
self.text_solve.append("参数:")
self.text_solve.append(f"\t每班人数:[{num_min}, {num_max}]")
self.text_solve.append(f"\t每班电脑或电器的老人数:[{tech_old_min_num}, {tech_old_max_num}]")
self.text_solve.append(f"\t每班老人数:[{_num_old_min}, {num_old_max}]")
self.text_solve.append(f"\t每班人资部小朋友数:[{num_hr_min}, {num_hr_max}]")
self.text_solve.append(f"\t每班小朋友数:[{num_new_min}, inf]")
else:
# 读取限制条件
num_min = int(self.line_edit_1_1.text()) if self.line_edit_1_1.text() else None
num_max = int(self.line_edit_1_2.text()) if self.line_edit_1_2.text() else None
num_tech_min = int(self.line_edit_2_1.text()) if self.line_edit_2_1.text() else None
num_tech_max = int(self.line_edit_2_2.text()) if self.line_edit_2_2.text() else None
num_old_min = int(self.line_edit_3_1.text()) if self.line_edit_3_1.text() else None
num_old_max = int(self.line_edit_3_2.text()) if self.line_edit_3_2.text() else None
num_hr_min = int(self.line_edit_4_1.text()) if self.line_edit_4_1.text() else None
num_hr_max = int(self.line_edit_4_2.text()) if self.line_edit_4_2.text() else None
num_new_min = int(self.line_edit_5_1.text()) if self.line_edit_5_1.text() else None
num_new_max = int(self.line_edit_5_2.text()) if self.line_edit_5_2.text() else None
self.text_solve.append("计算最优解中...")
vars = solve_program(preference_mat=preference_mat, want_num_array=want_num_array, is_new_array=is_new_array, is_tech_array=is_tech_array, is_hr_array=is_hr_array,
num_min=num_min, num_max=num_max, num_tech_min=num_tech_min, num_tech_max=num_tech_max,
num_old_min=num_old_min, num_old_max=num_old_max, num_hr_min=num_hr_min, num_hr_max=num_hr_max, num_new_min=num_new_min, num_new_max=num_new_max)
if vars is None:
self.text_solve.append("在目前限制条件下无解!请尝试更改限制条件!")
return
else:
self.text_solve.append("计算最优解成功!")
# 保存结果到 excel
self.text_solve.append("保存结果中...")
time_str = datetime.now().strftime('%Y%m%d_%H%M%S')
save_dir = f"result_{time_str}.xlsx"
if vars is not None:
save_to_excel(vars, all_data, index_to_name_dict, preference_mat, save_dir)
self.text_solve.append(f"保存结果成功!保存路径:{save_dir}")
except Exception as e:
self.text_solve.append("程序出现严重错误,请联系开发者解决问题!!!")
self.text_solve.append(f"Error Details:\n{traceback.format_exc()}")
if __name__ == "__main__":
app = QtWidgets.QApplication([])

BIN
pics/screen.jpg 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

105
solve.py
View File

@ -1,16 +1,20 @@
from ortools.sat.python import cp_model
from ortools.linear_solver import pywraplp
from utils import read_excel, save_to_excel
def solve_program(preference_mat:list,
want_num_array:list,
is_new_array:list,
is_tech_array:list,
is_hr_array:list,
num_min=None,
num_max=None,
num_tech_min=None,
num_tech_max=None,
num_old_min=None,
num_old_max=None,
num_hr_min=None,
num_hr_max=None,
num_new_min=None,
num_new_max=None
):
@ -19,95 +23,116 @@ def solve_program(preference_mat:list,
is_not_tech_array = [not is_tech for is_tech in is_tech_array]
# 这是一个整数规划问题,我们使用 CP-SAT 求解器来解决这个问题
model = cp_model.CpModel()
solver = pywraplp.Solver.CreateSolver("SCIP")
if not solver:
return
# 定义变量的规模,一共有 N*M 个变量需要求解器求解
N = len(preference_mat) # 学生人数
M = len(preference_mat[0]) # 班次数
avg_num = sum(want_num_array) / M # 每班次的平均人数
print(f"平均人数:{avg_num}")
variables = []
aux_vars = [] # 辅助变量
infinity = solver.infinity()
for i in range(N):
row_vars = []
for j in range(M):
var = model.NewBoolVar(f"choice_{i}_{j}")
var = solver.IntVar(0.0, 1.0, f"choice_{i}_{j}")
row_vars.append(var)
variables.append(row_vars)
for j in range(M):
aux_var = solver.NumVar(0.0, infinity, f"aux_{j}")
aux_vars.append(aux_var)
print("Number of variables =", solver.NumVariables())
# 添加约束:每个同学意愿一定要满足
for i in range(N):
for j in range(M):
solver.Add(variables[i][j] <= preference_mat[i][j])
# 辅助变量添加约束:
for j in range(M):
actual_num = sum(variables[i][j] for i in range(N)) # 每班次的实际人数
solver.Add(aux_vars[j] >= actual_num-avg_num)
solver.Add(aux_vars[j] >= avg_num-actual_num)
# 添加约束:满足每位同学的意愿班次
for i in range(N):
model.Add(sum(variables[i]) == want_num_array[i])
solver.Add(sum(variables[i]) == want_num_array[i])
# 添加约束每个班次至少有n位同学
# 添加约束:每个班次至少有位同学
if num_min is not None:
for j in range(M):
model.Add(sum(variables[i][j] for i in range(N)) >= num_min)
solver.Add(sum(variables[i][j] for i in range(N)) >= num_min)
# 添加约束每个班次至多有n位同学
# 添加约束:每个班次至多有位同学
if num_max is not None:
for j in range(M):
model.Add(sum(variables[i][j] for i in range(N)) <= num_max)
solver.Add(sum(variables[i][j] for i in range(N)) <= num_max)
# 添加约束:每班次最少包含n个电脑或电器的老人
# 添加约束:每班次最少包含个电脑或电器的老人
if num_tech_min is not None:
for j in range(M):
model.Add(sum(variables[i][j]*is_old_array[i]*is_tech_array[i] for i in range(N)) >= num_tech_min)
solver.Add(sum(variables[i][j]*is_old_array[i]*is_tech_array[i] for i in range(N)) >= num_tech_min)
# 添加约束:每班次最多包含n个电脑或电器的老人
# 添加约束:每班次最多包含个电脑或电器的老人
if num_tech_max is not None:
for j in range(M):
model.Add(sum(variables[i][j]*is_old_array[i]*is_tech_array[i] for i in range(N)) <= num_tech_max)
solver.Add(sum(variables[i][j]*is_old_array[i]*is_tech_array[i] for i in range(N)) <= num_tech_max)
# 添加约束:每班次至少包含n个老人
# 添加约束:每班次至少包含个老人
if num_old_min is not None:
for j in range(M):
model.Add(sum(variables[i][j]*is_old_array[i] for i in range(N)) >= num_old_min)
solver.Add(sum(variables[i][j]*is_old_array[i] for i in range(N)) >= num_old_min)
# 添加约束:每班次至多包含n个老人
# 添加约束:每班次至多包含个老人
if num_old_max is not None:
for j in range(M):
model.Add(sum(variables[i][j]*is_old_array[i] for i in range(N)) <= num_old_max)
solver.Add(sum(variables[i][j]*is_old_array[i] for i in range(N)) <= num_old_max)
# 添加约束每班次至少包含n个小朋友
# 添加约束:每班次至少包含?个人资部小朋友
if num_hr_min is not None:
for j in range(M):
solver.Add(sum(variables[i][j]*is_hr_array[i] for i in range(N)) >= num_hr_min)
# 添加约束:每班次至多包含?个人资部小朋友
if num_hr_max is not None:
for j in range(M):
solver.Add(sum(variables[i][j]*is_hr_array[i] for i in range(N)) <= num_hr_max)
# 添加约束:每班次至少包含?个小朋友
if num_new_min is not None:
for j in range(M):
model.Add(sum(variables[i][j]*is_new_array[i] for i in range(N)) >= num_new_min)
solver.Add(sum(variables[i][j]*is_new_array[i] for i in range(N)) >= num_new_min)
# 添加约束每班次至多包含n个小朋友
# 添加约束:每班次至多包含个小朋友
if num_new_max is not None:
for j in range(M):
model.Add(sum(variables[i][j]*is_new_array[i] for i in range(N)) <= num_new_max)
solver.Add(sum(variables[i][j]*is_new_array[i] for i in range(N)) <= num_new_max)
# 优化目标:最大化同学的满意度
target_variables = [variables[i][j]*preference_mat[i][j] for i in range(N) for j in range(M)]
model.Maximize(sum(target_variables)) # Maximize the sum of these variables.
# 优化目标:每班次人数尽可能平均
solver.Minimize(sum(aux_vars)) # Maximize the sum of these variables.
# 求解优化问题
solver = cp_model.CpSolver()
status = solver.Solve(model)
status = solver.Solve()
# 输出结果
variables_return = []
if status == cp_model.OPTIMAL:
aux_vars_return = []
if status == pywraplp.Solver.OPTIMAL:
print("Optimal solution found:")
for i in range(N):
row_solution = [solver.Value(variables[i][j]) for j in range(M)]
row_solution = [variables[i][j].solution_value() for j in range(M)]
variables_return.append(row_solution)
# print(" ".join(map(str, row_solution)))
for j in range(M):
aux_vars_return.append(aux_vars[j].solution_value())
# Print the optimized value of the objective function.
print(f"Optimized objective value: {solver.ObjectiveValue()}")
return variables_return
elif status == cp_model.FEASIBLE:
print("A potentially suboptimal solution was found.")
for i in range(N):
row_solution = [solver.Value(variables[i][j]) for j in range(M)]
variables_return.append(row_solution)
# print(" ".join(map(str, row_solution)))
# Print the optimized value of the objective function.
print(f"Suboptimal objective value: {solver.ObjectiveValue()}")
print(f"Optimized objective value: {solver.Objective().Value()}")
print(aux_vars_return)
return variables_return

View File

@ -1,5 +1,6 @@
import pandas as pd
from datetime import datetime
import random
index_to_departments = { # 部门编号和部门名称的对应关系
1: "电脑部",
@ -24,6 +25,7 @@ def read_excel(file_path):
want_num_array = [] # 长度为 N 的数组,表示每位学生想要值班的次数
is_new_array = [] # 长度为 N 的数组,表示每位学生是否是小朋友
is_tech_array = [] # 长度为 N 的数组,表示每位学生是否是电脑部或电器部成员
is_hr_array = [] # 长度为 N 的数组,表示每位学生是否是人资部成员
'''
遍历每一行对于每一行
@ -65,7 +67,10 @@ def read_excel(file_path):
# 是否是电脑部或电器部成员
is_tech_array.append(index_to_departments[info_list[7]] in ["电脑部", "电器部"])
return all_data, index_to_name_dict, preference_mat, want_num_array, is_new_array, is_tech_array
# 是否是电脑部或电器部成员
is_hr_array.append(index_to_departments[info_list[7]] == "人资部")
return all_data, index_to_name_dict, preference_mat, want_num_array, is_new_array, is_tech_array, is_hr_array
def save_to_excel(variables, all_data, index_to_name_dict, preference_mat, file_path):
@ -75,15 +80,42 @@ def save_to_excel(variables, all_data, index_to_name_dict, preference_mat, file_
for j in range(len(variables[0])):
on_duty_list = []
single_class_num = 0
hr_new_index = []
none_tech_new_index = []
new_index = []
all_index = []
_cnt = 0
for i in range(len(variables)):
if variables[i][j] == 1:
single_stu_info = {}
single_stu_info["name"] = index_to_name_dict[i]
single_stu_info["department"] = all_data[index_to_name_dict[i]][7]
single_stu_info["type"] = all_data[index_to_name_dict[i]][8]
single_stu_info["adjust"] = preference_mat[i][j] <= 0
single_stu_info["duty_head"] = False
on_duty_list.append(single_stu_info)
single_class_num += 1
all_index.append(_cnt)
if index_to_type[single_stu_info["type"]] == "小朋友":
new_index.append(_cnt)
if single_stu_info["department"] in [3,4,5]:
none_tech_new_index.append(_cnt)
if single_stu_info["department"] == 3:
hr_new_index.append(_cnt)
_cnt += 1
# 为每一班的值班人员中的值班组长打上标记
duty_head_index = -1
if hr_new_index:
duty_head_index = random.choice(hr_new_index)
elif none_tech_new_index:
duty_head_index = random.choice(none_tech_new_index)
elif new_index:
duty_head_index = random.choice(new_index)
else:
duty_head_index = random.choice(all_index)
on_duty_list[duty_head_index]["duty_head"] = True
on_duty_list = sorted(on_duty_list, key=lambda x: x["department"]) # 按部门编号升序排序
all_result.append(on_duty_list)
max_single_class_num = max(max_single_class_num, single_class_num)
@ -129,8 +161,8 @@ def save_to_excel(variables, all_data, index_to_name_dict, preference_mat, file_
col = chr(ord('B')+duty_index//3)
row = start_index[duty_index%3]+stu_index
str_info = f"{stu['name']} {index_to_departments[stu['department']]} {index_to_type[stu['type']]}"
if stu['adjust']:
ws[col+str(row)].fill = PatternFill(fill_type='solid', start_color='FF0000', end_color='FF0000')
if stu['duty_head']:
ws[col+str(row)].fill = PatternFill(fill_type='solid', start_color='FFFF00', end_color='FFFF00')
ws[col+str(row)] = str_info

View File

@ -1,4 +1,4 @@
# EVA 值班排班工具使用手册 - v1.0.0
# EVA 值班排班工具使用手册 - v1.1.0
## 前言
这是浙江大学学生 E 志者协会“排班工具软件”的使用手册,将简要地介绍该软件的使用方法和注意事项。请注意,这是面向使用者而非开发者的手册,如果想了解该工具的开发流程和所使用的技术,请移步至 github 仓库中的说明手册(尚未上传)。
@ -9,9 +9,16 @@
- 本软件将排班问题建模为组合优化问题,以“尽可能减少被调剂的人次”为优化目标。在指定的限制条件下,若有解,则得到的一定是最优解。
## 软件使用方法
双击运行可执行文件,进行以下三个步骤。
1. 选择待输入的问卷星结果 excel 表格。对于选择的 excel 表格,有以下格式要求:
双击运行可执行文件,会看到如下软件界面:
<div style="text-align: center;">
<img src="./pics/screen.jpg" style="width:50%;">
</div>
进行以下三个步骤:
1. 选择待输入的问卷星结果 excel 表格。对于选择的 excel 表格,有以下**格式要求**
- 第 1 行,即表头的名称不会影响软件运行,但需要保证每一列的数据的格式和含义正确
- 第 1 列为序号,不会用到,但需要有
- 第 2 列为提交答卷时间,格式为 "year/month/day hour:min:sec"
@ -40,22 +47,20 @@
- 第 10 ~ 29 列表示对于每个班次的意愿1 代表有时间0 代表没时间)。因为从周一第一班到周日第二班一共有 20 班,所以总共有 20 列。
- 第 30 列,表示愿意排几班,用一个数字表示。
我提供了一个名为“问卷星结果_样例输入.xlsx”的文件以作为正确输入格式的参考。
**我提供了一个名为“问卷星结果_样例输入.xlsx”的文件以作为正确输入格式的参考。**
本软件可以处理同一名同学多次填写问卷的情况,只要保证多次填写中“姓名”保持一致即可,软件将会自动选择最后一次填写的结果作为最终意愿。由于“姓名”是每位同学的唯一标识符,如果出现了某位同学“姓名”填错的情况,需要手动删除该姓名的记录。
**本软件可以自动处理同一名同学多次填写问卷的情况**,只要保证多次填写中“姓名”保持一致即可,软件将会自动选择最后一次填写的结果作为最终意愿。由于“姓名”是每位同学的唯一标识符,如果出现了某位同学“姓名”填错的情况,需要手动删除该姓名的记录。
如果出现了两位同学撞名的情况需要在填表时“姓名”后加上后缀以作区分比如“王五1”和“王五2”否则就会当作一个人来看待。
如果出现了**两位同学撞名的情况**需要在填表时“姓名”后加上后缀以作区分比如“王五1”和“王五2”否则就会当作一个人来看待。
2. 输入想要的限制条件。
2. 输入想要的限制条件。**若勾选“自动模式”则跳过这一步**
本软件是以“同学接受调剂”作为大前提,“限制条件必须满足”的条件下,以“尽可能减少被调剂的同学人次数”作为最终目的来求解的。所以设置的限制条件越紧,最后就有可能越多的同学被调剂
本软件是在“限制条件必须满足”的条件下以“每班次人数尽可能平均”作为最终目的来求解的。设置的限制条件太紧的话最后可能导致无解比如要求每班有4人但实际上有一班选的人数就只有3人这时候就需要手动宽松下限制参数
本软件预设了一些限制的参数。当然,你可以自由修改。我推荐多尝试几个不同的限制,如果最后只有很少数的人次被调剂,再人工调整下最终的排班表
本软件提供了自动模式来帮你尝试不同的参数并选择一个局部最优解。在自动模式下,用户无法自主设置参数
3. 开始排班
点击“开始排班!”按钮以开始排班,输出结果会以 excel 表格的形式输出输出的文件名称是“result_<当前时间戳>.xlsx”
在输出结果中,被调剂的人次都会被标红。
输出结果不会自动分配每一班的组长,需要最后人工分配下。
**在输出结果中,组长会被标黄。**