Description
给定一张 (n) 个点 (m) 条边的无向图,求最大匹配。
要求输出每个点对应的匹配点。
(nle 500,mle 124750)。
时空限制 ( ext{1s/256MB})。
Solution
以下内容参考:陈胤伯《浅谈图的匹配算法及其应用》
一些相关定义
- 交替路:匹配边和非匹配边交替出现的路径。
- 交替树:根到任意一点的路径,都是交替路。
- 未盖点:未匹配的点。
- 增广路:路径为交替路,且开头和结尾都是未盖点。
- 交替环:匹配边和非匹配边交替出现的环。
- 增广:把路径上每条边的状态取反,即匹配边变为非匹配边,非匹配边变为匹配边。
初步思路
我们先按二分图匹配来做。
枚举一个未盖点 (s),从 (s) 开始 DFS,尝试找出一条以 (s) 开头的增广路。
这样会 DFS 出一棵以 (s) 为根的交替树。
记一个数组 (vis_u),(vis_u=1) 表示 (u) 在交替树上到 (s) 的距离为偶数,(vis_u=2) 则为奇数。若 (vis_u=0),表示还没访问到 (u)。
令 (match_u) 表示 (u) 的匹配点,没有则为 (0)。
设当前 DFS 到 (u),(u) 是红点,枚举和 (u) 相连的点 (v):
- (vis_v=2),说明找到一个交替环,什么也不用做。
- (vis_v=0,match_v=0),这时候起点 (s) 和终点 (v) 都是未盖点,且 (s→v) 为交替路,因此 (s→v) 是一条增广路。那么将 (s→v) 增广,然后结束 DFS。
- (vis_v=0,match_v e 0),继续 DFS (match_v)。
以上三种都是二分图匹配中出现的情况。
一般图由于可能存在奇环,还会有 (vis_v=1) 的情况。
具体地,令 (p) 为 (u,v) 在交替树上的 (lca),则树上路径 (p→v,u→p),以及非树边 ((u,v)) 组成了一个奇环。
这里先给出做法:把这个奇环上的所有边删掉,并把整个环缩成一个点 (p)。即对于环上任意一点 (x),如果存在边 ((x,y)) 满足 (y) 不在环上,那么删除 ((x,y)),连接 ((p,y))。
然后,在缩点之后的新图中,重新寻找增广路。
接下来证明缩点的正确性,也就是要证明:设原图为 (F),缩点之后的图为 (G),那么:
- 如果 (F) 有增广路,那么 (G) 也有增广路。
- 如果 (G) 有增广路,那么 (F) 也有增广路。
如果上述两点均成立,那么 (F) 和 (G) 就是等价的,也就是缩点是合法的。
证明第一点
我们将 (F) 中 (s→p) 这一条路径上的边状态全部取反,得到 (F_1)。将 (G) 也通过同样的变换得到 (G_1)。
我们发现 (s→p) 路径长度必为偶数,即必有 (vis_p=1)。因为在交替树中,(p) 有至少两个儿子,所以 (match_p) 肯定是 (p) 的父节点,(p) 和儿子的边肯定是非匹配边。
而交替树中,(s) 和儿子的边肯定也是非匹配边,因为 (s) 是未盖点。所以 (vis_p=vis_s=1)。
这说明了,(F) 和 (F_1) 中的匹配数相同。
如果 (F) 有增广路,那么说明 (F) 的匹配不是最大匹配,那么 (F_1) 中的匹配也不是最大匹配。根据定理:(F) 的匹配是最大匹配,充要条件是 (F) 中不存在增广路。可知 (F_1) 也有增广路。
同理如果 (G_1) 有增广路,那么 (G) 也有增广路。
现在只要证明,如果 (F_1) 有增广路,那么 (G_1) 有增广路。
- 如果增广路没经过这个奇环,那么我们可以在 (G_1) 中找到一条一样的增广路。
- 如果经过奇环:设 (F_1) 存在一条增广路为 (s→t),且第一个在环上的点为 (x)。那么我们把增广路改为 (s→x→p),且 (x→p) 为环上路径。因为 (s→x,x→p) 都是交替路,而 (s,p) 在 (F_1,G_1) 中都是未盖点,所以 (s→x→p) 是一条合法的增广路。将其对应到 (G_1) 中,相当于走到缩成的新点 (w),就停下来。而 (w) 也是未盖点((w) 相当于 (F_1) 的 (p)),那么 (G_1) 的 (s→w) 也是增广路。
证毕。
证明第二点
和证明第一点一样,我们只要证明:
如果 (G_1) 有增广路,那么 (F_1) 有增广路。
同样只需考虑增广路经过缩成的新点(奇环)的情况。
已知 (w) 是未盖点,那么经过 (w) 的增广路,可以改成以 (w) 结尾。
考虑 (G_1) 中增广路以 (w) 为结尾的边 ((x,w))。在 (F_1) 中,找到环上的一个点 (y) 使得存在边 ((x,y)),那么 (F_1) 中的增广路可以是:(s→x→y→p)。
证毕。
具体实现
还是枚举未盖点 (s),寻找以 (s) 为开头的增广路。
但是不用 DFS,改用 BFS。
BFS 的过程中,还需要对每个点 (u) 维护以下信息:
- (u) 所在的花中,深度(指到 (s) 的树上距离)最小的点是哪个,可以使用并查集。
- (pre_u):若 (vis_u=1),则 (match_u) 是父节点,否则 (pre_u) 是父节点。(pre_u) 的记录可以便于增广。
先把 (s) 加入队列,并标记 (vis_s=1)。
每次取出队头 (u),枚举与其相连的点 (v)。
- (vis_v=2),或 (v,u) 已经被缩成同一个点(同一朵花)了,什么也不用做。
- (vis_v=0,match_v=0),令 (pre_v=u),增广 (s→v)。
- (vis_v=0,match_v e 0),令 (pre_v=u),并把 (match_v) 加入队列。
- (vis_v=1),令 (p=lca(u,v)),将奇环上的点缩掉。
找 (lca(u,v)):
注意到 (vis_u=vis_v=1),即 (u,v) 的深度均为偶数。那么可以轮流让 (u,v) 向上跳两步,即依次执行 (u=pre_{match_u},v=pre_{match_v},u=pre_{match_u},v=pre_{match_v})。
当然如果某一步无法再向上跳了,就跳过这一步。我们把经过的点全部标记,如果走到了已经有标记的点,就是 (lca) 了。
注意 (u) 每跳一步都要执行 (u=find(u)),即找并查集的根,(v) 也是,不然会凉。这个原因下面会讲。
将路径 ((u,p),(v,p)) 缩成一朵花:
要做三件事:
- 因为环上所有点都跟 (p) 合并了,所以要把环上所有 (vis=2) 的点全部标记 (vis=1),并加入队列。
- 把环上每个点所在的并查集都跟 (p) 所在的并查集合并。
- 修改 (pre) 数组,使得对于环上任意一条非匹配边 ((x,y)),都有 (pre_x=y,pre_y=x)。 此时环上的 (pre_x) 就是 (x) 走环上非匹配边到达的点,(match_x) 就是 (x) 走环上匹配边到达的点,当然这个 (x) 不能是 (p),因为只能从环上其它点走到 (p),不能从 (p) 走到环上其它点。那么 (pre,match) 数组维护了环上所有的边。
此时 (vis=2) 的 (pre),不一定都是交替树上的父边了。当然 (match_p) 肯定还是 (p) 的父边。因此在跳交替树的每一步都要 (u=find(u))。否则,(u) 不是所在花的根,执行 (u=pre_{match_u}) 时,可能会跳到别的花里去。注意这个时候 (u,v,p) 还没缩花,但 (u) 可能在别的花里面。
时间复杂度 (O(nmalpha(n)))。
Code
#include <bits/stdc++.h>
using namespace std;
template <class t>
inline void read(t & res)
{
char ch;
while (ch = getchar(), !isdigit(ch));
res = ch ^ 48;
while (ch = getchar(), isdigit(ch))
res = res * 10 + (ch ^ 48);
}
template <class t>
inline void print(t x)
{
if (x > 9) print(x / 10);
putchar(x % 10 + 48);
}
const int e = 1005, o = 3e5 + 5;
int adj[e], nxt[o], go[o], num, n, m, pre[e], match[e], ans, fa[e], tim, vis[e], tag[e];
queue<int>q;
inline void link(int x, int y)
{
nxt[++num] = adj[x]; adj[x] = num; go[num] = y;
nxt[++num] = adj[y]; adj[y] = num; go[num] = x;
}
inline int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
inline int lca(int x, int y)
{
tim++;
for (;;)
{
if (x)
{
x = find(x);
if (tag[x] == tim) return x;
tag[x] = tim; x = pre[match[x]];
}
swap(x, y);
}
}
inline void flower(int x, int y, int p)
{
while (find(x) != p)
{
pre[x] = y; y = match[x];
vis[y] = 1; q.push(y);
if (find(x) == x) fa[x] = p;
if (find(y) == y) fa[y] = p;
x = pre[y];
}
}
inline bool bfs(int s)
{
int i;
for (i = 1; i <= n; i++) vis[i] = pre[i] = 0, fa[i] = i;
while (!q.empty()) q.pop();
q.push(s); vis[s] = 1;
while (!q.empty())
{
int u = q.front();
q.pop();
for (i = adj[u]; i; i = nxt[i])
{
int v = go[i];
if (vis[v] == 2 || find(u) == find(v)) continue;
if (!vis[v])
{
vis[v] = 2; pre[v] = u;
if (!match[v])
{
int x = v;
while (x)
{
int y = pre[x], z = match[y];
match[x] = y; match[y] = x;
x = z;
}
return 1;
}
vis[match[v]] = 1;
q.push(match[v]);
}
else
{
int p = lca(u, v);
flower(u, v, p); flower(v, u, p);
}
}
}
return 0;
}
int main()
{
read(n); read(m);
int i, x, y;
while (m--)
{
read(x); read(y);
link(x, y);
}
for (i = 1; i <= n; i++)
if (!match[i] && bfs(i)) ans++;
cout << ans << endl;
for (i = 1; i <= n; i++)
{
print(match[i]);
putchar(i == n ? '
' : ' ');
}
return 0;
}