[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\)
- \(vis_v=0,match_v\ne 0\),繼續 DFS \(match_v\)。
以上三種都是二分圖匹配中出現的情況。
一般圖由於可能存在奇環,還會有 \(vis_v=1\) 的情況。
具體地,令 \(p\) 為 \(u,v\) 在交替樹上的 \(lca\),則樹上路徑 \(p→v,u→p\),以及非樹邊 \((u,v)\) 組成了一個奇環。
這裡先給出做法:把這個奇環上的所有邊刪掉,並把整個環縮成一個點 \(p\)
然後,在縮點之後的新圖中,重新尋找增廣路。
接下來證明縮點的正確性,也就是要證明:設原圖為 \(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(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;
}