|
|
||
|---|---|---|
| pics | ||
| .gitignore | ||
| .python-version | ||
| README.md | ||
| hook-ortools.py | ||
| main.py | ||
| pyproject.toml | ||
| solve.py | ||
| utils.py | ||
| uv.lock | ||
| 使用手册.md | ||
| 问卷星结果_样例输入.xlsx | ||
| 问卷星结果_较极端样例.xlsx | ||
README.md
EVA 值班排班工具
环境配置
本项目使用 uv 进行包管理。
安装项目中所需要的所有包的最新版本。其中 pyside6 是一个前端库,pyinstaller 用于打包项目,ortools 是一个高效的组合优化求解器,pandas 用于处理表格数据:
uv sync
项目结构
EVA_duty_arrange_tool/
├─ main.py // 主函数,定义了前端界面和组件的回调函数
├─ solve.py // 定义了值班排班问题的求解函数
├─ utils.py // 定义了读取、写入 excel 的函数
├─ pics // 储存了说明文档中用到的图片
│ ├─ *.jpg/*.png
├─ *.md // 说明文档
├─ *.xlsx // 测试用例
项目运行 & 打包
项目运行方式:
uv run main.py
本项目使用 pyinstaller 工具进行打包。如果要打包,请确保能够正常运行项目。打包命令如下:
uv run pyinstaller --onefile --windowed --name=EVA_duty_arrange_tool --hidden-import=ortools --collect-all ortools --additional-hooks-dir=. main.py
注意:由于
ortools库包含大量 DLL 依赖,需要使用--collect-all ortools参数来收集所有必要的动态链接库。--additional-hooks-dir=.参数指定使用项目根目录下的hook-ortools.py文件来确保正确打包。
数学原理
本项目将值班排班问题建模为了一个组合优化问题,使用 Google 的 OR-Tools 求解器中的 SCIP 求解器进行求解。
问题描述
在本问题中,优化目标是一个涉及多个方面的量化指标:
- 目标1:每一班同学数量的平均程度
- 目标2:每一班技术部老人数量接近期望值
- 目标3:每一班技术部小朋友数量接近期望值
- 目标4:每一班人资部小朋友数量接近期望值
- 目标5:每一班各部门人数的平均程度
在本问题中,主要约束是:
- 让每位同学每周的班次数符合意愿(特别地,选择3次的同学可以安排2-3次)
- 让每位同学只在自己有空的时间段值班
- 每班次的总人数在指定范围内
- 每班次技术部老人数量在指定范围内
- 每班次老人总数在指定范围内
- 每班次小朋友总数在指定范围内
数学建模
设一共有 n 位同学,m 个值班的班次,协会共有 t=5 个部门(电脑部、电器部、人资部、财外部、文宣部),定义以下符号:
决策变量:
x_{ij} \in \{0,1\}, \quad i=1,2,\dots,n,\quad j=1,2,\dots,m表示第i位同学是否值第j班
已知参数:
N_i \in \mathbb{N}, \quad i=1,2,\dots,n表示第i个同学每周愿意值班次数v_{ij} \in \{0,1\}, \quad i=1,2,\dots,n,\quad j=1,2,\dots,m表示第i位同学是否有空值第j班\text{old}_i \in \{0,1\}, \quad i=1,2,\dots,n表示第i位同学是否是老人(非小朋友)d_{ik}\in\{0,1\}, \quad i=1,2,\dots,n,\quad k=1,2,\dots,t表示第i位同学是否属于第k个部门\text{tech}_i = d_{i1} + d_{i2} \in \{0,1\}表示第i位同学是否属于技术部(电脑部或电器部)\text{hr}_i = d_{i3} \in \{0,1\}表示第i位同学是否属于人资部
派生量:
M_j = \sum_{i=1}^{n} x_{ij}, \quad j=1,2,\dots,m表示第j班次实际安排的值班人数\bar{M} = \frac{\sum_{i=1}^{n}N_i}{m}表示平均每班的人数
优化目标
为了处理多目标优化问题,我们采用线性加权法将多个目标组合成单一目标函数。引入归一化系数 m_1=2.5, m_2=3, m_3=4, m_4=1, m_5=8 和权重 w_1, w_2, w_3, w_4, w_5,总目标函数为:
\min \quad Z = w_1 \cdot \frac{X_1}{m_1 \cdot M} + w_2 \cdot \frac{X_2}{m_2 \cdot M} + w_3 \cdot \frac{X_3}{m_3 \cdot M} + w_4 \cdot \frac{X_4}{m_4 \cdot M} + w_5 \cdot \frac{X_5}{m_5 \cdot M}
各目标定义如下:
目标1:均衡班次人数
X_1 = \sum_{j=1}^{m} a_j^{(1)}
其中辅助变量 a_j^{(1)} \in [0,+\infty) 满足:
a_j^{(1)} \ge M_j - \bar{M}, \quad a_j^{(1)} \ge \bar{M} - M_j, \quad \forall j
注:在组合优化问题中只能定义线性约束,不能直接使用绝对值。因此引入辅助变量
a_j^{(1)}并添加两个不等式约束,使得在优化过程中a_j^{(1)}自动收敛到|M_j - \bar{M}|。
目标2:每班技术部老人数量接近期望值
期望每班有 m_2 = 3 位技术部老人,定义缺口:
X_2 = \sum_{j=1}^{m} a_j^{(2)}
其中辅助变量 a_j^{(2)} \in [0,+\infty) 满足:
a_j^{(2)} \ge \sum_{i=1}^{n} x_{ij} \cdot \text{old}_i \cdot \text{tech}_i - m_2
a_j^{(2)} \ge m_2 - \sum_{i=1}^{n} x_{ij} \cdot \text{old}_i \cdot \text{tech}_i, \quad \forall j
目标3:每班技术部小朋友数量接近期望值
期望每班有 m_3 = 4 位技术部小朋友,定义缺口:
X_3 = \sum_{j=1}^{m} a_j^{(3)}
其中辅助变量 a_j^{(3)} \in [0,+\infty) 满足:
a_j^{(3)} \ge \sum_{i=1}^{n} x_{ij} \cdot (1-\text{old}_i) \cdot \text{tech}_i - m_3
a_j^{(3)} \ge m_3 - \sum_{i=1}^{n} x_{ij} \cdot (1-\text{old}_i) \cdot \text{tech}_i, \quad \forall j
目标4:每班人资部小朋友数量接近期望值
期望每班有 m_4 = 1 位人资部小朋友,定义缺口:
X_4 = \sum_{j=1}^{m} a_j^{(4)}
其中辅助变量 a_j^{(4)} \in [0,+\infty) 满足:
a_j^{(4)} \ge \sum_{i=1}^{n} x_{ij} \cdot (1-\text{old}_i) \cdot \text{hr}_i - m_4
a_j^{(4)} \ge m_4 - \sum_{i=1}^{n} x_{ij} \cdot (1-\text{old}_i) \cdot \text{hr}_i, \quad \forall j
目标5:均衡各部门人数分布
对每个班次 j 和每个部门 k,定义该班次该部门的人数偏差:
X_5 = \sum_{j=1}^{m} \sum_{k=1}^{t} a_{jk}^{(5)}
其中 \bar{M}_j = \frac{M_j}{t} 表示第 j 班次各部门的平均人数,C_{jk} = \sum_{i=1}^{n} x_{ij} \cdot d_{ik} 表示第 j 班次第 k 部门的实际人数,辅助变量 a_{jk}^{(5)} \in [0,+\infty) 满足:
a_{jk}^{(5)} \ge C_{jk} - \bar{M}_j, \quad a_{jk}^{(5)} \ge \bar{M}_j - C_{jk}, \quad \forall j,k
约束条件
基本约束:
-
意愿班次约束:每位同学的值班次数必须符合其意愿
\sum_{j=1}^{m}x_{ij} = N_i, \quad \forall i \text{ where } N_i \neq 32 \le \sum_{j=1}^{m}x_{ij} \le 3, \quad \forall i \text{ where } N_i = 3 -
时间可行性约束:只在有空的时间段排班
x_{ij} \le v_{ij}, \quad \forall i,j
班次人数约束:
-
总人数约束:每班次人数在
[n_{\min}, n_{\max}]范围内n_{\min} \le \sum_{i=1}^{n} x_{ij} \le n_{\max}, \quad \forall j -
技术部老人约束:每班次技术部老人数在范围内
n_{\text{tech\_old\_min}} \le \sum_{i=1}^{n} x_{ij} \cdot \text{old}_i \cdot \text{tech}_i \le n_{\text{tech\_old\_max}}, \quad \forall j -
老人总数约束:每班次老人总数在范围内
n_{\text{old\_min}} \le \sum_{i=1}^{n} x_{ij} \cdot \text{old}_i \le n_{\text{old\_max}}, \quad \forall j -
小朋友总数约束:每班次小朋友总数在范围内
n_{\text{new\_min}} \le \sum_{i=1}^{n} x_{ij} \cdot (1-\text{old}_i) \le n_{\text{new\_max}}, \quad \forall j
求解器
以上完成了整个排班问题的建模。本项目使用 Google OR-Tools 中的 SCIP 求解器(Solving Constraint Integer Programs)进行求解。SCIP 是一个强大的混合整数规划(MIP)求解器,特别适合处理这类组合优化问题。
OR-Tools 支持 C++、Python、C#、Java 等多种语言,并提供跨平台支持。在本项目中,我们使用其 Python 接口 ortools.linear_solver.pywraplp 进行建模和求解。当找到最优解时,求解器会输出详细的目标值和各个优化指标的统计信息。
维护指南
- 如果你想更改 Excel 的读取、写入相关的功能,应该修改
utils.py中的相关函数。 - 如果你想更改软件的前端界面,应该修改
main.py中MyWidget这个类相关的代码。 - 如果你想更换排班问题的建模方式、更换求解器、增减限制条件,应该修改
solve.py中的相关代码。