ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

物流分拣中心排班问题:整数规划建模与Python求解

物流分拣中心排班问题:整数规划建模与Python求解 简介2026年辽宁省数学建模竞赛B题物流分拣中心排班问题全套参赛资料面向需要高效完成建模论文与可复现代码的参赛团队。压缩包共60个文件、约1.5MB按论文、代码、数据、图表、运行日志分类组织其中docx/doc为成品论文py及pyc为可运行代码csv/xlsx存放中间及最终数据png为可视化结果json/txt/md记录参数与说明能支撑从问题分析、模型求解到结果展示的整体流程。目前已有117人学习下载。核心价值包括特等奖标准格式论文可为摘要、问题重述、模型建立与求解、灵敏度分析等章节提供直接参考双版本解题代码带逐行中文注释内含数据清洗、模型训练与启发式寻优模块一键生成图表每小问的数据表与结果表可直接用于论文正文或附录思路解析则讲清题目每一问的数学本质适合想突破建模瓶颈的队长、编程小白和冲击特等奖的精英团队。1. 物流分拣中心排班问题为什么第一眼简单一建模就卡住物流分拣中心排班问题是这届辽宁数学建模竞赛 B 题的核心方向也是近几年数学建模赛题里最典型的“数据 运筹优化”混合题题干给的是分拣中心各时段的任务量、班次类型和人数限制要你做的是把“几点安排多少人、上什么班”这件事用数学语言讲清楚。这类题第一眼像 Excel 排班表真动手会发现峰值时段和凌晨时段互相打架班次一跨天就乱套纯靠人工填根本最优不了。整篇文章围绕这条主线展开怎么把题面翻译成整数规划怎么用 Python 跑通每小问的数据和代码以及结果图和参数表怎么组织成能直接放进论文附件的样子。适合正在备赛的队伍也适合想读代码自学排班建模的同学只要有 Python 基础就能跟下来。2. 把题面变成可求解模型变量、约束和目标函数怎么定2.1 先拆数据需求和班次是两张表不要混在一张B 题不管怎么包装底层数据基本逃不出两类。第一类是“时段需求”大多数情况下按小时给出比如 0 点到 1 点需要 2 个人8 点到 9 点需要 15 个人一天 24 个时段各有一个数。第二类是“班次定义”包含班次名称、开始时间、结束时间、单班成本和该班次可用人数上限。我习惯在建模前先把这两张表分开画出来而不是直接上手写公式。需求表长这样hour, demandhour 取 0 到 23。班次表长这样shift_id, start, end, cost, cap。很多新手会把“班次人数上限”和“该时段需求”混在一个 Excel 里后面写约束时就会把两个概念叠在一起导致模型跑出来要么人数爆炸要么 infeasible。分拣中心场景里需求数据有时给的是“件数”而不是“人数”。这时候需要先做一个换算每人每小时的拣货能力比如人均 60 件/小时那么某个时段的demand ceil(件数 / 60)。注意这个换算要在建模之前完成不要写进整数规划里当成变量否则模型变成非线性后面求解器直接罢工。数据结构上我一般用 Pandas 读两个 CSV 文件不做任何清洗直接进模型是不行的。常见坑是凌晨时段的空值题干 PDF 里 0 点到 4 点可能写的是“无数据”或者干脆没写读进来以后就是一个空行。此时要把缺失的 hour 补 0 或补实际值否则覆盖约束少了一段反而导致结果看着对、实际错。2.2 排班问题的整数规划建模目标函数与三条硬约束把排班问题写成整数规划是这一类题的标准做法也是每小问共用的一条主线。设班次集合为 (J)决策变量 (x_j) 表示第 (j) 个班次安排的人数它是一个非负整数。每个班次有单班成本 (c_j)、可用人数上限 (cap_j)。每个时段 (t \in {0,1,...,23}) 有需求 (d_t)。还需要一个参数 (a_{jt})表示班次 (j) 是否覆盖时段 (t)覆盖取 1不覆盖取 0。目标函数是所有班次的人数乘以成本再求和[ \min \sum_{j \in J} c_j x_j ]三条硬约束缺一不可。第一条是覆盖约束每个时段安排的人必须不少于该时段需求[ \sum_{j \in J} a_{jt} x_j \ge d_t, \quad \forall t ]第二条是人数上限约束每个班次不能超过现场可调配的人数[ 0 \le x_j \le cap_j, \quad \forall j ]第三条是整数约束[ x_j \in \mathbb{Z}_, \quad \forall j ]覆盖约束里我用的是“大于等于”而不是“等于”。原因是现场排班允许冗余多安排一两个备班人通常不算违规但少安排就会出事故。如果题干明确写“恰好满足需求”再把大于等于改成等于。大多数排班题都不会用等于因为等于约束会让模型非常脆稍微有一点数据误差就直接无解。对于分拣中心还有一类常见的隐性约束是“同一个人不能同时上两个班”。这个约束在模型里不需要额外写因为 (x_j) 表示班次人数一个人只会出现在一个班次里不存在交叉复用。但如果题干里允许“加班延班”或者“人员调班”就需要额外引入一个 0-1 变量来表示某个员工是否被安排在某个班次上模型规模会大不少一般只在扩展小问里出现。2.3 为什么这类题绕不开整数规划而不是手工拉表有人会问班次一共就几种为什么不能直接枚举以最常见的固定班次为例假设有 6 个班次每个班次人数上限 10那么每个班次的人数取值有 11 种总组合是 (11^6 1771561) 种。看起来还能枚举但如果第二问允许弹性排班班次集合从 6 个扩大到 24 个甚至更多组合数直接爆炸。手工 Excel 排班只能做到“局部看着合理”证明不了最优。整数规划的优势在于它把“人数分配”变成线性约束再由求解器去搜索。这个问题规模下用开源求解器 CBC 几十秒内就能拿到最优解。这是数学建模竞赛里最稳妥的一招模型简单、求解快、结果可解释。类似的排班骨架不仅在辽宁赛区能用华为杯研究生数学建模或者其他赛区的物流调度题也经常共用这套结构学过一次后面平移即可。3. 用 Python 实现排班优化数据和双代码怎么组织3.1 数据读入和班次表生成先把数据结构定下来写代码前先把目录结构想清楚。我一般会建一个data/放原始数据一个output/放结果图和结果表核心代码放在根目录两个脚本里一个跑求解一个做回代校验和画图。这样做的好处是每次跑不同小问只需要替换data/下的需求文件代码不用改。需求表的读取很简单但要注意编码import pandas as pd import pulp # 读取小时需求表假设列名为 hour 和 demand demand_df pd.read_csv(data/demand.csv, encodingutf-8-sig) print(demand_df.head()) # 检查 24 个小时是否齐全凌晨缺失是常见数据问题 missing_hours [h for h in range(24) if h not in set(demand_df[hour])] if missing_hours: print(缺失时段, missing_hours)这段代码的逻辑是先把数据读进来然后立刻检查 0 到 23 小时是否有缺失。encodingutf-8-sig是为了兼容 Excel 导出的带 BOM 头 CSV不加这个偶尔会读出乱码。打印缺失时段的行为很多人会跳过但排班题的数据几乎都是从题面 PDF 手敲进 Excel 的漏一行太常见了。接着生成班次表。这里以最常见的三班倒加一个夜班为例shifts pd.DataFrame([ {name: S1, start: 0, end: 8, cost: 120, cap: 10}, {name: S2, start: 4, end: 12, cost: 120, cap: 12}, {name: S3, start: 8, end: 16, cost: 120, cap: 12}, {name: S4, start: 12, end: 20, cost: 120, cap: 10}, {name: S5, start: 16, end: 24, cost: 120, cap: 10}, {name: S6, start: 20, end: 4, cost: 150, cap: 8}, ])参数说明start和end用整数表示小时S6 班次从 20 点开始、次日 4 点结束这个跨天班次是后面最容易写错的地方。cost是单班人均成本cap是该班次可调用的最大人数。参数不是随便写的需要回到题干确认。如果题干给的是“每人每班次成本 120 元”直接填如果给的是“每人每小时工资 15 元”就要用15 × 班次时长算出来填。3.2 第一套代码用 PuLP 求解最小成本排班求解代码的核心是把 2.2 节的约束逐条写出来。PuLP 是 Python 里最常用的整数规划建模库默认带 CBC 求解器没有额外安装依赖装好pulp就能跑。prob pulp.LpProblem(sorting_shift_scheduling, pulp.LpMinimize) # 决策变量每个班次安排的人数整数0 到 cap 之间 x { r[name]: pulp.LpVariable(r[name], 0, r[cap], catInteger) for _, r in shifts.iterrows() } # 目标函数总成本最小 prob pulp.lpSum(x[r[name]] * r[cost] for _, r in shifts.iterrows()) # 覆盖约束每个小时的人数必须满足需求 for t in range(24): demand_t demand_df.loc[demand_df[hour] t, demand].values[0] covering_shifts [ x[r[name]] for _, r in shifts.iterrows() if covers_hour(r[start], r[end], t) ] prob pulp.lpSum(covering_shifts) demand_t # 求解并输出结果 prob.solve() print(求解状态, pulp.LpStatus[prob.status]) print(最小总成本, pulp.value(prob.objective)) for name, var in x.items(): if var.value() and var.value() 0: print(name, 安排, int(var.value()), 人)其中covers_hour单独定义def covers_hour(start, end, t): if end start: return start t end # 跨天班次比如 20:00-04:00 return t start or t end这段代码里最关键的是covers_hour。普通班次结束时间大于开始时间直接比较区间跨天班次结束时间小于开始时间需要拆成“当天后半段 次日前半段”。如果你直接写start t endS6 班次在 23 点到凌晨 3 点这段就永远覆盖不上求解器要么给出畸形解要么直接 infeasible。求解状态是一个很容易被忽略的输出。Optimal表示拿到了最优解Infeasible说明约束之间冲突Unbounded说明漏了人数上限。看到 Infeasible 不要急着调参数先把约束打印出来检查。3.3 第二套代码把结果回代核对每个小时的覆盖人数求解器给出的结果是一组“每个班次安排多少人”。这个结果对不对不能只看目标函数小就完事必须把人数回代到每个小时重新算一遍覆盖。这一步我称它为“双代码里的第二套校验代码”也是整个方案里最不容易翻车的保障。import matplotlib.pyplot as plt # 按小时统计覆盖人数 cover_by_hour [0] * 24 for _, r in shifts.iterrows(): x_value x[r[name]].value() if x_value is None or x_value 0: continue for t in range(24): if covers_hour(r[start], r[end], t): cover_by_hour[t] int(x_value) # 与需求逐小时比对 demand_list [ demand_df.loc[demand_df[hour] t, demand].values[0] for t in range(24) ] for t in range(24): if cover_by_hour[t] demand_list[t]: print(f时段 {t}: 覆盖 {cover_by_hour[t]} 人需求 {demand_list[t]} 人缺口) # 生成项目结果图需求 vs 覆盖 plt.figure(figsize(10, 4)) plt.plot(range(24), demand_list, markero, label每小时需求) plt.bar(range(24), cover_by_hour, alpha0.5, label实际覆盖人数) plt.xlabel(小时) plt.ylabel(人数) plt.legend() plt.savefig(output/coverage_curve.png, dpi200) plt.show()这段代码的逻辑分三段。第一段把每个班次的人数按时间段展开累加得到每小时实际覆盖人数第二段逐小时比对需求和覆盖发现缺口立刻打印第三段把结果画成折线加柱状图保存到output/目录。alpha0.5是柱状图透明度目的是让折线和柱子叠在一起时都能看清。dpi200建议不要低于这个值论文插图太小会糊。这张图在论文里价值很高它直接证明“每个小时都覆盖住了”比一大堆公式更有说服力。4. 排班模型最容易踩的 4 个坑现象、原因与排查4.1 求解器报 infeasible先怀疑数据别怀疑模型现象prob.solve()跑完LpStatus显示 Infeasible没有任何排班结果输出。原因最常见的不是公式写错而是凌晨时段的数据缺失。题干 PDF 里凌晨 0 点到 3 点常常不显眼地空着手抄进 Excel 后这 4 个小时就没有对应行。模型对每个hour都要建立覆盖约束缺失时段如果被 Pandas 跳过看起来约束少了但如果其他地方把空值读成了 NaN比较时就会出错更隐蔽的是你手动补了一个小时列表range(24)然后用demand_df[demand_df[hour] t]去取数取到空值后约束变成 0看起来模型能跑实际上把那几个小时的需求漏掉了。解决在建模前先跑一遍缺失检查。如果缺失时段本来就是业务上的无人时段直接补 0如果是有数据的但漏了回到原始文件补全。用一句经验的话说排班题 90% 的 infeasible 是数据没洗干净不是模型有问题。4.2 班次跨天覆盖矩阵算错一半现象求解结果里S6 班次20:00 到次日 04:00被安排了不少人但你回代检查时发现凌晨 1 点到 3 点覆盖人数是 0。原因covers_hour函数里直接用start t end判断对于end start的跨天班次这个比较永远不可能成立。很多教程里的覆盖矩阵写法只适用于同一天内的班次分拣中心凌晨还有夜班跨天是标配。解决使用带跨天分支的判断逻辑。判断条件是如果end start正常区间判断如果end start表示跨天只要t start或者t end都算覆盖。写完这个函数后建议单独打印测试covers_hour(20, 4, 0)应该返回 Truecovers_hour(20, 4, 18)应该返回 False跑通测试再进模型。4.3 忘了整数约束回来一堆小数现象求解结果里出现“8.4 人”“12.7 人”目标函数值还很好看但论文里根本没法写。原因定义变量时把catInteger漏了或者用了默认的 Continuous。排班人数必须是整数这是题干里的硬性要求。有人觉得先用连续变量跑一遍、再四舍五入是一样的这个想法在这里不成立四舍五入可能让某个时段覆盖人数变成 8 人而需求是 9 人约束就破了。解决每个LpVariable都把cat参数写成Integer。注意 PuLP 对大小写不敏感但是建议统一小写Integer避免团队协作时有人写integer有人写INTEGER造成代码风格混乱。整数规划在这个问题规模下不会太慢不用为了性能牺牲正确性。4.4 需求给的是“包裹量”不是“人数”现象模型跑出来最优解要安排几百个人明显不符合常理。原因题干可能给出的是“每小时分拣件数”比如 8 点到 9 点有 900 件包裹而不是“需要几个人”。如果直接把 900 当需求人数代入人数会大得离谱。解决在数据预处理阶段完成件数到人数的换算。换算公式是need_t ceil(volume_t / per_capita_capacity)其中per_capita_capacity是每人每小时处理件数题干通常会给出一般取 50 到 80 之间。如果题干给的是“每人每班次处理 400 件”则需要先把每人每小时处理量算出来再按时段需求换算。这一步属于题面解读不写进求解代码但在论文里必须单独说明换算依据评阅老师很看重这个处理细节。5. 参数怎么调、结果图怎么摆从求解器输出到论文附件5.1 第一小问固定班次改班次集合不动模型骨架第一小问通常是最简单的班次类型固定比如只有早班、中班、晚班三种每种班次的开始时间、结束时间、成本、人数上限全部给定。此时不需要重新建模只需要把shifts表限定为题干给的那几类。我要强调一个容易走弯路的点很多人一上来就写一个复杂的“大模型”想兼容后面所有小问结果第一问就卡住。更稳妥的做法是第一问只做最小可用的模型把固定班次跑通拿到结果图和回代校验后再往第二问扩展班次集合。模型每次只改一处出问题也容易定位。参数上第一问最值得调的是人数上限cap。如果求出来某个班次正好顶格到cap说明这个班次是瓶颈班次论文里可以写一句“当前解受限于 S2 班次人数上限若实际可增派 x 人总成本可进一步降低 Y 元”。这不是花哨话而是敏感性分析的雏形评阅老师很买账。5.2 第二小问弹性排班把候选班次扩到全部时段第二小问常见的变体是允许班次从任意整点开始不再限定在某个固定时间。这时候的改法不是动约束而是扩大班次表。expanded_shifts [] for start in range(24): end start 8 if end 24: end - 24 # 跨天回绕 expanded_shifts.append({ name: fE{start:02d}, start: start, end: end, cost: 150, cap: 8, })这段代码生成了从任意小时开始的 24 个 8 小时班次覆盖了跨天情况。注意end start 8可能等于 32需要回绕到end - 24。这里不能用% 24直接取模后写成班次结束时间因为start22end(228)%246回绕逻辑一致但手动if end 24: end - 24更清晰方便打印检查。班次集合扩大后问题规模从 6 个变量变成 24 个变量CBC 求解时间通常不会有明显变化。但如果第三问允许每两个小时一个班次或者 6 小时班变量会更多求解超时的时候再考虑加“最少连续工时”之类的剪枝约束。5.3 增加场景对比敏感性分析让结果更可信很多队伍的论文止步于“给出一个最优解”但其实评阅老师更愿意看到“在不同需求水平下排班方案怎么变”。做法很简单把需求数据放大 80%、100%、120%每个场景跑一遍同一个模型记录总成本和关键班次人数做一张对比表。for ratio in [0.8, 1.0, 1.2]: temp_demand demand_df.copy() temp_demand[demand] (temp_demand[demand] * ratio 0.999).astype(int) # 传入同一求解函数记录结果 print(f需求比例 {ratio:.0%}最小成本 {run_model(temp_demand)})这里(demand * ratio 0.999).astype(int)是向上取整的人为写法因为ceil在 Pandas 里没有直接的向量化方法。也可以用np.ceil。这段代码本质上是把原来的求解过程封装成一个run_model(demand_df)函数参数只有需求数据返回目标值。三种场景跑出来后如果成本随需求线性上涨模型就是正常的如果某个场景突然 infeasible很可能说明当前班次集合在极端需求下覆盖不了这是一个值得写进论文的结论。5.4 结果图和表格的组织直接可放进论文附件结果图方面我固定会输出三张第一张是“需求与覆盖人数对比图”也就是第 3.3 节里那张折线加柱状图第二张是“各时段冗余人数图”画cover_by_hour[t] - demand_list[t]帮助看到哪些时段排多了第三张是班次人数占比饼图或者条形图。这三张图放到论文附件里已经能覆盖 90% 的评阅需求。结果表方面建议做成output/schedule_result.csv每行一个班次包含班次名、开始时间、结束时间、成本、安排人数。同时在论文正文给一个精简版的四列表班次、时段、人数、成本。表格放正文详细 CSV 放附录不要反过来。文件组织上我会把data/里每个小问单独建子目录比如data/question1_demand.csv、data/question2_demand.csv代码不变只改路径。这样整套项目文件从数据到结果图结构清晰也方便队友并行修改。6. 验证模型正确性的一个实操习惯把解回代进原约束最后说一个我从排班题踩坑里养成的习惯无论求解器给的结果多漂亮先别急着截图把每个班次的人数回代到原始的覆盖约束里重新算一遍每小时覆盖。这个动作看起来多余但它能拦住最蠢也最致命的错约束写错了但求解器认为你写对了。回代检查代码其实就是第 3.3 节那一小段但它值得单独当做一个标准动作。我会在求解之后立刻执行看到输出里没有“缺口”字样再去画图。宁可花十秒写回代也不要花一小时对着一个错误的图找原因。写排班模型的另一个教训是不要一开始就把模型做得太复杂。先固定班次、固定参数跑通一个最小可行解确认回代无误后再一步步放松条件。弹性班次、跨天、整数约束、敏感分析这些都是一层一层加上去的。每加一层重新回代一次这是成本最低的排错路径。还有一个小技巧求解完把结果存成 JSON 文件连同demand_df和shifts一起放进output/目录。这样不仅你自己能复盘队友也能基于同一份结果继续做后续小问不会出现“我这边跑出来 15 人你那边跑出来 16 人”的对不上账的情况。回代、存证、再画图这是我每次交排班题之前的固定顺序希望帮到你。本文还有配套的精品资源点击获取
返回列表