资讯动态

整数线性规划求解人力资源排程:从MATLAB到SciPy的建模实战

发布时间:2026/9/18 13:01:09 来源:尧图企业网站定制
简介一份聚焦大型软件公司人力资源配置优化问题的数学建模文档面向数学建模竞赛参与者、运筹学与整数规划学习者以及需要做人力成本优化决策的管理人员。文档围绕两个典型目标展开一是在尽量减少辞退员工的前提下制定招聘与培训计划二是在尽量减少费用的前提下维持公司运营并分别建立带整数线性规划约束的数学模型。内容覆盖系统分析师、高级程序员、程序员三类员工的动态流动关系涉及培训、降等、招聘、临时工、跳槽辞退等多种调整策略同时给出单纯形法及MATLAB求解思路。压缩包内为单个doc文档大小约57KB下载后可直接阅读文档结构完整包含问题提出、问题分析、模型假设、符号说明、约束条件与求解过程能帮助读者系统掌握人力资源优化问题的建模流程与求解方法并通过问题一与问题二的对比直观理解辞退人数与费用之间的权衡。已有70人学习使用。1. 大型软件公司的人力资源配置为什么需要整数线性规划一份三年期的人力资源排程可以复杂到什么程度这份《人力资源问题的数学模型》给出的场景是程序员需求从 200 人逐年降到 0高级程序员从 150 人升到 250 人系统分析员从 100 人升到 200 人。需求的错位迫使公司同时动用招聘、培训、辞退、降等、额外招聘、临时工六种手段每条路径都有上限、成本和跳槽概率变量相互联动很难靠经验拍板。文档把问题拆成两个目标问题一尽量减少辞退职员问题二尽量减少公司费用。两个模型共享同一组人员流动约束只改目标函数结果就显示出辞退人数与费用之间的取舍多保留 39 个岗位三年多付出约 106.45 万元。建模采用整数线性规划思路用单纯形法在 MATLAB 中求解适合 HR 系统开发、容量规划、数学建模相关工程师阅读。2. 员工流动模型17 个决策变量与人员流动方程任何线性规划模型的第一步不是写代码而是确定“决策变量”是什么。这份文档的处理方式是把三类岗位和六种调整手段做组合剔除掉不存在或不被允许的操作最终得到 17 个变量。程序员、高级程序员、系统分析员分别对应下标 1、2、3招聘、培训、辞退、额外招聘、临时工、降等分别用不同字母表示其中降等再按“高级程序员降到程序员、系统分析员降到程序员、系统分析员降到高级程序员”拆成三个变量。17 个变量刚好覆盖公司所有可操作的人力调整路径。2.1 三类岗位、六种调整手段如何映射成决策变量变量映射是建模的地基。文档中把 a1、a2、a3 记为招聘人数b1、b2 记为培训人数c1、c2、c3 记为辞退人数d1、d2、d3 记为额外招聘人数e1、e2、e3 记为临时工人数f1、f2、f3 记为三种降等路径。用 x1 到 x17 统一编号后目标函数和约束都可以写成矩阵形式。变量文档符号含义上限x1a1招聘程序员≤ 50x2a2招聘高级程序员≤ 80x3a3招聘系统分析员≤ 50x4b1培训程序员升至高级程序员≤ 20x5b2培训高级程序员升至系统分析员≤ Cn/4x6c1辞退程序员≤ An - b1x7c2辞退高级程序员≤ Bn - b2x8c3辞退系统分析员≤ Cn变量文档符号含义上限x9d1额外招聘程序员与 x10、x11 合计 ≤ 15x10d2额外招聘高级程序员与 x9、x11 合计 ≤ 15x11d3额外招聘系统分析员与 x9、x10 合计 ≤ 15x12e1临时工程序员≤ 5x13e2临时工高级程序员≤ 5x14e3临时工系统分析员≤ 5x15f1高级程序员降等为程序员≤ Bn - b2 - c2x16f2系统分析员降等为程序员f2 f3 ≤ Cn - c3x17f3系统分析员降等为高级程序员f2 f3 ≤ Cn - c3这里有几个细节值得注意。培训只允许发生在程序员和高级程序员身上系统分析员没有培训变量降等允许跨级所以系统分析员可以直接降到程序员对应 x16。临时工在问题一中不进入目标函数因为临时工费用低且工作量只有正式员工一半但它不会改变年末在编人数需要在建模时单独处理。2.2 跳槽概率系数与三条人员流动方程人员流动方程是整个模型的核心它把“年末人数 年初留存 流入 - 流出”写成了线性等式。以程序员为例年末人数由四部分构成老程序员中未培训、未辞退的留存新招聘程序员中留下来的从高级程序员和系统分析员降等下来且没有离职的人。A(n1) (A(n) - b1 - c1) × (1 - 10%) a1 × (1 - 25%) f1 × 50% f2 × 50% d1 × (1 - 25%)把常数移到等号右边就得到文档中 Aeq 矩阵第一行的形式0.75×x1 - 0.9×x4 - 0.9×x6 0.5×x15 0.5×x16 0.75×x9 A(n1) - 0.9×A(n)系数含义要逐项说明0.9 是老程序员留存率等于 1 减去 10% 的老员工跳槽概率0.75 是新招聘程序员的留存率0.5 是降等后员工继续留下的概率文档假设降等会带来 50% 的自然离职。高级程序员和系统分析员同理。B(n1) (B(n) - b2 - c2) × (1 - 5%) a2 × (1 - 20%) b1 - f1 f3 × 50% d2 × (1 - 20%) C(n1) (C(n) - c3) × (1 - 5%) a3 × (1 - 10%) b2 - f2 - f3 d3 × (1 - 10%)对应 Aeq 的第二行和第三行。第二行里格外要注意 x4、x15、x17 的符号培训上来的 b1 要加到高工人数中所以 x4 系数为 1f1 是高工降为程序员要减去f3 是分析员降为高工按 50% 留存加入。第三行则是 b2 升入分析员f2 和 f3 从分析员流出。2.3 约束条件与 linprog 矩阵化写法除了三条人员流动等式还有一组上限约束决定可行域边界。招聘人数每类都有年度上限培训人数受制于岗位存量额外招聘总数不能超过 15 人临时工每类最多 5 人。文档还假设被培训的员工当年不再辞退、不降等所以辞退上限写成 An - b1、Bn - b2。下面这个 MATLAB 函数把三年的矩阵构造封装起来方便后面直接调用。n 取 0、1、2 分别表示第一年到第三年。function [A, B, Aeq, Beq, xM] build_hr_model(n) % 决策变量顺序x1..x17 招聘/培训/辞退/额外招聘/临时工/降等 A_hist [200 150 100; % 当前 100 140 100; % 第一年 50 200 150; % 第二年 0 250 200]; % 第三年 An A_hist(n1, :); Anext A_hist(n2, :); % 不等式约束 A*x B A [0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0; % 培训辞退程序员 An 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0; % 培训辞退高工 Bn 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0; % 培训程序员高工降等 An 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 -1; % -(x16x17) 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 1; % 培训高工分析员降等 Cn 0 0 0 0 0 0 0 0 -1 -1 -1 0 0 0 0 0 0; % 额外招聘人数 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0]; % 额外招聘总人数 15 B [An(1); An(2); An(1); 0; An(3); 0; 15]; % 等式约束人员流动方程 Aeq [0.75 0 0 -1 0 -1 0 0 0.75 0 0 0 0 0 0 0.5 0.5; 0 0.8 0 1 -1 0 -1 0 0 0.8 0 0 0 0 -1 0 0.5; 0 0 0.9 0 1 0 0 -1 0 0 0.9 0 0 0 0 0 -1]; Beq Anext - [0.9*An(1); 0.95*An(2); 0.95*An(3)]; % 变量上界 xM [50; 80; 50; 20; An(3)/4; Inf; Inf; An(3); ... Inf; Inf; Inf; 5; 5; 5; Inf; Inf; Inf]; end这段代码里有一个地方需要特别说明第 3 行不等式写的是“培训程序员 高级程序员降等 当年程序员数”而严格按文档约束应该是 x4 x6 x15 An。文档附录的矩阵省略了 x6这是简化写法在复现时沿用原文不会改变最优解方向但如果要提交正式报告建议补全为 x4 x6 x15 An。x5 的上界取当前系统分析员数量的四分之一也就是 An(3)/4这个值在三年里分别是 25、25、37.5。提示Beq 的构造逻辑是“目标年末人数减去按留存率计算后的上年人数”。如果右侧出现大的负数说明该岗位留存人数已经超过需求单纯形法会倾向于用辞退或降等来压平差额。3. 单纯形法求解两个目标从 MATLAB linprog 到结果解读目标函数决定优化方向。问题一的目标是辞退总人数最小问题二的目标是各项人力资源操作的总费用最小。两个问题共享同一套 A、B、Aeq、Beq纯粹靠目标向量 f 区分。这正是线性规划把策略问题变成参数问题的价值换一个经营目标只需要换一行业务代码。3.1 问题一最小化辞退总人数问题一的目标函数是辞退三类员工的人数之和min F c1 c2 c3 x6 x7 x8调用 build_hr_model 函数求解第一年并把临时工变量上限置为 0因为问题一中不采用临时工。[A, B, Aeq, Beq, xM] build_hr_model(0); f zeros(1, 17); f(6:8) 1; % 目标min x6x7x8 xm zeros(17, 1); xM1 xM; xM1(12:14) 0; % 问题一不使用临时工 [x, fval] linprog(f, A, B, Aeq, Beq, xm, xM1); x, fvallinprog 的返回变量 x 是 17 维决策向量fval 是最小辞退人数。第一年输出 60 人第二年 20 人第三年 25 人三年合计 105 人。主要决策变量如下。变量含义第一年第二年第三年x2招聘高级程序员43.0872.6279.96x3招聘系统分析员0.1640.7440.20x4培训程序员20.0020.0020.00x5培训高级程序员2.5511.2115.90x6辞退程序员60.0020.0025.00x10额外招聘高级程序员10.405.866.45x11额外招聘系统分析员2.567.916.02x15高级程序员降为程序员62.734.5713.23从结果看三年中高级程序员和系统分析员的辞退人数都是 0说明公司在“少辞退”目标下会优先保留高等级岗位把结构调整压力全部释放到程序员层级。3.2 问题二最小化总费用问题二的目标函数是把培训费、辞退费、额外招聘附加费和临时工工资全部折算成钱min M 4000*x4 5000*x5 2000*x6 5000*x7 5000*x8 15000*x9 20000*x10 30000*x11 5000*x12 4000*x13 4000*x14培训一名程序员到高级程序员花费 4000 元培训高级程序员到系统分析员花费 5000 元辞退程序员的补偿是 2000 元辞退高工和分析员各 5000 元额外招聘的附加费则明显更贵系统分析员达到 30000 元每人。这些系数来自文档也是决定两个方案走向差异的关键。f2 zeros(1, 17); f2(4) 4000; f2(5) 5000; % 培训费 f2(6) 2000; f2(7) 5000; f2(8) 5000; % 辞退费 f2(9) 15000; f2(10) 20000; f2(11) 30000; % 额外招聘附加费 f2(12) 5000; f2(13) 4000; f2(14) 4000; % 临时工工资 [A, B, Aeq, Beq, xM] build_hr_model(0); xm zeros(17, 1); [x2, fval2] linprog(f2, A, B, Aeq, Beq, xm, xM);变量含义第一年第二年第三年x2招聘高级程序员48.0480.0080.00x3招聘系统分析员5.5650.0050.00x4培训程序员0.0013.008.50x5培训高级程序员0.0010.0012.50x6辞退程序员80.0027.0036.50x15高级程序员降为程序员40.930.000.00费用当年总费用160000156000169500问题二三年总费用 485500 元辞退总人数 144 人。与问题一对比辞退增加 39 人费用减少 1064500 元。减少辞退的方案相当于为每个多留下的岗位支付约 2.7 万元这个数值对于公司决策层是很直观的边际成本参考。3.3 用 SciPy linprog 复现第一问如果团队不使用 MATLAB可以用 Python 的 scipy.optimize.linprog 得到相同结果。下面的代码复现了问题一第一年import numpy as np from scipy.optimize import linprog A_hist np.array([ [200, 150, 100], [100, 140, 100], [50, 200, 150], [0, 250, 200], ], dtypefloat) An A_hist[0] Anext A_hist[1] A_ub np.array([ [0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0], [0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0], [0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0], [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,-1], [0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,1], [0,0,0,0,0,0,0,0,-1,-1,-1,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], ], dtypefloat) b_ub np.array([An[0], An[1], An[0], 0, An[2], 0, 15]) A_eq np.array([ [0.75,0,0,-1,0,-1,0,0,0.75,0,0,0,0,0,0,0.5,0.5], [0,0.8,0,1,-1,0,-1,0,0,0.8,0,0,0,0,-1,0,0.5], [0,0,0.9,0,1,0,0,-1,0,0,0.9,0,0,0,0,0,-1], ], dtypefloat) b_eq Anext - np.array([0.9*An[0], 0.95*An[1], 0.95*An[2]]) c np.zeros(17) c[5:8] 1 # min x6x7x8 bounds [(0, 50), (0, 80), (0, 50), (0, 20), (0, An[2]/4), (0, None), (0, None), (0, An[2]), (0, None), (0, None), (0, None), (0, 5), (0, 5), (0, 5), (0, None), (0, None), (0, None)] res linprog(c, A_ubA_ub, b_ubb_ub, A_eqA_eq, b_eqb_eq, boundsbounds, methodhighs) print(res.x) # 决策变量 print(res.fun) # 最小辞退人数SciPy 的 highs 方法在 1.6.0 之后成为默认求解器和 MATLAB linprog 默认的 dual-simplex 内核在数值稳定性上差别不大。需要留意的是 bounds 里的第 5 个变量 x5 上限依赖 An(3)/4换成第二年时要同步更新。4. 结果解读为什么辞退集中在程序员而高级岗位零辞退求解完成并不是终点。把两个方案的明细放在一起能看出几条稳定规律辞退全部集中在程序员高级程序员和系统分析员全程零辞退培训程序员常年顶满上限降等数量在第一问中明显大于第二问。这些结果映射到 IT 人力管理上其实就是一个典型的岗位升级路径设计问题。4.1 岗位需求错位决定了调整方向从需求表可以直接看出结构矛盾类别当前第一年第二年第三年程序员200100500高级程序员150100200250系统分析员100100150200程序员需求三年减少 200 人高级程序员和系统分析员合计增加 200 人。理论上可以把程序员全部培训升级但培训有年度上限且老程序员每年还有 10% 的跳槽流失所以不可能无缝转换。问题一第一年辞退 60 名程序员正是培训上限、跳槽概率和需求压缩共同作用的结果问题二第一年辞退更多则是因为费用目标下辞退比培训更便宜。4.2 培训资源的上限与效果问题一中 x4 三年都是 20也就是培训程序员的配额被完全用满。原因很直接培训一名程序员的成本是 4000 元而额外招聘一名高级程序员的附加费就要 20000 元再加上培训还能解决程序员冗余问题把原本要辞退的人转成高岗人员一举两得。高级程序员的培训上限是当年系统分析员人数的四分之一第一年 25 人、第三年 37.5 人实际使用量逐年上升说明越到后期越依赖内部晋升来填补系统分析员缺口。问题二第一年 x4 为 0第二年 13、第三年 8.5整体低于问题一。原因是费用最小化下辞退一名程序员只要 2000 元而培训要 4000 元直接辞退反而便宜只有到了第二年、第三年高工需求大幅上升、额外招聘成本更高时培训才重新具备性价比。4.3 降等是“软裁员”而不是惩罚降等在这里不是管理惩罚而是一个刻意设计的离职杠杆。文档假设降等后员工有 50% 的概率主动离职这就把“辞退”转换成了“自然流失”。问题一第一年 x15 62.73意味着约 63 名高级程序员被降到程序员岗位按 50% 计算约 31 人会主动离开公司不用支付辞退费同时高工岗位的冗余也被消化。剩下的降等员工填充程序员岗位又缓解了程序员需求归零带来的裁员压力。问题二第一年 x15 40.93显著低于问题一后两年直接为 0。这是因为费用目标下公司更倾向于支付 2000 元辞退程序员而不是承受降等带来的管理摩擦和潜在纠纷。这个对比说明降等和辞退虽然在人数方程里都减少冗余但目标函数会根据自己的偏好选择成本最低的那条路径。4.4 额外招聘与临时工的边界条件额外招聘在文档中假设总人数不超过 15且费用远高于普通招聘额外招聘高工的附加费 20000 元系统分析员 30000 元。它只在正常招聘和培训都补不上缺口时出现比如问题一第一年 x10 10.40、x11 2.56都是为了填满高工和分析员的年末目标人数。临时工在问题一被显式排除在问题二中也没有进入最优解因为临时工不进入在编人数方程在费用参数下无法改善目标值。% 绘制两个方案下程序员辞退人数对比 figure; bar([60 20 25; 80 27 36.5]); legend({问题一最少辞退, 问题二最少费用}); set(gca, XTickLabel, {第一年, 第二年, 第三年}); ylabel(辞退程序员人数);5. 连续松弛解的可信度整数化、跳槽概率与 Lingo 验证5.1 linprog 给出小数人数怎么办文档标题写的是整数线性规划但 MATLAB 代码实际调用的是 linprog求解的是线性松弛后的连续问题。所以 x2 43.0782、x3 0.1605 这类结果会出现。在人员规模百人以上的场景中连续解的方向性结论足够可靠但落到招聘名额、培训名额这类整数决策时需要把变量声明为整数。MATLAB 中对应的是 intlinprogintcon 1:17; % 全部决策变量取整数 [x_int, fval_int] intlinprog(f, intcon, A, B, Aeq, Beq, xm, xM1);整数化后辞退总人数可能在 105 的基础上浮动几人的量级这是正常现象。如果偏差超过 10%就要回头检查 Aeq 矩阵的系数是否漏乘了留存率。5.2 跳槽概率和降等离职率的敏感性模型对跳槽概率非常敏感。把老程序员跳槽率从 10% 改成 5%第一年 Beq(1) 就从 -80 变成 100 - 0.95×200 -90意味着程序员留存更多、需求缺口更小辞退压力反而增大。降等离职率 50% 同样是假设值改成 30% 时 x15 会明显变大。实际使用时应以历史离职数据作为输入并对关键概率做区间敏感性分析而不是直接信任单点结果。5.3 用 Lingo 做整数规划复核文档本身也把 Lingo 列为改进方向。Lingo 建模比 MATLAB 矩阵更直观适合快速验证整数解与连续解之间的差异。核心是用 GIN 声明整数变量、BND 设置上下界MIN x6 x7 x8; BND(0, x1, 50); BND(0, x2, 80); ... BND(0, x17, 100); GIN(x1); GIN(x2); ... GIN(x17); END交叉验证的判据很简单整数解的目标值不会优于连续松弛解。如果整数化后辞退总人数落在 105 到 115 之间说明连续解给出的调度方向和人员规模判断仍然成立如果偏差过大优先检查人员流动方程中的留存系数再检查降等和培训上限是否正确进入矩阵。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价