v1.1.0
parent
89132986a5
commit
4f7c605da8
292
main.py
292
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,9 +232,38 @@ 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:
|
||||
|
|
@ -194,9 +272,10 @@ class MyWidget(QtWidgets.QWidget):
|
|||
|
||||
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)
|
||||
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}")
|
||||
|
|
@ -212,7 +291,196 @@ class MyWidget(QtWidgets.QWidget):
|
|||
|
||||
all_sum = sum(want_num_array)
|
||||
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}")
|
||||
|
||||
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
|
||||
|
||||
# 每班至多?个人资小朋友
|
||||
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
|
||||
|
|
@ -220,13 +488,16 @@ class MyWidget(QtWidgets.QWidget):
|
|||
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
|
||||
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,
|
||||
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_new_min=num_new_min, num_new_max=num_new_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
|
||||
|
|
@ -240,6 +511,9 @@ class MyWidget(QtWidgets.QWidget):
|
|||
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([])
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
105
solve.py
105
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
|
||||
|
||||
|
|
|
|||
40
utils.py
40
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
|
||||
|
||||
|
||||
|
|
|
|||
29
使用手册.md
29
使用手册.md
|
|
@ -1,4 +1,4 @@
|
|||
# EVA 值班排班工具使用手册 - v1.0.0
|
||||
# EVA 值班排班工具使用手册 - v1.1.0
|
||||
|
||||
## 前言
|
||||
这是浙江大学学生 E 志者协会“排班工具软件”的使用手册,将简要地介绍该软件的使用方法和注意事项。请注意,这是面向使用者而非开发者的手册,如果想了解该工具的开发流程和所使用的技术,请移步至 github 仓库中的说明手册(尚未上传)。
|
||||
|
|
@ -9,9 +9,16 @@
|
|||
- 本软件将排班问题建模为组合优化问题,以“尽可能减少被调剂的人次”为优化目标。在指定的限制条件下,若有解,则得到的一定是最优解。
|
||||
|
||||
## 软件使用方法
|
||||
双击运行可执行文件,进行以下三个步骤。
|
||||
|
||||
1. 选择待输入的问卷星结果 excel 表格。对于选择的 excel 表格,有以下格式要求:
|
||||
双击运行可执行文件,会看到如下软件界面:
|
||||
|
||||
<div style="text-align: center;">
|
||||
<img src="./pics/screen.jpg" style="width:50%;">
|
||||
</div>
|
||||
|
||||
进行以下三个步骤:
|
||||
|
||||
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”
|
||||
|
||||
在输出结果中,被调剂的人次都会被标红。
|
||||
|
||||
输出结果不会自动分配每一班的组长,需要最后人工分配下。
|
||||
**在输出结果中,组长会被标黄。**
|
||||
|
|
|
|||
Loading…
Reference in New Issue