From 89132986a5c8866637eff83130de6dc45a660d6f Mon Sep 17 00:00:00 2001 From: happyw1nd Date: Sun, 12 Jan 2025 18:24:17 +0800 Subject: [PATCH] first commit --- .gitignore | 6 + main.py | 252 +++++++++++++++++++++++++++++++++++++++ solve.py | 118 ++++++++++++++++++ utils.py | 138 +++++++++++++++++++++ 使用手册.md | 61 ++++++++++ 问卷星结果_样例输入.xlsx | Bin 0 -> 17674 bytes 6 files changed, 575 insertions(+) create mode 100644 .gitignore create mode 100644 main.py create mode 100644 solve.py create mode 100644 utils.py create mode 100644 使用手册.md create mode 100644 问卷星结果_样例输入.xlsx 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 0000000000000000000000000000000000000000..f16b1035daaeb0d8387ed206e0bc1d3c9e70c273 GIT binary patch literal 17674 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
    H#w4R>gI%2#!k5a;U(vUyz}2KWZaL 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 literal 0 HcmV?d00001