尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
用整数规划与运筹学优化护士排班:Python+PuLP实战解析
简介一份以长征医院护士值班计划为背景的运筹学建模报告面向运筹学课程学习者、医院管理者及排班优化从业者。报告围绕护士值班人数最少化目标提出连续上班、周末轮休、部分护士放弃周末休息等三个方案并分别建立线性规划模型求解对比方案差异与经济性可帮助读者掌握用整数规划处理排班类实际问题的方法。资源共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][t1][N] x[i][t2][N] x[i][t3][N] 3连续夜班不能超过 3 天夜班后强制休息x[i][t][N] sum(k ! R) x[i][t1][k] 1上了夜班下一天必须休息或只能排休息这四个约束里最容易踩坑的是“夜班后强制休息”。如果写成“x[i][t][N] x[i][t1][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( fx_{i}_{t}_{k}, catpulp.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)三元元组。这种写法比三维数组更贴近“用约束索引变量”的习惯后面写约束时按护士遍历、按天遍历、按班次遍历都很直接。变量名fx_{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(maxSeconds60))30~120 秒超过 60 秒说明模型规模偏大或约束过紧这些参数里最值得留意的是夜班覆盖人数与连续夜班上限的组合。假设科室只有 5 个护士每晚却需要 3 个人上夜班再叠加“连续夜班不超过 2 天”“夜班后休息一天”两条约束后可行域几乎不存在。跑不出来先看有没有救列出每种班次的总需求再算算每人每月最多能产出的夜班数很快就能判断是数据问题还是模型问题。3.4 无解时先检查这 4 个位置第一检查约束是否互相矛盾。最常见的是“夜班后强制休息”和“人手不足”打架夜班需求高、总人数少强制休息会让部分护士整天处于不可用状态排不满需求。第二检查变量类型。如果LpBinary写成了LpInteger且没加取值范围 0 到 1求解器可能输出负数或大于 1 的解这在逻辑上毫无意义但求解器不会报错。写catpulp.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(fdev_{i}, lowBound0) prob night_total[i] - target dev[i] prob target - night_total[i] dev[i] # 目标总偏差最小化等价于让夜班尽量均分 prob pulp.lpSum(dev[i] for i in NURSES), MinimizeNightDeviation4.2 偏好怎么变成惩罚系数护士偏好可以拆成两种。一种是“硬偏好”比如某护士那天有考试或家里有事直接不能在岗写成一个硬约束x[(i, t, k)] 0。另一种是“软偏好”比如“我不喜欢连续上白班”如果把它当硬约束可能直接导致无解更好的做法是把它放进目标函数作为惩罚项。常见的偏好惩罚表如下偏好内容建模方式推荐惩罚系数护士 i 希望第 t 天休息目标函数中加P * x[(i, t, D)] P * x[(i, t, N)]P10护士 i 不希望和护士 j 同一天上夜班目标函数中加P * y_{ijt}并加辅助变量y表示两人同夜班的状态P20护士 i 希望周末连休两天对周末两天全部为 D/N的状态加惩罚P30要特别注意惩罚系数的量级。如果硬约束本身没有目标函数那么软惩罚之间的相对值很重要但绝对值无所谓如果硬约束附带成本比如多雇人、加班费惩罚系数要远远大于成本系数否则求解器会被成本“带偏”牺牲公平性去省那一点点钱。4.3 公平性维度太多时怎么取舍公平性不止夜班均分一个维度。还有周末班轮换、节假日值班次数、白班和夜班的总工时差异。如果全都写进目标函数模型会变得难以解释求解时间也可能从几秒膨胀到几分钟。我的经验是一次只保留 2 到 3 个公平性目标其他的做成硬约束。比如“每人每月周末值班不超过 2 次”直接写成 2硬约束把“夜班均分”和“节假日均分”写进目标函数。因为硬约束不会产生权衡求解器处理起来更稳定。5. 从报告角度验证排班结果的 3 个快速检查5.1 校验函数逐条把约束再查一遍求解器返回“Optimal”不代表排班表在业务层面没问题。求完解之后我建议写一个独立的校验函数把业务规则再逐条查一遍。下面这段代码可以直接放在solve()之后运行def validate(schedule, need, max_night_row2): 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, t1, D)] 1 or schedule[(i, t1, N)] 1): errors.append(f护士{i}第{t}天夜班后未休息) return errors这个函数和模型里的约束是重复的但必须写。因为求解器可能因为数值精度丢掉个别约束或者你的约束写错地方而“看起来正确”。独立的校验器能在报告里直接列出Errors: []这份证明比求解器的状态输出更有说服力。5.2 公平性指标用一屏数据说话报告里只写“模型公平”不够要给出可量化的数字。我一般统计每个护士的总班次数、夜班数、周末班数然后求最大值与最小值的差。差值越小说明公平性越好比如 15 个护士里夜班最少 2 个、最多 4 个差值为 2这个数字放在报告里比任何描述都有力。可以用 Excel 透视图或直接在代码里打印一个矩阵行是护士编号列是每天的状态顺手把夜班用不同颜色标注。打印机票式的白底黑字报告时可以在表格下方加一行“每人夜班总数”让评审老师一眼看到指标。5.3 一个可复用的调参技巧先松后紧模型跑不通或结果不理想时我的调参顺序是先把所有软约束去掉只看硬约束能否找到可行解找到后把公平性惩罚系数从 1、10、50 三个档位各跑一遍记录每一档的目标值和违反程度再选一个“违反略多但明显更公平”的档位。这个“先松后紧”的流程能让排班模型从“能跑”到“能用”也是报告里最值得写的一段对比数据。本文还有配套的精品资源点击获取
RELATED

相关推荐

对话量子场论:当语言遇见量子物理,重新理解语义的诞生

对话量子场论:当语言遇见量子物理,重新理解语义的诞生

如果你也属于那种平时喜欢琢磨“词到底是怎么有意思的”的人,那迟早会遇到一个绕不过去的坎:你翻词典、查文献、问朋友,最后发现一个词的含义永远是“大概是这样,但又好像不完全是”。2014年我在整理语言哲学笔记时,偶…

📅 2026/9/18 4:29:26
用视觉识别鱼:MiroFish计算机视觉项目实战复盘

用视觉识别鱼:MiroFish计算机视觉项目实战复盘

MiroFish 这个名字是我临时起的,Miro 在西班牙语里是“看”的意思,后面接一个 Fish,合起来就是“用视觉去看鱼”。我拿它做了差不多两个月的计算机视觉项目:用普通摄像头识别鱼缸和养殖池里的鱼,自动告诉你画面里出现的…

📅 2026/9/18 4:29:26
从Code Review到开放评审:一套可自托管的Git协作实践

从Code Review到开放评审:一套可自托管的Git协作实践

在团队协作里摸爬滚打几年后,你会发现一个很扎心的现实:代码评审(Code Review)这件事,嘴上人人都说重要,落地上却常常变成“走个过场”。要么是评审人随手点个“Looks Good To Me”,要么是评审讨…

📅 2026/9/18 4:24:25
MORE NEWS

更多资讯

📰

Hugo 开发服务器配置指南:Server 配置下的响应头、重定向规则与 404 处理

Hugo 开发服务器配置指南:Server 配置下的响应头、重定向规则与 404 处理 【免费下载链接】hugo The world’s fastest framework for building websites. 项目地址: https://gitcode.com/gh_mirrors/hu/hugo server 是 Hugo 中一组仅作用于开发服务器&#…

📰

Astro vs Next.js:从零JS到群岛架构的性能实战评测

别急着给 Next.js 判死刑,先看看你手里拿的到底是什么“锤子”如果你是个天天跟 React、Vue 打交道的前端,最近大概率被 Astro 刷屏了。铺天盖地的“放弃 Next.js,拥抱 Astro”,“首屏零 JS”,“速度提升 100%”……看…

📰

Slang 语义检查阶段深度解析:从 AST 到类型完备 IR 前置状态

Slang 语义检查阶段深度解析:从 AST 到类型完备 IR 前置状态 【免费下载链接】slang Making it easier to work with shaders 项目地址: https://gitcode.com/GitHub_Trending/sl/slang 本篇技术指南聚焦 Slang 着色语言编译器前端流水线中的**语义检查&…

📰

oh-my-hermes:为React Native定制一键式Hermes引擎开发工具链

1. 为什么会有 oh-my-hermes:先聊清楚它到底要解决什么问题1.1 Hermes 引擎开发里那些隐藏的重复劳动先说 Hermes 是什么。如果你做过 React Native 开发,Hermes 这个词大概率不陌生——它是专门为移动端设计的 JavaScript 引擎,Meta 开源出来…

📰

oh-my-hermes:模块化终端配置方案,一条命令还原你的开发环境

先说明一下,我今天要聊的不是某个神秘的希腊神话人物,而是一套我最近在折腾的终端环境配置方案,项目名就叫“oh-my-hermes”。名字确实有点玩梗的意思,灵感来自那几个经典的“oh-my-”系列工具,但核心目的很实在&#…

📰

企业AI服务市场分层与五类服务商精准定位

1. 企业AI服务市场现状与需求分析当前企业AI应用市场呈现明显的分层化特征,不同规模、不同数字化基础的企业在AI转型过程中面临着截然不同的挑战。根据Gartner最新调研数据显示,超过78%的中小企业表示在AI落地时遭遇技术选型困难,而大型企业则…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬