用整数规划与运筹学优化护士排班:Python+PuLP实战解析
2026/9/18 4:28:57 网站建设 项目流程

简介:一份以长征医院护士值班计划为背景的运筹学建模报告,面向运筹学课程学习者、医院管理者及排班优化从业者。报告围绕护士值班人数最少化目标,提出连续上班、周末轮休、部分护士放弃周末休息等三个方案,并分别建立线性规划模型求解,对比方案差异与经济性,可帮助读者掌握用整数规划处理排班类实际问题的方法。资源共1个doc文件,约293KB,内容包含问题提出、符号假设、模型建立、最优解与排班表,结构完整,适合作为课程设计或医院排班问题参考。已有320人学习,说明该案例在同类资源中具有较高参考价值。通过研读报告,可直观理解多班次约束下如何设置决策变量与约束条件,以及如何借助求解结果进行人员成本分析,是一份理论与实践结合的典型运筹学应用案例。

1. 护士排班为什么值得用运筹学重做一遍

排班这件事,绝大多数医院科室还在用 Excel 手工拼。15 个护士、30 天、白班夜班交替,护士长每个月底要花一整天做这件事,做完还要被护士们围着改三遍。表面看是“排一下谁哪天上班”的小事,实际是一个典型的 0-1 整数规划问题:每个护士每天只能上一个班次,每天每个班次的人数必须覆盖最低需求,夜班不能连续排太多天,还得尽量让每个人的班次数量公平。约束一多,手工排出来的表就经常在某一天发现夜班缺人,或者某个护士连续上了 5 天夜班。与其反复试错,不如把“排班规则”转成“数学约束”,用运筹学求解器一次性算出满足全部硬性要求、且尽量公平的方案。这篇内容面向正在做运筹学课程报告、毕业设计,或者在医院信息科想替护士长解决排班痛点的工程师,看完可以直接照着建模、写代码、跑出结果。

2. 把护士值班问题翻译成整数规划模型

2.1 先定义清楚“排班表”的数学形状

任何排班问题,第一步不是写代码,而是定义决策变量。护士排班里最常见的做法是把**“第 i 个护士在第 t 天被安排到第 k 个班次”**定义为一个 0-1 变量,记作x[i][t][k]。取 1 表示安排,取 0 表示不安排。班次集合至少要包含“白班 D、夜班 N、休息 R”三类;如果医院有小夜、大夜的区分,就扩展成四类或五类,模型结构不需要变。

用 0-1 变量而不是直接用“班次名称字符串”,是因为后续所有约束都必须写成线性等式或不等式。比如“每人每天最多一个班次”可以写成对每个i, tsum(k) x[i][t][k] <= 1。这是整数规划的标准写法,求解器(如 CBC、HiGHS 或 Gurobi)能高效处理这类变量。

实际建模时,我习惯用 Python 的元组索引而不是三维数组,比如(i, t, k)作为 key 存到一个字典里。这样做的好处是生成约束时可以很方便地按 “某一天”“某个人”“某个班次”三种维度去遍历,排错时也容易打印出问题变量所在的护士和日期。

2.2 硬约束:哪些规则是绝对不可以违反的

护士排班的硬约束通常有四类,翻译成数学式就是下面这张表:

约束名称数学表达(以变量x[i][t][k]表示)对应的业务含义
每日班次覆盖sum(i) x[i][t][D] >= 需求D[t]白班的人数必须满足当天患者护理需求
单人单日单班sum(k) x[i][t][k] <= 1不允许出现“白班连夜班”这种状态
夜班连续上限sum(k in 夜班) x[i][t][k] + x[i][t+1][N] + x[i][t+2][N] + x[i][t+3][N] <= 3连续夜班不能超过 3 天
夜班后强制休息x[i][t][N] + sum(k != R) x[i][t+1][k] <= 1上了夜班,下一天必须休息或只能排休息

这四个约束里,最容易踩坑的是“夜班后强制休息”。如果写成“x[i][t][N] + x[i][t+1][N] <= 1”意思是“不能连续上夜班”,但并没有禁止“夜班后接着上白班”——这在护理安全上是不可接受的。正确写法要把下一天所有工作班次(默认k != R)都加进来,只要前一天是夜班,下一天的全部工作变量都归零。

除了这些,常见的还有“每人每周工作时间不超过 40 小时”“每人每月夜班总数不超过 8 个”等,这些属于配额约束,写成对整周或整月的求和不等式即可。加入配额约束后模型会明显变紧,求解时间也会从几秒上升到几十秒,属正常现象。

2.3 为什么必须用 0-1 整数规划,而不是“线性规划+四舍五入”

如果只用普通线性规划,x[i][t][k]会被允许取 0.6、0.8 这类小数。你可以最后四舍五入凑成整数,但凑完大概率会破坏某些约束:某天白班人数从 7.3 人变成 7 人,恰好低于最低需求;或者某个人连续夜班的总和从 2.6 天变成 3 天,刚好触到上限。整数规划求解器是在“最优解”和“所有约束都被精确满足”之间同时做搜索,不存在四舍五入带来的二次违规。

注意,这里说的整数规划特指线性整数规划(MILP),不是非线性规划。所有约束和目标函数都要写成线性的“变量乘以系数再求和”的形式。比如“惩罚某个护士多上夜班”可以写目标函数里加入惩罚系数 * x[i][t][N]的求和,这仍然是线性的。但如果想表达“夜班分配越均匀越好”这种方差类目标,直接写方差就是二次函数,求解器会更慢甚至不收敛。常见做法是把它拆成“每人夜班数不超过平均值±2”的两个线性约束,或者用绝对值偏差的累加代替方差。

3. 用 Python + PuLP 跑一个最小可用的护士排班模型

3.1 最小复现:15 名护士、7 天、三班次

下面这段代码是我常用的最小模型骨架。它处理 15 名护士、7 天(周一到周日),每天需要的白班人数为 5、夜班人数为 2,连续夜班不超过 2 天,夜班后必须休息一天。安装依赖只需要pip install pulp

import pulp NURSES = list(range(15)) # 护士编号 0~14 DAYS = list(range(7)) # 0=周一 ... 6=周日 SHIFTS = ["D", "N", "R"] # 白班、夜班、休息 need = {"D": [5, 5, 5, 5, 5, 4, 4], # 各天白班最低人数 "N": [2, 2, 2, 2, 2, 3, 3]} # 各天夜班最低人数 MAX_NIGHT_IN_ROW = 2 # 连续夜班上限 # 0-1 变量:x[(i, t, k)] = 1 表示护士 i 在第 t 天排班次 k x = {} for i in NURSES: for t in DAYS: for k in SHIFTS: x[(i, t, k)] = pulp.LpVariable( f"x_{i}_{t}_{k}", cat=pulp.LpBinary) prob = pulp.LpProblem("NurseScheduling", pulp.LpMinimize) prob += 0, "仅求可行解,不设目标权重" # 约束 1:每人每天只上一个班次 for i in NURSES: for t in DAYS: prob += pulp.lpSum(x[(i, t, k)] for k in SHIFTS) == 1 # 约束 2:各天白班/夜班人数不低于需求 for t in DAYS: for k in ["D", "N"]: prob += pulp.lpSum(x[(i, t, k)] for i in NURSES) >= need[k][t] # 约束 3:连续夜班不超过 2 天 for i in NURSES: for t in range(len(DAYS) - MAX_NIGHT_IN_ROW): prob += pulp.lpSum(x[(i, t + d, "N")] for d in range(MAX_NIGHT_IN_ROW + 1)) <= MAX_NIGHT_IN_ROW # 约束 4:夜班后的第二天必须休息 for i in NURSES: for t in range(len(DAYS) - 1): # 前一天是夜班,则第二天只能是 R prob += x[(i, t, "N")] + pulp.lpSum( x[(i, t + 1, k)] for k in ["D", "N"]) <= 1 status = prob.solve() print("求解状态:", pulp.LpStatus[status]) # 输出排班矩阵 for i in NURSES: row = [] for t in DAYS: for k in SHIFTS: if pulp.value(x[(i, t, k)]) == 1: row.append(k) print(f"护士{i:02d}:", " ".join(row))

3.2 逐段解释这段代码的建模思路

变量定义部分用字典x存全部 0-1 变量,key 是(i, t, k)三元元组。这种写法比三维数组更贴近“用约束索引变量”的习惯:后面写约束时按护士遍历、按天遍历、按班次遍历都很直接。变量名f"x_{i}_{t}_{k}"是给求解器看的,一旦出现无解或约束冲突,求解器报错的变量名能直接对应到 “哪个护士哪一天哪个班次”,排错效率高很多。

约束 1 用== 1而不是<= 1,这是一个容易忽略的细节。“至多一个班次”允许某天全休,但对于每天在岗护士数量已经固定、且不考虑请假的情况,强制“必须等于 1”会让模型产生的解没有缺口,也方便后续计算每个人的周工时总和。如果改成<= 1,求解器可能让人人都休息,把需求全部压到少数人身上,虽然仍然满足覆盖要求,但这种解没什么实际意义,所以这里我采用穷尽式排班。

约束 3 写法是滑动窗口求和。for d in range(MAX_NIGHT_IN_ROW + 1)表示连续取 3 天的夜班变量求和,要求不大于 2。窗口在时间轴上每次滑动一天,就把“任意连续 3 天”这个条件全部覆盖了。如果要把上限改成 3,只需要把MAX_NIGHT_IN_ROW改为 3,窗口就自动变成 4 天。

目标函数这里用的是0(常量),实际上等价于“只求可行解”。碰到一天白班需要 8 人、夜班需要 3 人的场景,可行解可能不存在,这时求解器会返回 Infeasible;而加一个虚拟目标(比如最小化总夜班数)之后,即使可行域为空,也能通过松弛分析定位到具体是哪一类约束出了问题。更稳妥的做法是加一个很小的目标函数,下面这个案例就会涉及。

3.3 模型参数对照表与常见调整值

参数名代码中的写法建议取值范围调整时看什么
连续夜班上限MAX_NIGHT_IN_ROW2~3 天看护理部规定,超过 3 天基本会被护士投诉
白班覆盖人数need["D"][t]按科室内患者人数算,通常为在岗人数的 1/3 到 1/2看历史排班表里的在岗人数中位数
夜班覆盖人数need["N"][t]通常为 1~3 人,且周末可能比工作日多看是不是有 ICU、急诊等特殊单元
求解时间上限prob.solve(pulp.PULP_CBC_CMD(maxSeconds=60))30~120 秒超过 60 秒说明模型规模偏大或约束过紧

这些参数里最值得留意的是夜班覆盖人数与连续夜班上限的组合。假设科室只有 5 个护士,每晚却需要 3 个人上夜班,再叠加“连续夜班不超过 2 天”“夜班后休息一天”两条约束后,可行域几乎不存在。跑不出来先看有没有救,列出每种班次的总需求,再算算每人每月最多能产出的夜班数,很快就能判断是数据问题还是模型问题。

3.4 无解时先检查这 4 个位置

第一,检查约束是否互相矛盾。最常见的是“夜班后强制休息”和“人手不足”打架:夜班需求高、总人数少,强制休息会让部分护士整天处于不可用状态,排不满需求。

第二,检查变量类型。如果LpBinary写成了LpInteger且没加取值范围 0 到 1,求解器可能输出负数或大于 1 的解,这在逻辑上毫无意义,但求解器不会报错。写cat=pulp.LpBinary是最稳妥的。

第三,检查lpSum的索引范围。滑动窗口range(len(DAYS) - 3)在最后两天会少约束,但如果护士人数多、需求低,少约束不会暴露问题;一旦需求升高到接近能力边界,缺失的约束会导致最后两天夜班超标。我建议把窗口约束的范围写成range(len(DAYS) - MAX_NIGHT_IN_ROW),确保所有滑窗都被覆盖。

第四,把每个班次的实际覆盖人数打印出来比对需求。我习惯在solve()之后加一段汇总代码,统计每天的 D/N 人数,一眼就能看出哪一天缺人。求解器说“最优解”,不代表每个班次都排满了,只代表约束全部满足;而覆盖约束用的是>=,如果恰好有护士请假或休息,缺人的现象会被隐藏住。

4. 把“公平”和“偏好”塞进目标函数

4.1 硬约束只保证“不会出事”,不保证“没人骂”

只跑上一章的最小模型,得到的是可行解,但大概率不公平。某个护士可能在 7 天里上了 4 个夜班,另一个护士一个夜班都没有。业务上说得通,但护士长拿着这种表格发出去会被投诉。解决办法是增加软约束,把“每个人夜班总数尽量接近”“周末班尽量轮换”写成带有惩罚系数的两部分目标函数。

这里我给出一个带公平性惩罚的扩展模型。在上一段代码的基础上做增量修改:

# 目标函数第一项:最小化夜班总数方差近似值 night_total = {} for i in NURSES: night_total[i] = pulp.lpSum(x[(i, t, "N")] for t in DAYS) # 平均夜班数(常数,可预先计算) avg_night = sum( pulp.value(night_total[i]) for i in NURSES ) / len(NURSES) if False else None # 更实用的做法:用每人的夜班数不偏离均值的绝对偏差来线性化 target = sum(need["N"]) / len(NURSES) # 全局夜班总需求 / 人数 dev = {} for i in NURSES: dev[i] = pulp.LpVariable(f"dev_{i}", lowBound=0) prob += night_total[i] - target <= dev[i] prob += target - night_total[i] <= dev[i] # 目标:总偏差最小化(等价于让夜班尽量均分) prob += pulp.lpSum(dev[i] for i in NURSES), "MinimizeNightDeviation"

4.2 偏好怎么变成惩罚系数

护士偏好可以拆成两种。一种是“硬偏好”,比如某护士那天有考试或家里有事,直接不能在岗,写成一个硬约束x[(i, t, k)] == 0。另一种是“软偏好”,比如“我不喜欢连续上白班”,如果把它当硬约束可能直接导致无解,更好的做法是把它放进目标函数作为惩罚项。

常见的偏好惩罚表如下:

偏好内容建模方式推荐惩罚系数
护士 i 希望第 t 天休息目标函数中加P * x[(i, t, "D")] + P * x[(i, t, "N")]P=10
护士 i 不希望和护士 j 同一天上夜班目标函数中加P * y_{ijt},并加辅助变量y表示两人同夜班的状态P=20
护士 i 希望周末连休两天周末两天全部为 D/N的状态加惩罚P=30

要特别注意惩罚系数的量级。如果硬约束本身没有目标函数,那么软惩罚之间的相对值很重要,但绝对值无所谓;如果硬约束附带成本(比如多雇人、加班费),惩罚系数要远远大于成本系数,否则求解器会被成本“带偏”,牺牲公平性去省那一点点钱。

4.3 公平性维度太多时怎么取舍

公平性不止夜班均分一个维度。还有周末班轮换、节假日值班次数、白班和夜班的总工时差异。如果全都写进目标函数,模型会变得难以解释,求解时间也可能从几秒膨胀到几分钟。我的经验是:一次只保留 2 到 3 个公平性目标,其他的做成硬约束。比如“每人每月周末值班不超过 2 次”直接写成<= 2硬约束,把“夜班均分”和“节假日均分”写进目标函数。因为硬约束不会产生权衡,求解器处理起来更稳定。

5. 从报告角度验证排班结果的 3 个快速检查

5.1 校验函数:逐条把约束再查一遍

求解器返回“Optimal”不代表排班表在业务层面没问题。求完解之后,我建议写一个独立的校验函数,把业务规则再逐条查一遍。下面这段代码可以直接放在solve()之后运行:

def validate(schedule, need, max_night_row=2): errors = [] for t in DAYS: d_nurses = [i for i in NURSES if schedule[(i, t, "D")] == 1] n_nurses = [i for i in NURSES if schedule[(i, t, "N")] == 1] if len(d_nurses) < need["D"][t]: errors.append(f"第{t}天白班缺人: 需求{need['D'][t]}实际{len(d_nurses)}") if len(n_nurses) < need["N"][t]: errors.append(f"第{t}天夜班缺人: 需求{need['N'][t]}实际{len(n_nurses)}") for i in NURSES: for t in range(len(DAYS) - max_night_row): if all(schedule[(i, t + d, "N")] == 1 for d in range(max_night_row + 1)): errors.append(f"护士{i}第{t}天起连续夜班超上限") for t in range(len(DAYS) - 1): if schedule[(i, t, "N")] == 1 and \ (schedule[(i, t+1, "D")] == 1 or schedule[(i, t+1, "N")] == 1): errors.append(f"护士{i}第{t}天夜班后未休息") return errors

这个函数和模型里的约束是重复的,但必须写。因为求解器可能因为数值精度丢掉个别约束,或者你的约束写错地方而“看起来正确”。独立的校验器能在报告里直接列出Errors: [],这份证明比求解器的状态输出更有说服力。

5.2 公平性指标用一屏数据说话

报告里只写“模型公平”不够,要给出可量化的数字。我一般统计每个护士的总班次数、夜班数、周末班数,然后求最大值与最小值的差。差值越小说明公平性越好,比如 15 个护士里,夜班最少 2 个、最多 4 个,差值为 2,这个数字放在报告里比任何描述都有力。

可以用 Excel 透视图或直接在代码里打印一个矩阵,行是护士编号,列是每天的状态,顺手把夜班用不同颜色标注。打印机票式的白底黑字报告时,可以在表格下方加一行“每人夜班总数”,让评审老师一眼看到指标。

5.3 一个可复用的调参技巧:先松后紧

模型跑不通或结果不理想时,我的调参顺序是:先把所有软约束去掉,只看硬约束能否找到可行解;找到后,把公平性惩罚系数从 1、10、50 三个档位各跑一遍,记录每一档的目标值和违反程度,再选一个“违反略多但明显更公平”的档位。这个“先松后紧”的流程,能让排班模型从“能跑”到“能用”,也是报告里最值得写的一段对比数据。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询