zoukankan      html  css  js  c++  java
  • [UOJ79]一般图最大匹配(带花树)

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

    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 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)) 缩成一朵花:

    要做三件事:

    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(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;
    }
    
  • 相关阅读:
    转发URL请求
    服务端使用Zookeeper注册服务地址,客户端从Zookeeper获取可用的服务地址。
    Boss Group Worker Group NioEventLoopGroup
    Java NIO vs. IO
    解决了网关所面临的依赖于后端接口服务的上线问题
    Dealing with a Stream-based Transport 处理一个基于流的传输 粘包 即使关闭nagle算法,也不能解决粘包问题
    use Properties objects to maintain its configuration Writing Reading System Properties 维护配置 系统变量
    即使关闭了nagle算法,粘包依旧存在
    解Bug之路-TCP粘包Bug
    Netty 粘包/半包原理与拆包实战
  • 原文地址:https://www.cnblogs.com/cyf32768/p/13380851.html
Copyright © 2011-2022 走看看