二分图最大匹配(匈牙利算法)
首先是二分图定义与判断 http://www.cnblogs.com/wenruo/p/5243034.html
给定一个二分图G,M为G边集的一个子集,如果M满足当中的任意两条边都不连接同一个顶点,则称M是一个匹配。
一个很好的比喻是,一个二分图,左边代表男生,右边代表女生,连线代表有好感,匹配就是把他们互相有好感的撮合起来。当然,要一夫一妻!
所谓最大匹配,就是撮合最多对。(写下这句话的时候,我在图书馆,旁边有一对情侣在虐狗……)
完美匹配就是所有的人都不落单。
上图红线代表一个匹配,图中的每条红线的就代表一对匹配的情侣们。有个汉纸落单了,所以这个不是完美匹配。
好了,回归正题,不要再虐我,哦不,虐狗了。。。
我们把上图已经匹配的点叫做匹配点,没匹配的点叫未匹配点。红线叫做匹配边,其余叫做非匹配边。
介绍几个概念(表述未必标准):
假设我们已经找到了图G的一个匹配M
交替路:图中的一条路径,交替经过匹配边,非匹配边,则称为该路径为交替路。
增广路:从一个未匹配点出发,走交替路,如果到达另一个未匹配点,则这条交替路称为增广路。
增广路一定有奇数条边,而且非匹配边比匹配边多一条。(动手画一下很容易得知,如下图左边是一个匹配 路径5->d->1->b就是一个增广路)
那么对于一个匹配,如果我们能够找到一条增广路,然后把匹配边变成非匹配边,非匹配边变成匹配边,就一定可以多出一个匹配。
于是找最大匹配的过程就变成了找增广路,当找不到增广路的时候,该匹配就是一个最大匹配。(即匈牙利算法,证明略。)
一个简单的DFS版的模板(详细注释),用vector存边。时间复杂度O(NE)
const int N = 505; vector<int> G[N]; // vector存图 int n, m; int match[N]; // 记录每个点匹配的对象 没匹配的为-1 bool used[N]; // 保证每一次找增广路的时候每个点仅搜索一次 bool findPath(int u) { // 从u点开始走交替路径 直到找到一个未匹配点 for (unsigned i = 0; i < G[u].size(); ++i) { int v = G[u][i]; if (!used[v]) { // 和该点连接且没有被搜索过的点 used[v] = true; // 该点是未匹配点 或者 走过一条匹配边然后能找到未匹配点 // 这里 设c=match[v] 既然c与match匹配了 那么c再走到其他任何一点经过的边都是非匹配边 也就实现了走交替路 if (match[v] == -1 || findPath(match[v])) { // 找到一条增广路 把新匹配的点连起来 match[v] = u; return true; } } } return false; } int maxMatch() { int ans = 0; memset(match, -1, sizeof match); for (int i = 0; i < n; ++i) { // 这里 二分图有两部分点 只需要搜索其中一部分就可以 memset(used, false, sizeof used); used[i] = true; if (findPath(i)) ++ans; // 找到一条增广路 即多了一个匹配 } return ans; }
这里使用vector存的边,有些题解用矩阵存边G[a][b]表示X集合a点到Y集合b点的连线。a,b都是从1开始的。这里是不同的,因此用了同一个used和match来保存结果,所有的点都没有重复。
最大匹配的一些相关性质:
(1)二分图的最小顶点覆盖:用最少的点,让每条边都至少和其中一个点关联。
最大匹配数 = 最小点覆盖数(Konig 定理)
(2)二分图的最大独立集:在图中选取最多的点,使任意所选两点均不相连
最大独立数 = 顶点数 — 最大匹配数
(3)最小路径覆盖数:对于一个 DAG(有向无环图),选取最少条路径,使得每个顶点属于且仅属于一条路径。路径长可以为 0(即单个点)。
最小路径覆盖数 = 顶点数 - 最大匹配数