最小费用完美匹配:带花树算法从理论到C++工程落地

📅 发布时间:2026/10/13 15:50:04
最小费用完美匹配:带花树算法从理论到C++工程落地
简介这份资源是面向图论与组合优化方向学习者、算法工程师的C实现包聚焦一般图非二分图中的最大基数匹配与最小成本完美匹配问题核心采用Edmonds开花算法处理带权完美匹配适合具备一定算法基础、需要研究或复用匹配求解器的读者。包内共10个文件以4个cpp源文件与4个h头文件为主体分别承载图结构、二叉堆、匹配核心逻辑与调用示例另附LICENSE与README.md说明文档压缩包约12KB体量轻便。代码依据Gerards在《运筹学与管理科学手册》中的匹配章节实现Example.cpp给出API使用范例支持通过命令行参数切换最小权重完美匹配与最大基数匹配两种模式并定义了顶点数、边数的输入文件格式。目前已有345人学习下载读者可借此理解开花算法的工程落地方式掌握加权完美匹配的建模与求解思路并直接将其作为求解器嵌入自己的图论项目或课程实验中。1. 最小费用完美匹配从一般图到 C 落地的真实门槛带权一般图上的最小费用完美匹配是组合优化里少数几个“理论漂亮、工程难啃”的硬骨头。它要解决的问题很具体给一张无向图每条边带一个费用找一个覆盖所有顶点的匹配使总费用最小。注意是“一般图”不是二分图——这意味着不能直接套匈牙利算法或 KM必须处理奇环必须引入带花树Blossom那套收缩机制。很多做调度、任务分配、芯片布线、点云配准的工程师第一次听到“最小费用完美匹配”时会以为它就是二分图匹配加个权重结果写完发现样例过不了原因就出在奇环上。这篇文章面向的是需要把该算法真正写进 C 工程的人你要么在做一个多目标跟踪的帧间关联要么在做某种资源两两配对要么在复现某篇论文的 baseline。我会把 Edmonds 带花树与费用最小化的结合讲清楚给出可编译的骨架代码、关键参数、调试手段以及我踩过的坑。读完你应该能自己搭出一个能跑中等规模数据的版本并知道它在什么规模下会开始变慢。2. 为什么一般图必须用带花树从二分图匹配的思维定式里跳出来2.1 奇环为什么让增广路失效在二分图里增广路搜索是干净的从某个未匹配点出发交替走非匹配边、匹配边遇到另一个未匹配点就翻转。之所以干净是因为二分图没有奇环所有环长度都是偶数搜索树里不会出现“同一个点既被当作外点又被当作内点”的矛盾。一般图不一样。你从根出发做 BFS交替标记外点even和内点odd一旦遇到一条连接两个外点的边就形成了一个奇环。这个奇环如果不处理搜索树的状态就乱了环上的点既可能是外点也可能是内点增广路的存在性判断直接失效。Edmonds 的贡献就是给出“花”的概念把奇环收缩成一个超级点在收缩后的图上继续搜索找到增广路后再把花展开还原成原图上的增广路。这个收缩-展开的过程是带花树算法的核心也是所有实现里最容易写错的地方。2.2 费用最小化怎么叠加上去最小费用完美匹配不是“先找最大匹配再调费用”而是要在增广的每一步都选费用增量最小的增广路。常见做法是维护一套对偶变量dual variable也叫势potential把“找最小费用增广路”转化为“在 reduced cost 非负的图上找零费用增广路”。这套对偶框架和带花树的收缩机制必须同时维护收缩花的时候花内边的 reduced cost 要重新计算展开的时候对偶变量要回滚。这就是为什么很多开源实现里带花树和费用部分耦合得非常紧拆不开。我一般会建议如果你只是要一个能用的实现不要从零手写对偶更新先找一个结构清晰的参考实现把它的数据结构吃透再改。下面给的是一个简化但结构完整的骨架重点在展示数据组织和主循环不是生产级优化版。2.3 一个可编译的 C 骨架#include bits/stdc.h using namespace std; const int MAXN 405; const long long INF (long long)1e18; struct Edge { int u, v; long long cost; }; int n, m; vectorEdge edges; vectorint head[MAXN], nxt; vectorlong long cap; // 这里用费用流视角的残量简化演示 // 带花树核心状态 int match[MAXN], p[MAXN], base[MAXN], q[MAXN]; bool used[MAXN], blossom[MAXN]; int qh, qt; int lca(int a, int b) { static int vis[MAXN], t 0; t; while (true) { if (a ! -1) { a base[a]; if (vis[a] t) return a; vis[a] t; if (match[a] -1) a -1; else a p[match[a]]; } swap(a, b); } } void markPath(int v, int b, int child) { while (base[v] ! b) { blossom[base[v]] blossom[base[match[v]]] true; p[v] child; child match[v]; v p[match[v]]; } } int findPath(int root) { fill(used, used n, false); fill(p, p n, -1); for (int i 0; i n; i) base[i] i; qh qt 0; q[qt] root; used[root] true; while (qh qt) { int v q[qh]; for (int id : head[v]) { int to edges[id].v; if (base[v] base[to] || match[v] to) continue; if (to root || (match[to] ! -1 p[match[to]] ! -1)) { int curbase lca(v, to); fill(blossom, blossom n, false); markPath(v, curbase, to); markPath(to, curbase, v); for (int i 0; i n; i) if (blossom[base[i]]) { base[i] curbase; if (!used[i]) { used[i] true; q[qt] i; } } } else if (p[to] -1) { p[to] v; if (match[to] -1) return to; to match[to]; used[to] true; q[qt] to; } } } return -1; }这段代码只覆盖了无权带花树的增广搜索费用部分没有展开因为完整版的对偶更新会让篇幅失控。逻辑说明base数组维护每个点当前所属的花的编号lca找两个点的最近公共花祖先markPath沿路径标记花上的点主循环里遇到连接两个外点的边就收缩花。参数上MAXN按你的顶点数上限设INF用于费用初始化别用INT_MAX加法会溢出。提示如果你要的是最小费用版本增广时不能随便找一条增广路必须找 reduced cost 之和最小的那条。常见做法是在 BFS 时用优先队列按对偶变量排序或者用原始-对偶方法每轮更新势。3. 把费用塞进带花树对偶变量、收缩与展开的工程细节3.1 对偶变量的初始化与更新时机最小费用完美匹配的原始-对偶框架里每个顶点有一个势y[v]每条边有 reduced costc(u,v) c(u,v) - y[u] - y[v]。算法要求始终保持c(u,v) 0。初始时把所有y[v]设为min(c(u,v))/2之类的值保证非负。每轮找一条 reduced cost 全为零的增广路找不到就调整势把所有外点的y减小一个增量内点的y增大同样的增量这个增量取所有“外点-未访问点”边的最小 reduced cost。这个更新过程必须和花的收缩同步因为花内部的结构会影响哪些点是外点。我一般会把势的更新写成一个独立函数每轮增广前调用一次更新完再跑带花树搜索。这样调试时容易定位是势更新错了还是搜索错了。3.2 收缩花时 reduced cost 怎么维护花收缩后花内所有点的势不再单独更新而是由花这个超级点统一维护。展开时花内点的势要按收缩时记录的偏移量还原。这里最容易翻车的地方是收缩时你改了base但忘了同步更新花内边的 reduced cost 缓存导致展开后费用算错。血泪经验是每次收缩前把花内所有边的原始费用和当前势快照存一份展开时直接回滚不要试图增量计算。3.3 一个最小费用版本的增广主循环// 伪代码风格展示主循环结构 long long minCostPerfectMatching() { long long total 0; initPotentials(); // 初始化势保证 reduced cost 非负 while (true) { int v findFreeVertex(); if (v -1) break; // 没有未匹配点结束 while (true) { int to findPathWithZeroReducedCost(v); if (to ! -1) { augment(to); // 翻转增广路 break; } if (!updatePotentials()) return -1; // 无法更新无完美匹配 } } for (int i 0; i n; i) if (match[i] ! -1 i match[i]) total edgeCost(i, match[i]); return total; }逻辑说明外层循环每次找一个未匹配点作为根内层不断找零 reduced cost 增广路找不到就更新势。updatePotentials返回 false 表示当前图不存在完美匹配。参数上势的增量要取最小正值如果所有候选边的 reduced cost 都是 INF说明图不连通。注意augment里翻转匹配边时要沿着p数组回溯到根同时处理花展开。3.4 复杂度与规模边界带花树本身是 O(n^3)叠加上费用和对偶更新后常见实现是 O(n^3 * C) 或 O(n^2 * m * log) 取决于具体变体。实测在 n200、m2000 左右的稠密图上优化过的 C 版本单次求解在几十毫秒量级n 到 500 以上如果边很稠密耗时会明显上升。如果你的场景 n 超过 1000建议先考虑问题是否真的需要一般图能不能转成二分图或者用近似算法。4. 避坑与排查带花树费用版最常见的五类翻车4.1 样例过但随机数据错花展开时匹配边没还原现象小样例能跑对随机生成 20 个点的图结果费用比暴力枚举大。原因收缩花时改了match数组展开时只还原了base忘了还原花内点的匹配关系。解决在markPath和收缩逻辑里用一个栈记录所有被修改的match、p、base展开时逆序回滚。别偷懒只存base。4.2 费用出现负数势初始化没保证 reduced cost 非负现象跑着跑着总费用变成负数或者增广路费用为负。原因初始势设得太随意导致某些边的 reduced cost 为负原始-对偶的前提被破坏。解决初始化时对每条边算c(u,v)取所有边费用最小值的一半作为全局初始势或者用更保守的min(c)/2。如果边费用有负值先整体加一个偏移量转正最后再减回去。4.3 死循环势更新增量算成零现象程序卡在while(true)里势更新后没有任何边 reduced cost 归零。原因增量取了零或者浮点误差导致本该为零的 reduced cost 变成 1e-9。解决增量用整数运算如果费用是浮点先乘一个大系数转成整数。更新后检查是否至少有一条边的 reduced cost 变为零没有就报错退出。4.4 内存爆炸花嵌套导致 base 数组越界现象n300 时程序崩溃gdb 显示base数组访问越界。原因花可以嵌套base[i]里存的编号可能超过 n如果你用base[base[i]]这种写法没做边界检查就会越界。解决所有对base的间接访问前先判断值是否在[0, n)范围内。嵌套花的层数理论上可以到 O(n)别用固定小数组。4.5 结果比暴力大但没报错增广路选错了现象程序正常结束匹配覆盖所有点但费用不是最小。原因增广时找到了一条零 reduced cost 的路就翻转但那条路不是当前势下费用增量最小的。解决在 BFS 时用优先队列按路径累计 reduced cost 排序确保每次增广的是最小费用增广路。或者严格按原始-对偶理论只找零 reduced cost 路但势更新必须正确。5. 验证与调优怎么确认你的实现真的对以及怎么让它更快5.1 用暴力枚举做小规模对拍最可靠的验证手段是对拍。n 10 时枚举所有完美匹配算最小费用和你实现的输出比。写一个bruteForce函数用递归生成所有匹配组合。对拍脚本跑 1000 组随机图费用范围设 1 到 100边密度从 0.3 到 1.0 都覆盖。只要有一组不一致就把那组图 dump 出来单独调。这个习惯帮我抓过至少三次花展开的 bug。long long bruteForce(int n, vectorEdge edges) { // 仅用于 n 10 的对拍 vectorint perm(n); iota(perm.begin(), perm.end(), 0); long long best INF; // 枚举所有配对方式这里省略具体递归实现 // 核心是每次选一个未匹配点尝试和剩余每个点配对 return best; }逻辑说明暴力枚举的复杂度是双阶乘级别n10 时约 945 种匹配完全可接受。参数上随机图生成时确保至少存在一个完美匹配否则两边都返回无解比不出问题。5.2 性能调优的三个实际手段第一把边按费用排序BFS 时优先访问费用低的边能更早找到增广路。第二用邻接表存图但花收缩时把花内边标记为无效避免重复遍历。第三势更新时只遍历活跃的外点集合不要全图扫描。我实测在 n400、m5000 的图上这三条加起来能把耗时从 1.2 秒压到 300 毫秒左右。5.3 一个我常犯的错误早期我总想把带花树和费用流分开写先跑一个最大匹配再在匹配上做费用调整。结果发现调整阶段会破坏匹配的合法性因为一般图上不存在“保持匹配大小不变同时降费用”的简单局部操作。后来老老实实把对偶更新嵌进增广循环才稳定下来。如果你的场景允许近似可以考虑先转成二分图做 KM再对奇环做局部修正但那只适合对最优性要求不高的场合。希望帮到你。本文还有配套的精品资源点击获取