From 4f7c605da81e3722d8bc3ea8a240d502928c73d0 Mon Sep 17 00:00:00 2001 From: happyw1nd Date: Fri, 17 Jan 2025 01:40:38 +0800 Subject: [PATCH] v1.1.0 --- main.py | 374 +++++++++++++++++++++++++++++++++++++++++------- pics/screen.jpg | Bin 0 -> 38039 bytes solve.py | 105 ++++++++------ utils.py | 40 +++++- 使用手册.md | 29 ++-- 5 files changed, 442 insertions(+), 106 deletions(-) create mode 100644 pics/screen.jpg 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 0000000000000000000000000000000000000000..30ceae49163683e3d93700e5c07d76e00e6c7c05 GIT binary patch literal 38039 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

w>r{lmaqj1N|AH-Co*b( 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 literal 0 HcmV?d00001 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 表格,有以下格式要求: +双击运行可执行文件,会看到如下软件界面: + +

+ +
+ +进行以下三个步骤: + +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” - 在输出结果中,被调剂的人次都会被标红。 - - 输出结果不会自动分配每一班的组长,需要最后人工分配下。 \ No newline at end of file + **在输出结果中,组长会被标黄。**