[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\),那么:

  1. 如果 \(F\) 有增广路,那么 \(G\) 也有增广路。
  2. 如果 \(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\) 有增广路。

  1. 如果增广路没经过这个奇环,那么我们可以在 \(G_1\) 中找到一条一样的增广路。
  2. 如果经过奇环:设 \(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)\) 缩成一朵花:

要做三件事:

  1. 因为环上所有点都跟 \(p\) 合并了,所以要把环上所有 \(vis=2\) 的点全部标记 \(vis=1\),并加入队列。
  2. 把环上每个点所在的并查集都跟 \(p\) 所在的并查集合并。
  3. 修改 \(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;
}
posted @ 2020-07-26 16:39  花淇淋  阅读(159)  评论(0编辑  收藏  举报