[UOJ79]一般图最大匹配(带花树)
Description
给定一张 \(n\) 个点 \(m\) 条边的无向图,求最大匹配。
要求输出每个点对应的匹配点。
\(n\le 500,m\le 124750\)。
时空限制 \(\text{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\ne 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\ne 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(nm\alpha(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 ? '\n' : ' ');
}
return 0;
}