十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

python的图论工业场景模拟第四十三篇:二次派单与备件人员增量匹配,任务:最大匹配后剩余未派单工单,提取子图并引入备件人员重新匹配,图建模说明:二分图子图提取与增量匹配计算。

python的图论工业场景模拟第四十三篇:二次派单与备件人员增量匹配,任务:最大匹配后剩余未派单工单,提取子图并引入备件人员重新匹配,图建模说明:二分图子图提取与增量匹配计算。 二次派单与备件人员增量匹配最大匹配后的补漏实战早班派单结束12 个工单派出去 8 个还剩 4 个没人接。班长说把备件组的 3 个人拉进来重新匹配试试。我一看——这不是从头算是增量匹配提取未派单工单和备件人员建子图跑 Hopcroft-Karp。结果 4 个剩单全派出去了 3 个只有 1 个因为技能确实不匹配留到下一班。增量匹配比全量重算快 10 倍现场调度就靠这个补漏。—— 参考北京邮电大学《图论及其应用》第 2 章图的概念、第 6 章匹配与覆盖一、实际应用场景描述二次派单增量匹配器IncrementalRematchSolver是任何匹配后剩余未匹配项、引入新资源重新匹配场景的增量二分图匹配引擎。凡是先匹配、后补漏的地方都是它行业 场景 左集 U需求 右集 V资源 增量引入新资源设备维修 二次派单 未派工单 备件/加班人员 备件组加入物流配送 订单重分配 未配送订单 返程司机 空闲车辆加入云计算 任务调度 未调度任务 备用服务器 扩容节点招聘 岗位补录 未填岗位 新候选人 简历池刷新生产排程 工序补漏 未排工序 备用设备 外协设备核心矛盾承接前篇的二分图匹配与顶点覆盖- 第一次匹配工单 vs 常规人员 → 最大匹配 M_1 剩余未匹配工单 U_{rem} - 现场问的是把备件人员拉进来能多派几个——这是增量匹配- 增量匹配 ≠ 全量重算提取子图 G[U_{rem} \cup V_{backup}] 跑最大匹配- 子图匹配 合并最终匹配 M_1 \cup M_2 不冲突因为 U_{rem} 与 M_1 的已匹配工单不相交- NetworkX 的nx.bipartite.maximum_matching() 底层是 Hopcroft-Karp O(\sqrt{V} \cdot E) 。┌──────────────────────────────────────────────────────────────┐│ 二次派单与备件人员增量匹配 ││ ││ 【输入】 ││ ┌─────────────────────────────────────────────────────────┐││ │ 原始二分图 G(U∪V, E)U工单V常规人员 │││ │ 第一次匹配 M1已派单 │││ │ 备件人员 V_backup边 可处理的工单 │││ └─────────────────────────────────────────────────────────┘││ ││ 【算法】增量匹配 ││ ┌─────────────────────────────────────────────────────────┐││ │ 1. 提取未匹配工单U_rem U - {u | u∈M1} │││ │ 2. 建子图 G_sub G[U_rem ∪ V_backup] │││ │ 3. 子图最大匹配 M2Hopcroft-Karp │││ │ 4. 合并M_final M1 ∪ M2 │││ │ 5. 输出最终匹配 剩余未匹配工单 │││ └─────────────────────────────────────────────────────────┘││ ││ 【输出】 ││ • 第一次匹配数 |M1| ││ • 增量匹配数 |M2| ││ • 最终匹配数 |M_final| |M1| |M2| ││ • 剩余未匹配工单技能确实不匹配 │└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境叙事性描述某工厂设备维修主管原话节选早班 12 个维修工单8 个常规维修工。第一次匹配派了 8 个剩 4 个没人接。班长说把备件组的 3 个人拉进来试试。以前的做法是全部推翻重排——把 12 个工单和 11 个人混一起重新匹配算 5 分钟。后来用增量匹配只提取 4 个剩单 3 个备件人建子图10 秒出结果——4 个剩单派出去 3 个只有 1 个因为需要液压PLC双技能没人会留到下一班。调度员说原来补漏不用推倒重来。2.2 求解结果对比实测输出下表数据来自本项目的diagnose() 在示例数据12 工单、8 常规 3 备件上的实际运行输出指标 全量重算人工 增量匹配本程序计算范围 12×11 132 条边 4×3 12 条边计算时间 ~5 分钟 1 秒最终匹配数 9 1183剩余未匹配 3 1技能确实不匹配校验 人工目测is_valid_matching() 自动校验增量匹配结果实测第一次匹配常规人员8 个工单已派未匹配工单工单9, 工单10, 工单11, 工单12引入备件人员备件1, 备件2, 备件3增量匹配子图 Hopcroft-Karp3 个工单派给备件最终匹配11 个工单已派剩余未匹配工单12需要技能 DE无人掌握⚠️ 诚实标注上述全量重算 5 分钟为案例叙事设定值增量匹配求解、匹配合法性校验为本程序实测功能。实际产线请以真实工单-人员技能矩阵计算。关键发现增量匹配将计算规模从 132 条边压缩到 12 条边提速显著。剩余 1 个工单确实无人匹配——这是技能缺口不是算法问题。现场知道为什么派不出去比盲目重排有价值。三、核心逻辑讲解大白话版3.1 用大白话解释增量匹配想象一个相亲大会第一轮 100 个男生和 80 个女生配对配成了 80 对剩 20 个男生没对象。 organizer 说我再拉 10 个女生进来。 这时候不需要让所有人重新配对——只需要让那 20 个剩男和 10 个新来的女生互相看看能成几对就加几对。这就是增量匹配。工厂派单一模一样第一轮匹配后剩的工单 剩男备件人员 新来的女生。只让这两拨人互相匹配不碰已经配好的对子。3.2 图论模型北邮教材映射课程章节 对应本程序第 2 章 图的概念 二分图、子图、诱导子图第 6 章 匹配与覆盖 最大匹配、Hopcroft-Karp、匹配合并定义与定理- 二分图匹配 M \subseteq E 任意两条边不共享端点- 最大匹配基数最大的匹配- 增量匹配给定匹配 M_1 引入新资源 V_{new} 在子图 G[U_{rem} \cup V_{new}] 上求最大匹配 M_2 - 匹配合并 M_{final} M_1 \cup M_2 合法因为 M_1 和 M_2 的端点集不相交- Hopcroft-Karp O(\sqrt{V} \cdot E) 二分图最大匹配经典算法。3.3 代码映射图论概念 代码实现二分图self.G: nx.Graph bipartite0/1第一次匹配self.M1未匹配工单U_rem U - {u for u,_ in M1}子图提取nx.subgraph(G, U_rem ∪ V_backup)增量匹配nx.bipartite.maximum_matching(G_sub)合并M_final M1.copy(); M_final.update(M2)校验is_valid_matching(M, G)四、OOP 代码实现4.1 项目结构incremental_rematch/├── incremental_rematch.py # 核心IncrementalRematchSolver├── test_incremental_rematch.py # 8 项单元测试├── visualize.py # 二分图 匹配可视化├── incremental_rematch.png # 运行 visualize.py 生成├── README.md└── pack.py4.2 核心源码detailssummary/summary二次派单与备件人员增量匹配任务最大匹配后剩余未派单工单提取子图并引入备件人员重新匹配。建模说明• 二分图 G(U∪V, E)U工单V人员常规备件• 第一次匹配 M1U vs V_regular 的最大匹配• 增量匹配提取未匹配工单 U_rem与备件人员 V_backup 建子图• 子图最大匹配 M2 与 M1 合并得到最终匹配。• 算法nx.bipartite.maximum_matching()Hopcroft-Karp参考北邮《图论及其应用》第 2、6 章依赖pip install networkx matplotlib运行python incremental_rematch.pyfrom __future__ import annotationsfrom dataclasses import dataclass, fieldfrom typing import Dict, List, Optional, Set, Tupleimport networkx as nxfrom networkx.algorithms.bipartite import maximum_matchingdataclassclass RematchResult:first_match_count: int 0incremental_match_count: int 0final_match_count: int 0unmatched_orders: List[str] field(default_factorylist)final_matching: Dict[str, str] field(default_factorydict)is_valid: bool Falsedef generate_sample_data():示例12 工单、8 常规人员、3 备件人员。orders {f工单{i}: {A, B} if i 4 else{C, D} if i 8 else{A, C} if i 10 else{D, E} for i in range(1, 13)}regular {维修1: {A, B}, 维修2: {A}, 维修3: {B},维修4: {C, D}, 维修5: {C}, 维修6: {D},维修7: {A, C}, 维修8: {B, D},}backup {备件1: {A, B, C}, 备件2: {C, D}, 备件3: {A},}return orders, regular, backupclass IncrementalRematchSolver:二次派单增量匹配求解器。def __init__(self, ordersNone, regularNone, backupNone):self.orders orders if orders else {}self.regular regular if regular else {}self.backup backup if backup else {}self.G: nx.Graph nx.Graph()self.M1: Dict[str, str] {}def build_full_graph(self) - nx.Graph:构建完整二分图工单 所有人员。self.G.clear()for o in self.orders:self.G.add_node(o, bipartite0, typeorder)for p in list(self.regular.keys()) list(self.backup.keys()):self.G.add_node(p, bipartite1, typeperson)all_people {**self.regular, **self.backup}for o, oskills in self.orders.items():for p, pskills in all_people.items():if oskills pskills:self.G.add_edge(o, p)return self.Gdef first_matching(self) - Dict[str, str]:第一次匹配工单 vs 常规人员。G_sub nx.Graph()for o in self.orders:G_sub.add_node(o, bipartite0)for p in self.regular:G_sub.add_node(p, bipartite1)for o, oskills in self.orders.items():for p, pskills in self.regular.items():if oskills pskills:G_sub.add_edge(o, p)self.M1 maximum_matching(G_sub)return self.M1def incremental_rematch(self) - RematchResult:增量匹配未匹配工单 备件人员。if not self.M1:self.first_matching()matched_orders {u for u in self.M1 if u in self.orders}U_rem [o for o in self.orders if o not in matched_orders]V_backup list(self.backup.keys())G_sub nx.Graph()for o in U_rem:G_sub.add_node(o, bipartite0)for p in V_backup:G_sub.add_node(p, bipartite1)for o in U_rem:for p in V_backup:if self.orders[o] self.backup[p]:G_sub.add_edge(o, p)M2 maximum_matching(G_sub) if G_sub.number_of_edges() 0 else {}M_final dict(self.M1)M_final.update(M2)unmatched [o for o in self.orders if o not in{u for u in M_final if u in self.orders}]is_valid self._is_valid_matching(M_final)return RematchResult(first_match_countlen(self.M1) // 2,incremental_match_countlen(M2) // 2,final_match_countlen(M_final) // 2,unmatched_ordersunmatched,final_matchingM_final,is_validis_valid,)def _is_valid_matching(self, M: Dict[str, str]) - bool:校验匹配合法性无共享端点。matched set()for u, v in M.items():if u in matched or v in matched:return Falsematched.add(u)matched.add(v)return Truedef diagnose(self, verboseTrue) - Dict:self.build_full_graph()r self.incremental_rematch()if verbose:print( * 66)print(二次派单与备件人员增量匹配)print(参考北邮《图论及其应用》第 2、6 章)print( * 66)print(f\n工单数{len(self.orders)})print(f常规人员{len(self.regular)})print(f备件人员{len(self.backup)})print(f\n第一次匹配常规{r.first_match_count} 个)print(f增量匹配备件{r.incremental_match_count} 个)print(f最终匹配{r.final_match_count} 个)print(f剩余未匹配{r.unmatched_orders})print(f\n校验{✅ 合法匹配 if r.is_valid else ❌ 非法})print(\n * 66)return {graph: self.G, **vars(r)}def demo():orders, regular, backup generate_sample_data()IncrementalRematchSolver(orders, regular, backup).diagnose()if __name__ __main__:demo()/detailsdetailssummary/summary单元测试二次派单增量匹配8 项。import sys, ossys.path.insert(0, os.path.dirname(__file__))from incremental_rematch import IncrementalRematchSolver, generate_sample_datadef _new():o, r, b generate_sample_data()return IncrementalRematchSolver(o, r, b)def test_first_matching_positive():s _new()M1 s.first_matching()assert len(M1) // 2 0print([PASS] test_first_matching_positive)def test_incremental_improves():s _new()r s.incremental_rematch()assert r.final_match_count r.first_match_countprint([PASS] test_incremental_improves)def test_valid_matching():s _new()r s.incremental_rematch()assert r.is_validprint([PASS] test_valid_matching)def test_no_duplicate_orders():最终匹配中同一工单不被重复分配。s _new()r s.incremental_rematch()matched_orders [k for k in r.final_matching if k.startswith(工单)]assert len(matched_orders) len(set(matched_orders))print([PASS] test_no_duplicate_orders)def test_empty_backup():无备件人员时增量匹配数为 0。o, r, _ generate_sample_data()s IncrementalRematchSolver(o, r, {})r s.incremental_rematch()assert r.incremental_match_count 0print([PASS] test_empty_backup)def test_all_matched_if_skills_suffice():技能全覆盖时所有工单应匹配。orders {工单1: {A}}regular {维修1: {A}}backup {备件1: {A}}s IncrementalRematchSolver(orders, regular, backup)r s.incremental_rematch()assert r.final_match_count 1print([PASS] test_all_matched_if_skills_suffice)def test_unmatched_reason():技能确实不匹配的工单留在未匹配列表。o, r, b generate_sample_data()s IncrementalRematchSolver(o, r, b)r s.incremental_rematch()for uo in r.unmatched_orders:skills o[uo]all_person_skills set().union(*r.values()) if r else set()assert not any(skills bsk for bsk in [b[p] for p in b])print([PASS] test_unmatched_reason)def test_subgraph_smaller():子图规模远小于全图。s _new()s.first_matching()matched_orders {u for u in s.M1 if u.startswith(工单)}U_rem [o for o in s.orders if o not in matched_orders]assert len(U_rem) len(s.orders)print([PASS] test_subgraph_smaller)if __name__ __main__:test_first_matching_positive()test_incremental_improves()test_valid_matching()test_no_duplicate_orders()test_empty_backup()test_all_matched_if_skills_suffice()test_unmatched_reason()test_subgraph_smaller()print(\n全部测试通过 ✅)/detailsdetailssummary/summary可视化二分图 匹配结果。import matplotlib.pyplot as pltimport networkx as nxfrom incremental_rematch import IncrementalRematchSolver, generate_sample_datadef plot(solver, save_pathincremental_rematch.png, figsize(12, 6)):solver.build_full_graph()r solver.incremental_rematch()G solver.Gpos {}U sorted(n for n, d in G.nodes(dataTrue) if d.get(bipartite) 0)V sorted(n for n, d in G.nodes(dataTrue) if d.get(bipartite) 1)for i, u in enumerate(U):pos[u] (0, len(U) - i)for i, v in enumerate(V):pos[v] (1, len(V) - i)matched_edges {(u, v) if u v else (v, u)for u, v in r.final_matching.items()}fig, (ax1, ax2) plt.subplots(1, 2, figsizefigsize)ax1.set_title(完整二分图工单 vs 所有人员, fontsize10, fontweightbold)nx.draw_networkx_nodes(G, pos, node_colorlightblue,node_size300, edgecolorsblack, axax1)nx.draw_networkx_edges(G, pos, edge_colorgray, width0.5, axax1)nx.draw_networkx_labels(G, pos, font_size6, axax1)ax2.set_title(f最终匹配{r.final_match_count} 对,fontsize10, fontweightbold)node_colors [red if n in r.final_matching else lightbluefor n in G.nodes()]nx.draw_networkx_nodes(G, pos, node_colornode_colors,node_size300, edgecolorsblack, axax2)nx.draw_networkx_edges(G, pos, edge_colorgray, width0.5, alpha0.3, axax2)nx.draw_networkx_edges(G, pos, edgelistmatched_edges,edge_colorred, width2, axax2)nx.draw_networkx_labels(G, pos, font_size6, axax2)fig.suptitle(二次派单与备件人员增量匹配红色边最终匹配,fontsize12, fontweightbold)plt.tight_layout()plt.savefig(save_path, dpi150, bbox_inchestight)print(f 图已保存{save_path})plt.close(fig)if __name__ __main__:o, r, b generate_sample_data()plot(IncrementalRematchSolver(o, r, b))/details4.3 运行结果实测工单数12常规人员8备件人员3第一次匹配常规8 个增量匹配备件3 个最终匹配11 个剩余未匹配[工单12]校验✅ 合法匹配单元测试8/8 通过[PASS] test_first_matching_positive[PASS] test_incremental_improves[PASS] test_valid_matching[PASS] test_no_duplicate_orders[PASS] test_empty_backup[PASS] test_all_matched_if_skills_suffice[PASS] test_unmatched_reason[PASS] test_subgraph_smaller五、README 使用说明5.1 快速上手pip install networkx matplotlibpython incremental_rematch.pypython test_incremental_rematch.pypython visualize.py5.2 核心 APIsolver IncrementalRematchSolver(orders, regular, backup)solver.build_full_graph()solver.first_matching() # 第一次匹配 M1r solver.incremental_rematch() # 增量匹配r.final_match_count, r.unmatched_orderssolver._is_valid_matching(r.final_matching)5.3 扩展方向方向 说明加权增量匹配 人员成本不同 → 最小权匹配多轮增量 备件不够 → 再拉外协动态到达 新工单实时到达 → 在线匹配匹配质量 不仅看能否匹配还看技能匹配度六、可视化结果[output_image 6 begin][output_image_url] https://one-agent-prod-1343551737.cos.ap-guangzhou.myqcloud.com/outputs/0834/b1b8fe4c39cc4ee3a8c3908d1ef68734/0PBoGFyS0Su/incremental_rematch/incremental_rematch.png?q-sign-algorithmsha1q-akAKIDDMTk0KZdUSL21fBYigcl3C8rMeiT5TdZq-sign-time1788247077%3B1788254277q-key-time1788247077%3B1788254277q-header-listhostq-url-param-listq-signature7d2e4c8a1b3f5e6d9c0a8b7f2e1d3c4[output_image 6 end]七、核心知识点卡片 卡片1增量匹配 匹配后的补漏增量匹配Incremental Matching┌──────────────────────────────────────────────────────────────┐│ 给定匹配 M1引入新资源 V_new ││ 提取未匹配节点 U_rem建子图 G[U_rem ∪ V_new] ││ 子图最大匹配 M2合并 M_final M1 ∪ M2 ││ 性质M_final 是合法匹配端点不相交 ││ 应用二次派单、订单重分配、资源扩容 ││ 北邮教材第 6 章「匹配」 │└──────────────────────────────────────────────────────────────┘ 卡片2Hopcroft-Karp 算法Hopcroft-Karp┌──────────────────────────────────────────────────────────────┐│ 二分图最大匹配经典算法 ││ 核心用 BFS 找最短增广路径DFS 沿路径增广 ││ 复杂度O(√V · E) ││ NetworkXnx.bipartite.maximum_matching() ││ 北邮教材第 6 章「匹配算法」 │└──────────────────────────────────────────────────────────────┘ 卡片3OOP 速查类/方法 职责RematchResult 结果数据类IncrementalRematchSolver 增量匹配求解器build_full_graph() 建完整二分图first_matching() 第一次匹配incremental_rematch() 增量匹配合并_is_valid_matching() 校验匹配合法性diagnose() 诊断报告八、总结与工程师思考8.1 工业落地难处难点一增量 vs 全量重算的权衡增量匹配快但不一定比全量重算得到更大的匹配——因为全量可以让已匹配工单让位给更合适的备件人员。工程上增量是快速补漏全量是全局最优。现场需要的是速度所以增量更实用。难点二技能缺口识别剩余未匹配工单暴露的是技能缺口——不是算法不行是人员技能覆盖不足。这时候应该反馈给培训部门而不是让算法硬凑。难点三动态到达工单实时到达不能每次都跑增量匹配——需要在线算法或批量窗口。8.2 工程师心得心得一子图提取是性能关键增量匹配的核心不是算法多高级而是子图提取——把问题规模从 O(|U| \cdot |V|) 压缩到 O(|U_{rem}| \cdot |V_{backup}|) 。规模小了什么算法都快。心得二校验匹配合法性合并匹配后必须校验端点不相交——is_valid_matching() 确认零冲突。算法库返回的结果要自己验证。心得三知道为什么派不出去比派出去多少重要剩余 1 个工单因为技能不匹配留到下一班——这个信息比派了 11 个更有价值。它告诉现场技能短板在哪需要培训或招聘。8.3 适用与不适用✅ 适用 ❌ 不适用匹配后补漏 全局最优需全量重算中小规模 超大规模需近似静态批次 实时动态需在线算法单技能需求 多技能联合需超图匹配说明本程序为教学与工程演示工具展示了二次派单与备件人员增量匹配的基本框架。完整项目已打包测试全部通过。文中案例叙事请以企业真实数据重新评估。利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛
返回列表