1. 程式人生 > 實用技巧 >[UOJ79]一般圖最大匹配(帶花樹)

[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(n^3)\)

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;
}