#include <algorithm>#include <ctime>#include <iostream>#include <random>#include <tuple>#include <vector>std::mt19937_64 rng( static_cast<std::mt19937_64::result_type>(std::time(nullptr)));struct BipartiteGraph { int n1, n2; // number of vertices in X and Y, resp. std::vector<std::vector<int>> g; // edges from X to Y std::vector<int> ma, mb; // matches from X to Y and from Y to X, resp. std::vector<bool> vis; // visiting marks for DFS. BipartiteGraph(int n1, int n2) : n1(n1), n2(n2), g(n1), ma(n1, -1), mb(n2, -1) {} // Add an edge from u in X to v in Y. void add_edge(int u, int v) { g[u].emplace_back(v); } // Find an augmenting path starting at u. bool dfs(int u) { vis[u] = true; // Heuristic: find unsaturated vertices whenever possible. for (int v : g[u]) { if (mb[v] == -1) { ma[u] = v; mb[v] = u; return true; } } for (int v : g[u]) { if (!vis[mb[v]] && dfs(mb[v])) { ma[u] = v; mb[v] = u; return true; } } return false; } // Kuhn's maximum matching algorithm. std::vector<std::pair<int, int>> kuhn_maximum_matching() { // Randomly shuffle the edges. for (int u = 0; u < n1; ++u) { std::shuffle(g[u].begin(), g[u].end(), rng); } // Find a maximal set of vertex-disjoint augmenting paths in each round. while (true) { bool succ = false; vis.assign(n1, false); for (int u = 0; u < n1; ++u) { succ |= ma[u] == -1 && dfs(u); } if (!succ) break; } // Collect the matched pairs. std::vector<std::pair<int, int>> matches; matches.reserve(n1); for (int u = 0; u < n1; ++u) { if (ma[u] != -1) { matches.emplace_back(u, ma[u]); } } return matches; }};int main() { std::ios::sync_with_stdio(false), std::cin.tie(nullptr); int n1, n2, m; std::cin >> n1 >> n2 >> m; BipartiteGraph gr(n1, n2); for (int i = 0; i < m; ++i) { int u, v; std::cin >> u >> v; gr.add_edge(u, v); } auto res = gr.kuhn_maximum_matching(); std::cout << res.size() << '\n'; for (int i = 0; i < res.size(); ++i) { std::cout << res[i].first << ' ' << res[i].second << '\n'; } return 0;}
#include <algorithm>#include <iostream>#include <queue>#include <tuple>#include <vector>struct BipartiteGraph { int n1, n2; // number of vertices in X and Y, resp. std::vector<std::vector<int>> g; // edges from X to Y std::vector<int> ma, mb; // matches from X to Y and from Y to X, resp. std::vector<int> dist; // distance from unsaturated vertices in X. BipartiteGraph(int n1, int n2) : n1(n1), n2(n2), g(n1), ma(n1, -1), mb(n2, -1) {} // Add an edge from u in X to v in Y. void add_edge(int u, int v) { g[u].emplace_back(v); } // Build the level graph. bool bfs() { dist.assign(n1, -1); std::queue<int> q; for (int u = 0; u < n1; ++u) { if (ma[u] == -1) { dist[u] = 0; q.emplace(u); } } // Build the level graph for all reachable vertices. bool succ = false; while (!q.empty()) { int u = q.front(); q.pop(); for (int v : g[u]) { if (mb[v] == -1) { succ = true; } else if (dist[mb[v]] == -1) { dist[mb[v]] = dist[u] + 1; q.emplace(mb[v]); } } } return succ; } // Find an augmenting path starting at u. bool dfs(int u) { for (int v : g[u]) { if (mb[v] == -1 || (dist[mb[v]] == dist[u] + 1 && dfs(mb[v]))) { ma[u] = v; mb[v] = u; return true; } } dist[u] = -1; // Mark this point as inreachable after one visit. return false; } // Hopcroft-Karp maximum matching algorithm. std::vector<std::pair<int, int>> hopcroft_karp_maximum_matching() { // Build the level graph and then find a blocking flow. while (bfs()) { for (int u = 0; u < n1; ++u) { if (ma[u] == -1) { dfs(u); } } } // Collect the matched pairs. std::vector<std::pair<int, int>> matches; matches.reserve(n1); for (int u = 0; u < n1; ++u) { if (ma[u] != -1) { matches.emplace_back(u, ma[u]); } } return matches; }};int main() { std::ios::sync_with_stdio(false), std::cin.tie(nullptr); int n1, n2, m; std::cin >> n1 >> n2 >> m; BipartiteGraph gr(n1, n2); for (int i = 0; i < m; ++i) { int u, v; std::cin >> u >> v; gr.add_edge(u, v); } auto res = gr.hopcroft_karp_maximum_matching(); std::cout << res.size() << '\n'; for (int i = 0; i < res.size(); ++i) { std::cout << res[i].first << ' ' << res[i].second << '\n'; } return 0;}
图 G 的任一最大匹配都包含 U 的顶点之间的一个完美匹配,且将 O 中的每一个顶点都匹配到 E 中的一个顶点。也就是说,图 G 的最大匹配的大小等于 ∣O∣+∣U∣/2。
图 G 中不包含连接 E 中顶点和 E∪U 中顶点的边。
证明
按照定义,U 和 E∪O 不交。只需要证明 E 和 O 不交。假若不然,对于顶点 v∈E∩O,存在一条从未匹配点 a 到 v 的长度为偶数的交错路,也存在一条从未匹配点 b 到 v 的长度为奇数的交错路。由于图 G 是二分图,a=b,且两条路径到达 v 时的边分别是匹配边和非匹配边。因此,将两条路连接起来,就得到一条从 a 经 v 到 b 的交错路。这是一条增广路。这与 M 是最大匹配这一点相矛盾。因此,E∩O=∅。
设 M′ 是一个与 M 不同的最大匹配。重复 Berge 引理的证明 可以说明,M′⊕M 仅由偶数长度的路径和偶环组成。从最大匹配 M 出发,可以逐个将这些连通块(路径和环)中的边翻转(匹配边和非匹配边交换),就能得到最大匹配 M′。翻转偶环时,未匹配点仍然是未匹配点,从它出发的交错路的长度奇偶性也不会改变;翻转偶数长度路径时,路径两个端点的匹配状态互换,但是从它们出发到达路径中的任何一个点的路径长度的奇偶性也是一致的。因此,在翻转过程中,集合 E,O,U 均始终保持不变。这就说明这一分解与最大匹配 M 的选取无关。
如果一条匹配边出现在某条从未匹配点 v 出发的交错路中,那么它的两个端点与点 v 的距离的奇偶性必然不同,因而分别属于集合 E 和 O;否则,它的两个端点必然都在 U 中。这说明,最大匹配中的匹配边必然是 EO 边或 UU 边。反过来说,未匹配点可以沿着从它自身出发、长度为零的交错路到达,所以只会出现在集合 E 中,这说明,集合 O 和 U 中都是匹配点。简单计数可知,最大匹配的大小就是 ∣O∣+∣U∣/2。
按照定义,E 中的任一顶点 a 可以从未匹配点 v 出发沿偶数长度交错路到达,也就是说,E 中的顶点要么是未匹配点,要么到达该点的交错路 P 以匹配边结束。如果图 G 中存在连接 a 和 E∪U 中某个顶点 b 的一条边,那么根据上一段的讨论,这条边必然是非匹配边,可以沿着它延长交错路 P。这说明顶点 b 也属于集合 O,这就与第一条性质矛盾。因此,图 G 中不存在连接 E 中顶点和 E∪U 中顶点的边。
根据 Dulmage–Mendelsohn 分解的性质可知,在图 G 的任一最大匹配中,O 和 U 中的顶点都必然是匹配点。因此,O∪U 中的顶点必然是关键点。然后,需要说明集合 E 中一定没有关键点。如果在最大匹配 M 中,顶点 a∈E 是关键点,那么存在一条偶数长度的交错路 P 连接顶点 a 和某个未匹配点 b∈E。将这条路上的所有边翻转,得到的最大匹配 M⊕P 中,顶点 a 就变成未匹配点。因此,集合 E 中没有关键点。
因此,要求出最大匹配的关键点,只需要求出 Dulmage–Mendelsohn 分解即可。
最大匹配关键边
类似地,如果一条边 e 在二分图 G 的每一个最大匹配中都是匹配边,那么它就称为最大匹配的关键边。二分图的最大匹配是唯一的,当且仅当它的一个最大匹配中,所有匹配边都是关键边。
定理
设二分图 G=(X,Y,E) 的 Dulmage–Mendelsohn 分解为 V=E∪O∪U,且 M 是它的一个最大匹配。那么,边 e∈E 是关键边,当且仅当 e 的端点都在 U 中,边 e 是 M 中匹配边,且相对于 M 不存在一个包含边 e 的交错环。
证明
关键边的端点必须是关键点。根据 Dulmage–Mendelsohn 分解的性质,最大匹配的边只能是 EO 边或 UU 边。但是,E 中没有关键点,所以,关键边只能是 UU 边。当然,关键边也必须是 M 中的匹配边。设 e∈M 是一条 UU 边。它不是关键边,当且仅当存在另一个最大匹配 M′=M 使得 e∈M⊕M′。重复 Berge 引理的证明 可以说明,M′⊕M 仅由偶数长度的路径和偶环组成。这些路径的端点之一是相对于 M 的未匹配点,所以路径中的顶点都不是 U 中的点,这与边 e 的选取矛盾。因此,边 e 只能出现在偶环中。因此,一条 UU 边 e∈M 不是关键边,当且仅当相对于 M 存在一个包含边 e 的交错环。这就是所要求证的。
设二分图 G=(X,Y,E) 的一个最大匹配为 M。设图 G 中可以由未匹配的左部点 U 出发,沿着某条交错路到达的顶点集合为 Z。于是,顶点集合 C=(X∖Z)∪(Y∩Z) 就是所求的最小点覆盖。
首先,集合 C 是点覆盖。假设不然,存在边 (u,v)∈E 使得 u∈X∩Z 且 v∈Y∖Z。设 Pu 是到达 u 的一条交错路。如果边 (u,v) 是匹配边,那么,路径 Pu 中的最后一条边就是 (v,u),这与 v∈/Z 矛盾;如果边 (u,v) 不是匹配边,那么,可以沿着边 (u,v) 延长 Pu 得到一条到达 v 的交错路,同样与 v∈/Z 矛盾。这些矛盾说明所有边都至少包含一个 C 中的端点,所以 C 是点覆盖。
然后,需要说明 C 是最小点覆盖。为了覆盖最大匹配 M 的所有边,任何点覆盖都至少需要 ∣M∣ 个顶点。因此,只需要证明 ∣C∣=∣M∣,它就一定是最小点覆盖。这等价于证明,除了包含每条匹配边各一个端点之外,C 再不包含其它顶点;也就是说,C 不包含未匹配点。假设不然,存在未匹配点 v∈C。如果 v∈X,就一定有 v∈U⊆Z,这与 C 的构造矛盾;如果 v∈Y,那么,到达 v 的一条交错路是相对于 M 的一条增广路,由 Berge 引理,这与 M 是最大匹配矛盾。这些矛盾说明不存在这样的未匹配点,进而 C 是最小点覆盖。
二分图 G′ 的每个匹配 M′,都对应着图 G 的一张子图 F,且子图 F 中每个顶点的入度和出度都至多为一,也就是说,子图 F 实际上是有向图 G 中若干互不相交的路径或环的集合。但是,已经假设 G 中不存在环路,所以 F 只包含若干不交的路径。反过来,对于每个这样的子图 F,都能构造出相应的匹配。因为匹配 M 的大小,就是顶点数量与 F 中路径数量的差值,所以,图 G 的最小路径覆盖问题,就对应着图 G′ 的最大匹配问题。
Bast, Holger; Mehlhorn, Kurt; Schäfer, Guido; Tamaki, Hisao (2006), “Matching algorithms are fast in sparse random graphs”, Theory of Computing Systems, 39 (1): 3–14. ↩