diff --git a/main.py b/main.py index ca54480..c15e4d9 100644 --- a/main.py +++ b/main.py @@ -6,18 +6,39 @@ from solve import solve_program from datetime import datetime import traceback +# 约束条件配置 +CONSTRAINTS_CONFIG = [ + ("每班次人数:", "4", "8"), + ("每班次电脑或电器的老人数:", "", "2"), + ("每班次老人数:", "1", ""), + ("每班次人资小朋友数:", "1", ""), + ("每班次小朋友数:", "2", "") +] + +# 参数调优顺序和尝试值 +PARAM_CONFIGS = [ + ('num_tech_min', [1, 0]), + ('num_tech_max', [1, 2, 3, 4, 5, 6, 7, 8]), + ('num_old_min', [2, 1, 0]), + ('num_old_max', [1, 2, 3, 4, 5, 6, 7, 8]), + ('num_min', [4, 3, 2, 1]), + ('num_max', [8, 9, 10, 11, 12, 13, 14, 15]), + ('num_new_min', [1, 0]), + ('num_hr_min', [1, 0]), + ('num_hr_max', [1, 2, 3, 4, 5, 6, 7, 8]) +] + class MyWidget(QtWidgets.QWidget): def __init__(self): super().__init__() - self.main_layout = QtWidgets.QVBoxLayout() - self.setLayout(self.main_layout) + self.main_layout = QtWidgets.QVBoxLayout(self) bold_font = QFont() - bold_font.setBold(True) # 设置字体加粗 + bold_font.setBold(True) thin_font = QFont() - thin_font.setBold(False) # 设置字体不加粗 + thin_font.setBold(False) # 选择文件部分 self._excel_dir = None @@ -55,151 +76,18 @@ class MyWidget(QtWidgets.QWidget): 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 - self.label_cond_1_1 = QtWidgets.QLabel("每班次人数:", self) - self.label_cond_1_1.setFont(thin_font) - # 数字输入框1 - self.line_edit_1_1 = QtWidgets.QLineEdit(self) - self.line_edit_1_1.setFont(thin_font) - 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) - # 数字输入框2 - self.line_edit_1_2 = QtWidgets.QLineEdit(self) - self.line_edit_1_2.setFont(thin_font) - 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) - self.cond_layout_1.addWidget(self.label_cond_1_2) - self.cond_layout_1.addWidget(self.line_edit_1_2) - self.cond_layout_overall.addLayout(self.cond_layout_1) - - # 限制条件2 - self.cond_layout_2 = QtWidgets.QHBoxLayout() - # 文字1 - self.label_cond_2_1 = QtWidgets.QLabel("每班次电脑或电器的老人数:", self) - self.label_cond_2_1.setFont(thin_font) - # 数字输入框1 - self.line_edit_2_1 = QtWidgets.QLineEdit(self) - 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) - # 数字输入框2 - self.line_edit_2_2 = QtWidgets.QLineEdit(self) - 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("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) - self.cond_layout_2.addWidget(self.label_cond_2_2) - self.cond_layout_2.addWidget(self.line_edit_2_2) - self.cond_layout_overall.addLayout(self.cond_layout_2) - - # 限制条件3 - self.cond_layout_3 = QtWidgets.QHBoxLayout() - # 文字1 - self.label_cond_3_1 = QtWidgets.QLabel("每班次老人数:", self) - self.label_cond_3_1.setFont(thin_font) - # 数字输入框1 - self.line_edit_3_1 = QtWidgets.QLineEdit(self) - self.line_edit_3_1.setFont(thin_font) - 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) - # 数字输入框2 - self.line_edit_3_2 = QtWidgets.QLineEdit(self) - 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) - self.cond_layout_3.addWidget(self.label_cond_3_2) - self.cond_layout_3.addWidget(self.line_edit_3_2) - self.cond_layout_overall.addLayout(self.cond_layout_3) - - # 限制条件4 - self.cond_layout_4 = QtWidgets.QHBoxLayout() - # 文字1 - 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("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) - # 数字输入框2 - self.line_edit_4_2 = QtWidgets.QLineEdit(self) - 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) - self.cond_layout_4.addWidget(self.label_cond_4_2) - 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.constraint_widgets = {} + for i, (label_text, min_value, max_value) in enumerate(CONSTRAINTS_CONFIG, 1): + layout, min_edit, max_edit = self._create_constraint_row(label_text, min_value, max_value, thin_font) + self.constraint_widgets[f'layout_{i}'] = layout + self.constraint_widgets[f'min_{i}'] = min_edit + self.constraint_widgets[f'max_{i}'] = max_edit + self.cond_layout_overall.addLayout(layout) self.main_layout.addWidget(self.group_box_2) - # 处理文件部分 self.group_box_3 = QtWidgets.QGroupBox("Step 3. 获取最优结果") self.group_box_3.setFont(bold_font) @@ -223,41 +111,50 @@ class MyWidget(QtWidgets.QWidget): self.group_box_3.setLayout(self.solve_layout) self.main_layout.addWidget(self.group_box_3) - def open_file(self): - # 弹出文件选择对话框 - file_path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "打开文件", "", "所有文件 (*.*);;文本文件 (*.txt)") + def _create_constraint_row(self, label_text, min_value, max_value, font): + """创建一行限制条件输入框""" + layout = QtWidgets.QHBoxLayout() - # 如果用户选择了文件,则更新标签显示文件路径 + label = QtWidgets.QLabel(label_text, self) + label.setFont(font) + + # 创建输入框的辅助函数 + def create_input(value): + edit = QtWidgets.QLineEdit(self) + edit.setFont(font) + edit.setValidator(QtGui.QIntValidator()) + edit.setPlaceholderText("无限制") + if value: + edit.setText(value) + edit.setEnabled(False) + return edit + + min_edit = create_input(min_value) + to_label = QtWidgets.QLabel("到", self) + to_label.setFont(font) + max_edit = create_input(max_value) + + layout.addWidget(label) + layout.addWidget(min_edit) + layout.addWidget(to_label) + layout.addWidget(max_edit) + + return layout, min_edit, max_edit + + def open_file(self): + file_path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "打开文件", "", "所有文件 (*.*);;文本文件 (*.txt)") if file_path: 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) + enabled = state == 0 + self._auto_mode = not enabled + + for i in range(1, 6): + self.constraint_widgets[f'min_{i}'].setEnabled(enabled) + self.constraint_widgets[f'max_{i}'].setEnabled(enabled) @QtCore.Slot() @@ -275,28 +172,21 @@ class MyWidget(QtWidgets.QWidget): 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}") - - 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 = sum(want_num_array[i] for i in range(len(preference_mat)) + if not is_new_array[i] and is_tech_array[i]) all_sum = sum(want_num_array) + min_prefer = min(sum(preference_mat[i][j] for i in range(len(preference_mat))) + for j in range(class_num)) + + self.text_solve.append("信息统计:") + self.text_solve.append(f"\t学生总人数:{stu_num}") + self.text_solve.append(f"\t班次总数:{class_num}") + self.text_solve.append(f"\t电脑部或电气部的所有老人的意愿班次之和:{tech_sum}") self.text_solve.append(f"\t所有人的意愿班次之和:{all_sum}") self.text_solve.append(f"\t平均每班人数:{all_sum/20}") - - 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}") vars = [] @@ -304,201 +194,78 @@ class MyWidget(QtWidgets.QWidget): if self._auto_mode: self.text_solve.append(f"自动调参开始...") - # 至少?个电脑或电器的老人 - 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 + # 自动调参配置 + auto_params: dict[str, int | None] = { + '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 + } - # 每班至多?个人资小朋友 - 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 + # 逐步优化参数 + for param_name, try_values in PARAM_CONFIGS: + value = None + vars = None + for try_value in try_values: + test_params = auto_params.copy() + test_params[param_name] = try_value + 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, + **test_params + ) + if vars: + value = try_value + break + if value: + auto_params[param_name] = value + else: 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]") + self.text_solve.append(f"\t每班人数:[{auto_params['num_min']}, {auto_params['num_max']}]") + self.text_solve.append(f"\t每班电脑或电器的老人数:[{auto_params['num_tech_min']}, {auto_params['num_tech_max']}]") + self.text_solve.append(f"\t每班老人数:[{auto_params['num_old_min']}, {auto_params['num_old_max']}]") + self.text_solve.append(f"\t每班人资部小朋友数:[{auto_params['num_hr_min']}, {auto_params['num_hr_max']}]") + self.text_solve.append(f"\t每班小朋友数:[{auto_params['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 + def get_value(key): + text = self.constraint_widgets[key].text() + return int(text) if text else None + + manual_params = { + 'num_min': get_value('min_1'), + 'num_max': get_value('max_1'), + 'num_tech_min': get_value('min_2'), + 'num_tech_max': get_value('max_2'), + 'num_old_min': get_value('min_3'), + 'num_old_max': get_value('max_3'), + 'num_hr_min': get_value('min_4'), + 'num_hr_max': get_value('max_4'), + 'num_new_min': get_value('min_5'), + 'num_new_max': get_value('max_5') + } 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) + 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, + **manual_params + ) if vars is None: self.text_solve.append("在目前限制条件下无解!请尝试更改限制条件!") @@ -508,12 +275,10 @@ class MyWidget(QtWidgets.QWidget): # 保存结果到 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, save_dir) + save_dir = f"result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" + save_to_excel(vars, all_data, index_to_name_dict, save_dir) self.text_solve.append(f"保存结果成功!保存路径:{save_dir}") - except Exception as e: + except Exception: self.text_solve.append("程序出现严重错误,请联系开发者解决问题!!!") self.text_solve.append(f"Error Details:\n{traceback.format_exc()}") diff --git a/solve.py b/solve.py index 0396587..8806b96 100644 --- a/solve.py +++ b/solve.py @@ -18,128 +18,87 @@ def solve_program(preference_mat:list, ): is_old_array = [not is_new for is_new in is_new_array] - is_not_tech_array = [not is_tech for is_tech in is_tech_array] - # 这是一个组合优化问题,我们使用 SCIP 求解器来解决这个问题 + # 使用 SCIP 求解器求解组合优化问题 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 # 每班次的平均人数 + 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 = 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) + # 创建决策变量:variables[i][j] 表示第i个学生是否选择第j个班次 + variables = [[solver.IntVar(0.0, 1.0, f"choice_{i}_{j}") + for j in range(M)] for i in range(N)] + + # 创建辅助变量用于优化目标(均衡班次人数) + aux_vars = [solver.NumVar(0.0, solver.infinity(), f"aux_{j}") + for j in range(M)] 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)) # 每班次的实际人数 + 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): + total_shifts = sum(variables[i]) if want_num_array[i] == 3: - solver.Add(sum(variables[i]) <= 3) - solver.Add(sum(variables[i]) >= 2) + solver.Add(total_shifts >= 2) + solver.Add(total_shifts <= 3) else: - solver.Add(sum(variables[i]) == want_num_array[i]) + solver.Add(total_shifts == want_num_array[i]) - # 添加约束:每个班次至少有?位同学 - if num_min is not None: + # 添加班次人数约束的辅助函数 + def add_shift_constraint(array, min_val, max_val): for j in range(M): - solver.Add(sum(variables[i][j] for i in range(N)) >= num_min) + shift_count = sum(variables[i][j] * array[i] for i in range(N)) + if min_val is not None: + solver.Add(shift_count >= min_val) + if max_val is not None: + solver.Add(shift_count <= max_val) - # 添加约束:每个班次至多有?位同学 - if num_max is not None: - for j in range(M): - solver.Add(sum(variables[i][j] for i in range(N)) <= num_max) + # 约束:每个班次的总人数 + add_shift_constraint([1] * N, num_min, num_max) + + # 约束:每班次电脑或电器的老人数量 + tech_old_array = [is_old_array[i] * is_tech_array[i] for i in range(N)] + add_shift_constraint(tech_old_array, num_tech_min, num_tech_max) + + # 约束:每班次老人数量 + add_shift_constraint(is_old_array, num_old_min, num_old_max) + + # 约束:每班次人资部小朋友数量 + add_shift_constraint(is_hr_array, num_hr_min, num_hr_max) + + # 约束:每班次小朋友数量 + add_shift_constraint(is_new_array, num_new_min, num_new_max) - # 添加约束:每班次最少包含?个电脑或电器的老人 - if num_tech_min is not None: - for j in range(M): - solver.Add(sum(variables[i][j]*is_old_array[i]*is_tech_array[i] for i in range(N)) >= num_tech_min) + # 优化目标:最小化各班次人数与平均人数的偏差总和 + solver.Minimize(sum(aux_vars)) - # 添加约束:每班次最多包含?个电脑或电器的老人 - if num_tech_max is not None: - for j in range(M): - solver.Add(sum(variables[i][j]*is_old_array[i]*is_tech_array[i] for i in range(N)) <= num_tech_max) - - # 添加约束:每班次至少包含?个老人 - if num_old_min is not None: - for j in range(M): - solver.Add(sum(variables[i][j]*is_old_array[i] for i in range(N)) >= num_old_min) - - # 添加约束:每班次至多包含?个老人 - if num_old_max is not None: - for j in range(M): - solver.Add(sum(variables[i][j]*is_old_array[i] for i in range(N)) <= num_old_max) - - # 添加约束:每班次至少包含?个人资部小朋友 - 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): - solver.Add(sum(variables[i][j]*is_new_array[i] for i in range(N)) >= num_new_min) - - # 添加约束:每班次至多包含?个小朋友 - if num_new_max is not None: - for j in range(M): - solver.Add(sum(variables[i][j]*is_new_array[i] for i in range(N)) <= num_new_max) - - # 优化目标:每班次人数尽可能平均 - solver.Minimize(sum(aux_vars)) # Maximize the sum of these variables. - - # 求解优化问题 + # 求解 status = solver.Solve() - # 输出结果 - variables_return = [] - aux_vars_return = [] if status == pywraplp.Solver.OPTIMAL: print("Optimal solution found:") - for i in range(N): - row_solution = [variables[i][j].solution_value() for j in range(M)] - variables_return.append(row_solution) - - for j in range(M): - aux_vars_return.append(aux_vars[j].solution_value()) - - # Print the optimized value of the objective function. + variables_return = [[variables[i][j].solution_value() for j in range(M)] + for i in range(N)] + aux_vars_return = [aux_vars[j].solution_value() for j in range(M)] + print(f"Optimized objective value: {solver.Objective().Value()}") print(aux_vars_return) - return variables_return - else: print("No solution found.") - return None \ No newline at end of file diff --git a/utils.py b/utils.py index b330b93..fb65f6c 100644 --- a/utils.py +++ b/utils.py @@ -31,7 +31,6 @@ index_to_type = { # 老人 / 小朋友和对应的编号 } def read_excel(file_path): - # 读取 Excel 文件 df = pd.read_excel(file_path) # 待返回的所有信息。N 为学生人数,M 为班次数 @@ -46,66 +45,68 @@ def read_excel(file_path): 考虑到原始填表信息中可能有某位同学多次提交的记录,先过滤一下冗余信息 ''' all_data = {} # 储存过滤后的数据 - format = "%Y/%m/%d %H:%M:%S" + time_format = "%Y/%m/%d %H:%M:%S" for index, row in df.iterrows(): - # 读取学生姓名,比较时间戳 name = row[col_name] - time_this = datetime.strptime(row[col_timestamp], format) - if (name not in all_data) or (time_this > datetime.strptime(all_data[name][col_timestamp], format)): - info_list = row.tolist() - all_data[name] = info_list + time_this = datetime.strptime(row[col_timestamp], time_format) + if name not in all_data or time_this > datetime.strptime(all_data[name][col_timestamp], time_format): + all_data[name] = row.tolist() + + # 构建返回的数据结构 + index_to_name_dict = {} + preference_mat = [] + want_num_array = [] + is_new_array = [] + is_tech_array = [] + is_hr_array = [] - # 遍历过滤后的数据,组建待返回的信息 for index, (name, info_list) in enumerate(all_data.items()): - # 学生姓名 index_to_name_dict[index] = name - # 意愿度 preference_mat.append(info_list[col_dutystart:col_dutyend + 1]) - # 想要值班的次数 want_num_array.append(info_list[col_dutyfreq]) - # 是否是小朋友 is_new_array.append(index_to_type[info_list[col_member]] == "小朋友") - # 是否是电脑部或电器部成员 is_tech_array.append(index_to_departments[info_list[col_department]] in ["电脑部", "电器部"]) - # 是否是人资部成员 is_hr_array.append(index_to_departments[info_list[col_department]] == "人资部") 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, file_path): - # 用一个 list 储存每一班的值班人员,该 list 中每个元素是一个存有若干 dict 的 list,每个 dict 表示某一班的某一值班人员信息 + # 收集每个班次的值班人员信息 all_result = [] - max_single_class_num = 0 # 用于记录最大的班次人数,以便后写入 excel + max_single_class_num = 0 + 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"] = index_to_departments[all_data[index_to_name_dict[i]][col_department]] - single_stu_info["type"] = index_to_type[all_data[index_to_name_dict[i]][col_member]] - single_stu_info["duty_monitor"] = False + name = index_to_name_dict[i] + department = index_to_departments[all_data[name][col_department]] + member_type = index_to_type[all_data[name][col_member]] + + single_stu_info = { + "name": name, + "department": department, + "type": member_type, + "duty_monitor": False + } on_duty_list.append(single_stu_info) - single_class_num += 1 + + idx = len(all_index) + all_index.append(idx) + if member_type == "小朋友": + new_index.append(idx) + if department in ["人资部", "财外部", "文宣部"]: + none_tech_new_index.append(idx) + if department == "人资部": + hr_new_index.append(idx) - all_index.append(_cnt) - if single_stu_info["type"] == "小朋友": - new_index.append(_cnt) - if single_stu_info["department"] in ["人资部", "财外部", "文宣部"]: - none_tech_new_index.append(_cnt) - if single_stu_info["department"] == 3: - hr_new_index.append(_cnt) - _cnt += 1 - - # 为每一班的值班组长打上标记 - duty_monitor_index = -1 + # 选择值班组长(优先级:人资部小朋友 > 非技术部小朋友 > 小朋友 > 所有人) if hr_new_index: duty_monitor_index = random.choice(hr_new_index) elif none_tech_new_index: @@ -116,56 +117,43 @@ def save_to_excel(variables, all_data, index_to_name_dict, file_path): duty_monitor_index = random.choice(all_index) on_duty_list[duty_monitor_index]["duty_monitor"] = True - on_duty_list = sorted(on_duty_list, key=lambda x: x["department"]) # 按部门编号升序排序 + on_duty_list.sort(key=lambda x: x["department"]) all_result.append(on_duty_list) - max_single_class_num = max(max_single_class_num, single_class_num) + max_single_class_num = max(max_single_class_num, len(on_duty_list)) from openpyxl import Workbook from openpyxl.styles import PatternFill - # 创建一个新的工作簿 wb = Workbook() - ws = wb.active - if ws is None: - ws = wb.create_sheet(title="Sheet1") + ws = wb.active if wb.active else wb.create_sheet(title="Sheet1") - # 写入表头 - width = 25 - ws['B1'] = "星期一" - ws.column_dimensions['B'].width = width - ws['C1'] = "星期二" - ws.column_dimensions['C'].width = width - ws['D1'] = "星期三" - ws.column_dimensions['D'].width = width - ws['E1'] = "星期四" - ws.column_dimensions['E'].width = width - ws['F1'] = "星期五" - ws.column_dimensions['F'].width = width - ws['G1'] = "星期六" - ws.column_dimensions['G'].width = width - ws['H1'] = "星期日" - ws.column_dimensions['H'].width = width + # 设置表头 + weekdays = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] + for idx, day in enumerate(weekdays, start=2): + col = chr(ord('A') + idx - 1) + ws[f'{col}1'] = day + ws.column_dimensions[col].width = 25 + # 设置班次标题和起始行 + shift_names = ["第一班", "第二班", "第三班"] start_index = [] - ws.merge_cells(f'A2:A{2 + max_single_class_num}') - ws['A2'] = "第一班" - start_index.append(2) - ws.merge_cells(f'A{2 + max_single_class_num + 1}:A{2 + 2 * max_single_class_num + 1}') - ws[f'A{2 + max_single_class_num + 1}'] = "第二班" - start_index.append(2 + max_single_class_num + 1) - ws.merge_cells(f'A{2 + 2 * max_single_class_num + 2}:A{2 + 3 * max_single_class_num + 2}') - ws[f'A{2 + 2 * max_single_class_num + 2}'] = "第三班" - start_index.append(2 + 2 * max_single_class_num + 2) + for shift_idx, shift_name in enumerate(shift_names): + start_row = 2 + shift_idx * (max_single_class_num + 1) + end_row = start_row + max_single_class_num + ws.merge_cells(f'A{start_row}:A{end_row}') + ws[f'A{start_row}'] = shift_name + start_index.append(start_row) + # 填写值班人员信息 + yellow_fill = PatternFill(fill_type='solid', start_color='FFFF00', end_color='FFFF00') for duty_index, duty in enumerate(all_result): + col = chr(ord('B') + duty_index // 3) for stu_index, stu in enumerate(duty): - col = chr(ord('B') + duty_index // 3) row = start_index[duty_index % 3] + stu_index - str_info = f"{stu['name']} {stu['department']} {stu['type']}" + cell = ws[f'{col}{row}'] + cell.value = f"{stu['name']} {stu['department']} {stu['type']}" if stu['duty_monitor']: - ws[col + str(row)].fill = PatternFill(fill_type='solid', start_color='FFFF00', end_color='FFFF00') - ws[col + str(row)] = str_info + cell.fill = yellow_fill - # 保存文件 wb.save(file_path) \ No newline at end of file