diff --git a/main.py b/main.py index 1e0a018..8db6e47 100644 --- a/main.py +++ b/main.py @@ -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("信息统计:") - stu_num = len(index_to_name_dict) - self.text_solve.append(f"\t学生总人数:{stu_num}") + 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}") - 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([]) diff --git a/pics/screen.jpg b/pics/screen.jpg new file mode 100644 index 0000000..30ceae4 Binary files /dev/null and b/pics/screen.jpg differ diff --git a/solve.py b/solve.py index 66c5365..69fc312 100644 --- a/solve.py +++ b/solve.py @@ -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 diff --git a/utils.py b/utils.py index d72821b..3ac0743 100644 --- a/utils.py +++ b/utils.py @@ -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 diff --git a/使用手册.md b/使用手册.md index 0e5d48b..cf950b1 100644 --- a/使用手册.md +++ b/使用手册.md @@ -1,4 +1,4 @@ -# EVA 值班排班工具使用手册 - v1.0.0 +# EVA 值班排班工具使用手册 - v1.1.0 ## 前言 这是浙江大学学生 E 志者协会“排班工具软件”的使用手册,将简要地介绍该软件的使用方法和注意事项。请注意,这是面向使用者而非开发者的手册,如果想了解该工具的开发流程和所使用的技术,请移步至 github 仓库中的说明手册(尚未上传)。 @@ -9,9 +9,16 @@ - 本软件将排班问题建模为组合优化问题,以“尽可能减少被调剂的人次”为优化目标。在指定的限制条件下,若有解,则得到的一定是最优解。 ## 软件使用方法 -双击运行可执行文件,进行以下三个步骤。 -1. 选择待输入的问卷星结果 excel 表格。对于选择的 excel 表格,有以下格式要求: +双击运行可执行文件,会看到如下软件界面: + +
+