commit 89132986a5c8866637eff83130de6dc45a660d6f Author: happyw1nd Date: Sun Jan 12 18:24:17 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e048a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +build/ +dist/ +other/ +*.egg-info/ +*.pdf diff --git a/main.py b/main.py new file mode 100644 index 0000000..1e0a018 --- /dev/null +++ b/main.py @@ -0,0 +1,252 @@ +import sys +from PySide6 import QtCore, QtWidgets, QtGui +from PySide6.QtGui import QFont +from utils import read_excel, save_to_excel +from solve import solve_program +from datetime import datetime + +class MyWidget(QtWidgets.QWidget): + def __init__(self): + super().__init__() + + self.main_layout = QtWidgets.QVBoxLayout() + self.setLayout(self.main_layout) + + bold_font = QFont() + bold_font.setBold(True) # 设置字体加粗 + + thin_font = QFont() + thin_font.setBold(False) # 设置字体不加粗 + + # 选择文件部分 + self._excel_dir = None + self.group_box_1 = QtWidgets.QGroupBox("Step 1. 选择问卷星导出的表格文件") + self.group_box_1.setFont(bold_font) + + self.openfile_layout = QtWidgets.QVBoxLayout() + self.button_openfile = QtWidgets.QPushButton("选择文件") + self.button_openfile.setFont(thin_font) + self.button_openfile.clicked.connect(self.open_file) + self.label_openfile = QtWidgets.QLabel("未选择文件") + self.label_openfile.setFont(thin_font) + + self.openfile_layout.addWidget(self.button_openfile) + self.openfile_layout.addWidget(self.label_openfile) + self.group_box_1.setLayout(self.openfile_layout) + self.main_layout.addWidget(self.group_box_1) + + + # 限制条件部分 + self.group_box_2 = QtWidgets.QGroupBox("Step 2. 输入限制条件") + self.group_box_2.setFont(bold_font) + self.cond_layout_overall = QtWidgets.QVBoxLayout() + self.group_box_2.setLayout(self.cond_layout_overall) + + # 限制条件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") + # 文字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.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("无限制") + # 文字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("1") + + 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") + # 文字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.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("2") + # 文字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.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) + + + self.main_layout.addWidget(self.group_box_2) + + + # 处理文件部分 + self.group_box_3 = QtWidgets.QGroupBox("Step 3. 获取最优结果") + self.group_box_3.setFont(bold_font) + + self.button_solve = QtWidgets.QPushButton("开始排班!") + self.button_solve.setFont(thin_font) + self.text_solve = QtWidgets.QTextEdit(self) + self.text_solve.setFont(thin_font) + self.text_solve.setReadOnly(True) # 设置为只读模式 + + # 创建 QScrollArea 并将 QTextEdit 添加进去 + self.scroll_area = QtWidgets.QScrollArea(self) + self.scroll_area.setWidget(self.text_solve) # 将 QTextEdit 设置为 QScrollArea 的内容 + self.scroll_area.setWidgetResizable(True) # 允许 QTextEdit 根据 QScrollArea 的大小自动调整 + + self.button_solve.clicked.connect(self.magic) + + self.solve_layout = QtWidgets.QVBoxLayout() + self.solve_layout.addWidget(self.scroll_area) + self.solve_layout.addWidget(self.button_solve) + 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)") + + # 如果用户选择了文件,则更新标签显示文件路径 + if file_path: + self.label_openfile.setText(f"选中文件: {file_path}") + self._excel_dir = file_path + + @QtCore.Slot() + def magic(self): + self.text_solve.append("*******************") + + # 读取文件 + 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}") + + 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}") + + all_sum = sum(want_num_array) + self.text_solve.append(f"\t所有人的意愿班次之和:{all_sum}") + + # 读取限制条件 + 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 + + 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("计算最优解成功!") + + # 保存结果到 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}") + +if __name__ == "__main__": + app = QtWidgets.QApplication([]) + + widget = MyWidget() + widget.setWindowTitle("EVA 值班排班软件") + widget.resize(600, 600) + widget.show() + + sys.exit(app.exec()) \ No newline at end of file diff --git a/solve.py b/solve.py new file mode 100644 index 0000000..66c5365 --- /dev/null +++ b/solve.py @@ -0,0 +1,118 @@ +from ortools.sat.python import cp_model +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, + num_min=None, + num_max=None, + num_tech_min=None, + num_tech_max=None, + num_old_min=None, + num_old_max=None, + num_new_min=None, + num_new_max=None + ): + + 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] + + # 这是一个整数规划问题,我们使用 CP-SAT 求解器来解决这个问题 + model = cp_model.CpModel() + + # 定义变量的规模,一共有 N*M 个变量需要求解器求解 + N = len(preference_mat) # 学生人数 + M = len(preference_mat[0]) # 班次数 + + variables = [] + for i in range(N): + row_vars = [] + for j in range(M): + var = model.NewBoolVar(f"choice_{i}_{j}") + row_vars.append(var) + variables.append(row_vars) + + # 添加约束:满足每位同学的意愿班次 + for i in range(N): + model.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) + + # 添加约束:每个班次至多有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) + + # 添加约束:每班次最少包含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) + + # 添加约束:每班次最多包含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) + + # 添加约束:每班次至少包含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) + + # 添加约束:每班次至多包含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) + + # 添加约束:每班次至少包含n个小朋友 + 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) + + # 添加约束:每班次至多包含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) + + # 优化目标:最大化同学的满意度 + 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 = cp_model.CpSolver() + status = solver.Solve(model) + + # 输出结果 + variables_return = [] + if status == cp_model.OPTIMAL: + print("Optimal solution 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"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()}") + + return variables_return + + else: + print("No solution found.") + + return None + \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..d72821b --- /dev/null +++ b/utils.py @@ -0,0 +1,138 @@ +import pandas as pd +from datetime import datetime + +index_to_departments = { # 部门编号和部门名称的对应关系 + 1: "电脑部", + 2: "电器部", + 3: "人资部", + 4: "财外部", + 5: "文宣部" +} + +index_to_type = { # 老人/小朋友和对应的编号 + 1: "老人", + 2: "小朋友" +} + +def read_excel(file_path): + # 读取 Excel 文件 + df = pd.read_excel(file_path) + + # 待返回的所有信息。下令 N 为学生人数,M 为班次数 + index_to_name_dict = {} # 长度为 N 的字典,包含数组下标和学生姓名的对应关系 + preference_mat = [] # N x M 的二维数组,表示每位学生对每个班次的满意度 + want_num_array = [] # 长度为 N 的数组,表示每位学生想要值班的次数 + is_new_array = [] # 长度为 N 的数组,表示每位学生是否是小朋友 + is_tech_array = [] # 长度为 N 的数组,表示每位学生是否是电脑部或电器部成员 + + ''' + 遍历每一行,对于每一行: + i=1 -> 时间戳 + i=6 -> 姓名 + i=7 -> 部门编号 + i=8 -> 老人/小朋友 + i=9~28 -> 共20个班次,每个班次的意愿度 + i=29 -> 想要值班的次数 + + 考虑到原始填表信息中可能有某位同学多次提交的记录,先过滤一下冗余信息 + ''' + all_data = {} # 储存过滤后的数据 + for index, row in df.iterrows(): + # 读取学生姓名,比较时间戳 + name = row[6] + format = "%Y/%m/%d %H:%M:%S" + time_this = datetime.strptime(row[1], format) + if (name not in all_data) or (time_this > datetime.strptime(all_data[name][1], format)): + info_list = df.iloc[index].tolist() + all_data[name] = info_list + + # 遍历过滤后的数据,组建待返回的信息 + for index, (name, info_list) in enumerate(all_data.items()): + # 学生姓名 + index_to_name_dict[index] = name + + # 意愿度 + preference = info_list[9:29] + preference_mat.append(preference) + + # 想要值班的次数 + want_num = info_list[29] + want_num_array.append(want_num) + + # 是否是小朋友 + is_new_array.append(index_to_type[info_list[8]] == "小朋友") + + # 是否是电脑部或电器部成员 + 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 + +def save_to_excel(variables, all_data, index_to_name_dict, preference_mat, file_path): + + # 用一个list储存每一班的值班人员,该list中每个元素是一个存有若干dict的list,每个dict表示某一班的某一值班人员信息 + all_result = [] + max_single_class_num = 0 # 用于记录最大的班次人数,以便后写入 excel + for j in range(len(variables[0])): + on_duty_list = [] + single_class_num = 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 + on_duty_list.append(single_stu_info) + single_class_num += 1 + 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) + + + from openpyxl import Workbook + from openpyxl.styles import Font, Alignment, Border, Side, PatternFill + + # 创建一个新的工作簿 + wb = Workbook() + ws = wb.active + + # 写入表头 + 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 + + 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 duty_index, duty in enumerate(all_result): + 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']} {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') + ws[col+str(row)] = str_info + + + # 保存文件 + wb.save(file_path) \ No newline at end of file diff --git a/使用手册.md b/使用手册.md new file mode 100644 index 0000000..0e5d48b --- /dev/null +++ b/使用手册.md @@ -0,0 +1,61 @@ +# EVA 值班排班工具使用手册 - v1.0.0 + +## 前言 +这是浙江大学学生 E 志者协会“排班工具软件”的使用手册,将简要地介绍该软件的使用方法和注意事项。请注意,这是面向使用者而非开发者的手册,如果想了解该工具的开发流程和所使用的技术,请移步至 github 仓库中的说明手册(尚未上传)。 + +## 软件运行环境&基本情况 +- 本软件只能在 windows 系统上运行。 +- 本软件在部分版本的 win10 和 win11 系统上均测试过,能够正常运行。但是不排除可能出现缺少相关 dll 文件而无法运行的情况,如果出现请联系开发者解决。 +- 本软件将排班问题建模为组合优化问题,以“尽可能减少被调剂的人次”为优化目标。在指定的限制条件下,若有解,则得到的一定是最优解。 + +## 软件使用方法 +双击运行可执行文件,进行以下三个步骤。 + +1. 选择待输入的问卷星结果 excel 表格。对于选择的 excel 表格,有以下格式要求: + - 第 1 行,即表头的名称不会影响软件运行,但需要保证每一列的数据的格式和含义正确 + - 第 1 列为序号,不会用到,但需要有 + - 第 2 列为提交答卷时间,格式为 "year/month/day hour:min:sec" + - 第 3 列为填表所用时间,不会用到,但需要有 + - 第 4 列为来源,不会用到,但需要有 + - 第 5 列为来源详情,不会用到,但需要有 + - 第 6 列为 IP,不会用到,但需要有 + - 第 7 列为姓名,这是每一位同学的唯一标识 + - 第 8 列为部门序号,用一个数字表示,映射关系如下: + ```python + index_to_departments = { # 部门编号和部门名称的对应关系 + 1: "电脑部", + 2: "电器部", + 3: "人资部", + 4: "财外部", + 5: "文宣部" + } + ``` + - 第 9 列标识老人还是小朋友,用一个数字表示,映射关系如下: + ```python + index_to_type = { # 老人/小朋友和对应的编号 + 1: "老人", + 2: "小朋友" + } + ``` + - 第 10 ~ 29 列,表示对于每个班次的意愿(1 代表有时间,0 代表没时间)。因为从周一第一班到周日第二班一共有 20 班,所以总共有 20 列。 + - 第 30 列,表示愿意排几班,用一个数字表示。 + + 我提供了一个名为“问卷星结果_样例输入.xlsx”的文件,以作为正确输入格式的参考。 + + 本软件可以处理同一名同学多次填写问卷的情况,只要保证多次填写中“姓名”保持一致即可,软件将会自动选择最后一次填写的结果作为最终意愿。由于“姓名”是每位同学的唯一标识符,如果出现了某位同学“姓名”填错的情况,需要手动删除该姓名的记录。 + + 如果出现了两位同学撞名的情况,需要在填表时“姓名”后加上后缀以作区分,比如“王五1”和“王五2”,否则就会当作一个人来看待。 + +2. 输入想要的限制条件。 + + 本软件是以“同学接受调剂”作为大前提,“限制条件必须满足”的条件下,以“尽可能减少被调剂的同学人次数”作为最终目的来求解的。所以设置的限制条件越紧,最后就有可能越多的同学被调剂。 + + 本软件预设了一些限制的参数。当然,你可以自由修改。我推荐多尝试几个不同的限制,如果最后只有很少数的人次被调剂,再人工调整下最终的排班表。 + +3. 开始排班 + + 点击“开始排班!”按钮以开始排班,输出结果会以 excel 表格的形式输出,输出的文件名称是“result_<当前时间戳>.xlsx” + + 在输出结果中,被调剂的人次都会被标红。 + + 输出结果不会自动分配每一班的组长,需要最后人工分配下。 \ No newline at end of file diff --git a/问卷星结果_样例输入.xlsx b/问卷星结果_样例输入.xlsx new file mode 100644 index 0000000..f16b103 Binary files /dev/null and b/问卷星结果_样例输入.xlsx differ