---
tags: 成大高階競技程式設計 2021
---
# 2021 Week17 - Matching
註::bulb: 代表相對少見、較進階或與主題關聯性較低的內容,讀者可自行斟酌觀看。
---
# 匹配(Matching)
# 最大匹配(Maximum Cardinality Matching)
```graphviz
graph {
A [label="A", style=filled];
B [label="B", style=filled];
C [label="C"];
D [label="D", style=filled];
E [label="E", style=filled];
F [label="F", style=filled];
G [label="G", style=filled];
A -- B [style=bold, color="#B000B0"];
A -- C;
A -- D;
C -- F;
B -- E;
D -- E [style=bold, color="#B000B0"];
F -- G [style=bold, color="#B000B0"];
}
```
> 二個節點有邊相連時,彼此可以形成一對,同時不再與其他點配對。
**一個圖 $G$ 上兩兩不相鄰的邊組成一個集合 $M$,稱為圖上的一個匹配(matching)。**
> * 匹配邊(matched edge):$M$ 中的邊
> * 非匹配邊(unmatched edge)
> * 匹配點(matched vertex):某個匹配邊的端點
> * 非匹配點(unmatched vertex)
> <br>
> * 最大匹配(maximum matching):所有 $G$ 的匹配中,匹配邊最多的匹配
> * 完美匹配(perfect matching):$G$ 的所有節點均為匹配點
<hr class="thin">
#### 交錯路徑
```graphviz
graph {
rankdir=LR;
A [label=""];
node [label="", style=filled];
A -- B;
B -- C [style=bold, color="#B000B0"];
C -- D;
D -- E [style=bold, color="#B000B0"];
}
```
$G$ 上的一條路徑,邊由 $M$ 的匹配邊與非匹配邊交錯組成,且起點為非匹配點,稱為 $M$-交錯路徑($M$-alternating path)。
<hr class="thin">
#### 可擴增路徑
```graphviz
graph {
rankdir=LR;
A [label=""];
B [label="", style=filled];
C [label="", style=filled];
D [label=""];
A -- B;
B -- C [style=bold, color="#B000B0"];
C -- D;
}
```
**一條 $M$-交錯路徑($M$-alternating path),起點與終點皆為非匹配點,稱為 $M$-可擴增路徑($M$-augmenting path)。**
> 「$M$-」代表它跟匹配 $M$ 有關。
將一條 $M$-可擴增路徑 $P$ 擴增,匹配邊變成非匹配邊,非匹配邊變成匹配邊,
可以得到一個更大的匹配 $M'=M\;\triangle\; P=(M\cup P)\setminus (M\cap P)$。
> $\triangle$ 是對稱差(symmetric difference)。
> ```graphviz
> graph {
> rankdir=LR;
> label="Augmented :";
> fontsize=15;
> labelloc=t;
> labeljust=l;
>
> A [label="", style=filled];
> B [label="", style=filled];
> C [label="", style=filled];
> D [label="", style=filled];
>
> A -- B [style=bold, color="#B000B0"];
> B -- C;
> C -- D [style=bold, color="#B000B0"];
> }
> ```
<hr class="dotted">
### Berge's lemma
:::danger
**一個匹配 $M$ 是最大匹配若且唯若不存在 $M$-可擴增路徑。**
:::
> #### 證明:bulb:
>
> 1. 如果存在 $M$-可擴增路徑,就可以得到更大的匹配,所以 $M$ 就不是最大匹配。
> 2. 如果不存在 $M$-可擴增路徑,對於任意一個匹配 $M'$,
> 考慮 $M\;\triangle\;M'$ 所構成的圖 $H$,$\Delta(H)\le 2$,
> 所以 $H$ 的每個連通部分要不是路徑就是環。
>
> 如果是路徑的話,一定是 $M$-交錯路徑,
> 但是根據假設,它不是 $M$-可擴增路徑,
> 所以路徑上屬於 $M$ 的邊不少於屬於 $M'$ 的邊;
> 如果是環的話,環上屬於 $M$ 的邊跟屬於 $M'$ 的邊一樣多。
>
> 得到結論,$|M|\ge |M'|$,所以 $M$ 是最大匹配。
>
> > $\Delta(G)$ 代表圖 $G$ 的最大度數(maximum degree)。
<hr class="thin">
#### 奇點/偶點
```graphviz
digraph {
rankdir=LR;
A [label="", color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
C [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
D [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
A -> B [arrowhead=o];
B -> C [style=bold, arrowhead=o, color="#B000B0"];
C -> D [arrowhead=o];
D -> E [style=bold, arrowhead=o, color="#B000B0"];
}
```
一條交錯路徑上,距離起點基數條邊的點為**奇點(odd vertex)**;
距離起點偶數條邊的點為**偶點(even vertex)**(包含起點)。
> 交錯路徑並沒有真的起點,端看我們怎麼看它,
> 而在尋找時,通常以某個非匹配點作為起點開始。
<hr class="dashed">
## 二分圖最大匹配(Maximum Cardinality Bipartite Matching)
> 二分圖,代表節點可以被分為二個集合,同個集合內的節點不能有邊相連。
```graphviz
graph {
rankdir=LR;
subgraph cluster_X {
style=dashed;
label="X";
A [label="0", fontcolor="#FF0000", style=filled];
B [label="1", fontcolor="#FF0000", style=filled];
C [label="2", fontcolor="#FF0000"];
}
subgraph cluster_Y {
style=dashed;
label="Y";
a[label="0", fontcolor="#0000FF", style=filled];
b[label="1", fontcolor="#0000FF", style=filled];
c[label="2", fontcolor="#0000FF"];
}
A -- a [style=bold, color="#B000B0"];
A -- c;
B -- a;
B -- b [style=bold, color="#B000B0"];
C -- a;
C -- b;
}
```
因為可擴增路徑長度是奇數,二分圖上的一條可擴增路徑,兩個非匹配點一定分屬集合 $X,Y$。
因此在找可擴增路徑時,只需要**以 $X$ 中的非匹配點為起點**就可以了。
> 交錯路徑上,**$X$ 的點都是偶點,$Y$ 的點都是奇點**。
```graphviz
digraph {
rankdir=LR;
A [label="0", fontcolor="#FF0000", style=filled, fillcolor=lightgray, color="#FF0000"];
C [label="2", fontcolor="#FF0000", color="#FF0000"];
a[label="0", fontcolor="#0000FF", style=filled, fillcolor=lightgray, color="#0000FF"];
c[label="2", fontcolor="#0000FF", color="#0000FF"];
C -> a [arrowhead=o];
a -> A [style=bold, color="#B000B0", arrowhead=o];
A -> c [arrowhead=o];
}
```
以一個某個 $X$ 中的起點,沿著不同的交錯路徑往下找,
交錯路徑 $P_1,P_2$ 可能會出現重疊的情況,
但是在 $P_2$ 上,$P_1$ 的偶點還是偶點,奇點還是奇點,
也就是說 $P_1$ 從交會點往下走,與 $P_2$ 從交會點往下走,
是否能變成可擴增路徑的結果是一樣的,而且最終也只能選擇一條,
所以**當路徑遇到已經被拜訪過的節點,不考慮往此節點繼續走**。
```graphviz
digraph {
rankdir=LR;
A [label="0", fontcolor="#FF0000", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="1", fontcolor="#FF0000", style=filled, fillcolor=lightgray, color="#FF0000"];
C [label="2", fontcolor="#FF0000", color="#FF0000"];
a[label="0", fontcolor="#0000FF", style=filled, fillcolor=lightgray, color="#0000FF"];
b[label="1", fontcolor="#0000FF", style=filled, fillcolor=lightgray, color="#0000FF"];
c[label="2", fontcolor="#0000FF", color="#0000FF"];
C -> a [arrowhead=o];
a -> A [style=bold, arrowhead=o, color="#B000B0"];
A -> c [arrowhead=o];
C -> b [arrowhead=o];
b -> B [style=bold, arrowhead=o, color="#B000B0"];
B -> a [style=dotted, arrowhead=o];
}
```
> **奇點到偶點是匹配邊,偶點到奇點是非匹配邊**,
> 因為奇點的匹配邊唯一,只會到唯一的偶點(並且沒被拜訪過),
> **cross edge 只可能是「偶點到奇點」或「偶點到偶點」的邊**;
> 又因為二分圖沒有偶點到偶點($X$ 到 $X$)的邊,
> 因此遇到被拜訪過的點,也就是 **cross edge,一定是偶點到奇點的邊**。
<hr class="dotted">
### Augmenting Path Algorithm
依序以 $X$ 中每個非匹配點為起點,進行 DFS,尋找可擴增路徑,
如果有找到的話就進行擴增,最後就可以得到最大匹配。
> 為什麼可以這樣做呢?
> 如果演算法結束後,每個 $X$ 的非匹配點都沒有可擴增路徑,
> 根據 Berge's lemma,匹配就是最大匹配了。
>
> 首先擴增只會把某些非匹配點變為匹配點,所以演算法結束後還是 $X$ 的非匹配點,
> 代表一開始它就是 $X$ 的非匹配點,我們已經確認過不存在以它為起點的可擴增路徑。
> 因此剩下要確認的就是,**原本沒有可擴增路徑的點,在進行擴增後,還是沒有可擴增路徑**。
>
> #### 證明:bulb:
> 假設對於可擴增路徑 $P_v=v_0\dots v_k$ 進行擴增,
> 造成另一個 $X$ 的非匹配點 $u_1$,因此多了一條可擴增路徑 $P_u=u_1\dots u_2$,
> 則 $P_u$ 與 $P_v$ 一定有部分重疊路徑 $v_i\dots v_j$。
>
> ```graphviz
> digraph {
> rankdir=LR;
> label="Before :";
> fontsize=30;
> labelloc=t;
> labeljust=l;
>
> A [label="v0", fontcolor="#FF0000", color="#FF0000"];
> B [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> C [label="vi", fontcolor="#FF0000", style=filled, fillcolor=lightgray, color="#FF0000"];
> D [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> F [label="vj", fontcolor="#0000FF", style=filled, fillcolor=lightgray, color="#0000FF"];
> G [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> H [label="vk", fontcolor="#0000FF", color="#0000FF"];
>
> a [label="u1", fontcolor="#FF0000", style=dashed];
> b [label="", style="filled, dashed", fillcolor=lightgray];
> c [label="", style="filled, dashed", fillcolor=lightgray];
> d [label="u2", fontcolor="#0000FF", style=dashed];
>
> A -> B [arrowhead=o];
> B -> C [style=bold, arrowhead=o, color="#B000B0"];
> C -> D [arrowhead=o];
> D -> E [style=bold, arrowhead=o, color="#B000B0"];
> E -> F [arrowhead=o];
> F -> G [style=bold, arrowhead=o, color="#B000B0"];
> G -> H [arrowhead=o];
>
> b -> a [style=dashed, dir=none];
> c -> b [style="bold, dashed", dir=none, color="#B000B0"];
> F :ne -> c [style=dashed, dir=none];
> d -> C :sw [style=dashed, dir=none];
> }
> ```
> >
> ```graphviz
> digraph {
> rankdir=LR;
> label="After :";
> fontsize=30;
> labelloc=t;
> labeljust=l;
>
> A [label="v0", fontcolor="#FF0000", style=filled, fillcolor=lightgray];
> B [label="", style=filled, fillcolor=lightgray];
> C [label="vi", fontcolor="#FF0000", style=filled, fillcolor=lightgray, color="#FF0000"];
> D [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> F [label="vj", fontcolor="#0000FF", style=filled, fillcolor=lightgray, color="#0000FF"];
> G [label="", style=filled, fillcolor=lightgray];
> H [label="vk", fontcolor="#0000FF", style=filled, fillcolor=lightgray];
>
> a [label="u1", fontcolor="#FF0000", style=dashed, color="#FF0000"];
> b [label="", style="filled, dashed", fillcolor=lightgray, color="#0000FF"];
> c [label="", style="filled, dashed", fillcolor=lightgray, color="#FF0000"];
> d [label="u2", fontcolor="#0000FF", style=dashed, color="#0000FF"];
>
> A -> B [style=bold, dir=none, color="#B000B0"];
> B -> C [dir=none];
> C -> D [style=bold, dir=back, arrowtail=o, color="#B000B0"];
> D -> E [dir=back, arrowtail=o];
> E -> F [dir=back, style=bold, arrowtail=o, color="#B000B0"];
> F -> G [dir=none];
> G -> H [dir=none, style=bold, color="#B000B0"];
>
> b -> a [style=dashed, dir=back, arrowtail=o];
> c -> b [style="bold, dashed", dir=back, arrowtail=o, color="#B000B0"];
> F :ne -> c [style=dashed, dir=back, arrowtail=o];
> d -> C :sw [style=dashed, dir=back, arrowtail=o];
> }
> ```
>
> $(v_i,v_{i+1})$ 與 $(v_{j-1},v_j)$ 擴增前一定是非匹配邊,因此如圖所示,
> 擴增前 $u_1$ 應該要有可擴增路徑($u_1\dots v_k$),與假設衝突,原命題成立。
>
> > 如果 $(v_i,v_{i+1})$ 或 $(v_{j-1},v_j)$ 擴增前是匹配邊,擴增後是非匹配邊,
> > $(P_u\setminus P_v)$ 上要有另一條匹配邊連接著 $v_i$ 或 $v_j$,
> > 但是 $(v_{i-1},v_i)$ 與 $(v_j,v_{j+1})$ 也是匹配邊,這就不是匹配了。
<hr class="dotted">
實作上,**DFS 只停留在 $X$ 的點**,**每次走一條非匹配邊**,
若此時遇到的是 $Y$ 的非匹配點,就找到可擴增路徑了;
如果是 $Y$ 的匹配點,則**直接接著再走一條匹配邊**,到另一個 $X$ 的點。
> $Y$ 的點的匹配邊唯一,這就是為什麼只以 $X$ 的點為基準,
> 並且每次走二條邊(**一條非匹配邊 $+$ 一條匹配邊**)。
```cpp!
class BipartiteMatching {
vector<bool> vis;
vector<int> mx{}, my{};
bool dfs(const vector<vector<int>>& adj, int x) {
for (auto& y : adj[x]) {
if (vis[y]) continue;
vis[y] = true;
if (my[y] == -1 || dfs(adj, my[y])) {
mx[x] = y, my[y] = x;
return true;
}
}
return false;
}
public:
pair<vector<int>, vector<int>> operator()(const vector<vector<int>>& adj, int ny) {
vis.resize(ny), mx.assign(adj.size(), -1), my.assign(ny, -1);
int c{0};
for (int x{0}; x < adj.size(); ++x) {
fill(vis.begin(), vis.end(), false);
if (dfs(adj, x)) ++c;
}
return {move(mx), move(my)};
}
};
```
每個點進行一次 DFS,複雜度為 $O(|V|(|V|+|E|))=O(|V||E|)$。
<hr class="dotted">
#### greedy matching
先找到一個極大匹配,不要從空的匹配開始。
> 一個極大匹配,不為任何其他匹配的子集。
```cpp!
for (int x{0}; x < adj.size(); ++x) // greedy matching
for (auto& y : adj[x]) {
if (my[y] != -1) continue;
mx[x] = y, my[y] = x, ++c;
break;
}
```
<hr class="dashed">
### Hopcroft-Karp Algorithm
Augmenting Path Algorithm 透過 DFS 搜尋,
常常去找較長的可擴增路徑,導致複雜度並不優異;
Hopcroft-Karp Algorithm 改為尋找最短的可擴增路徑,
我們沒辦法事先知道最短可擴增路徑的起點是誰,
因此以所有 $X$ 的非匹配點當作樹根,進行 BFS。
> 如果分別從 $x_i,x_j$ 開始尋找,當它們交會時,
> 在 $x_i$ 繼續找下去的結果與 $x_j$ 一致,
> 所以只從 $x_i$ 找下去即可,與之前的結論相同,
> **當路徑遇到已經被拜訪過的節點,不往此節點進行搜尋**。
這邊只是「尋找」可擴增路徑,還沒有進行擴增,所以路徑重疊沒關係。
```graphviz
digraph {
rankdir=LR;
A [label="xi", fontcolor="#FF0000", color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
C [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
D1 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", color="#0000FF"];
D2 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
a [label="xj", fontcolor="#FF0000", color="#FF0000"];
b [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
c [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
A :e -> B :nw [arrowhead=o];
B -> C [style=bold, arrowhead=o, color="#B000B0"];
C :ne -> D1 :w [arrowhead=o];
D1 -> E [style=bold, arrowhead=o, color="#B000B0"];
E -> F [arrowhead=o];
C :se -> D2 :w [arrowhead=o];
a -> b [arrowhead=o];
b -> c [style=bold, arrowhead=o, color="#B000B0"];
c :e -> B :sw [style=dashed, arrowhead=o];
}
```
那為什麼不能跟之前一樣保留一條路徑就好呢?
因為 $x_i$ 可能有多條可擴增路徑,最後不一定會擴增重疊的部分。
```graphviz
digraph {
rankdir=LR;
A [label="xi", fontcolor="#FF0000", color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
C [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
D1 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", color="#0000FF"];
D2 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
G [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
H [label="", color="#0000FF"];
I [label="", color="#0000FF"];
D3 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
J [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
K [label="", color="#0000FF"];
a [label="xj", fontcolor="#FF0000", color="#FF0000"];
b [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
c [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
A -> B [arrowhead=o];
B -> C [style=bold, arrowhead=o, color="#B000B0"];
C :ne -> D1 :w [arrowhead=o];
D1 -> E [style=bold, arrowhead=o, color="#B000B0"];
E -> F [arrowhead=o];
C :e -> D2 :nw [arrowhead=o];
D2 -> G [style=bold, arrowhead=o, color="#B000B0"];
G -> H [arrowhead=o];
G -> I [arrowhead=o];
C :se -> D3 :nw [arrowhead=o];
D3 -> J [style=bold, arrowhead=o, color="#B000B0"];
J -> K [arrowhead=o];
a -> b [arrowhead=o];
b -> c [style=bold, arrowhead=o, color="#B000B0"];
c :e -> D3 :sw [style=dashed, arrowhead=o];
}
```
1. 重複步驟 $2,3$,直到沒有可擴增路徑為止
2. 把**所有的 $X$ 非匹配點**當作樹根,透過 BFS,尋找**所有最短可擴增路徑**
3. 對於 BFS 森林的每個樹葉,透過 **DFS 回溯,找可擴增路徑並擴增**
> 可擴增路徑有可能重疊,因此在擴增後,要把這條路徑**從 BFS 森林中移除**。
>
> 不是有 $k$ 條可擴增路徑,就代表可以擴增 $k$ 次。
步驟 $2$ 嚴格來說,是找到一個「點不重複的最短可擴增路徑」的極大集合
(a maximal set of vertex-disjoint shortest augmenting paths);
換句話說,這些路徑都可以進行擴增,並且擴增後,就不存在其他長度相等的可擴增路徑。
> 下面二種情況都是極大集合:
>
> ```graphviz
> digraph {
> rankdir=LR;
>
> A [label="xi", fontcolor="#FF0000", color="#FF0000"];
> B [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> C [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> D1 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> F [label="", color="#0000FF"];
> D2 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> G [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> H [label="", color="#0000FF"];
> I [label="", color="#0000FF"];
> D3 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> J [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> K [label="", color="#0000FF"];
>
> a [label="xj", fontcolor="#FF0000", color="#FF0000"];
> b [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> c [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
>
> A -> B [dir=back, arrowtail=o];
> B -> C [style=bold, dir=back, arrowtail=o, color="#B000B0"];
> C :ne -> D1 :w [dir=none];
> D1 -> E [style=bold, dir=none, color="#B000B0"];
> E -> F [dir=none];
> C :e -> D2 :nw [dir=none];
> D2 -> G [style=bold, dir=none, color="#B000B0"];
> G -> H [dir=none];
> G -> I [dir=none];
> C :se -> D3 :nw [dir=back, arrowtail=o];
> D3 -> J [style=bold, dir=back, arrowtail=o, color="#B000B0"];
> J -> K [dir=back, arrowtail=o];
>
> a -> b [dir=none];
> b -> c [style=bold, dir=none, color="#B000B0"];
> c :e -> D3 :sw [style=dashed, dir=none];
> }
> ```
>
> ```graphviz
> digraph {
> rankdir=LR;
>
> A [label="xi", fontcolor="#FF0000", color="#FF0000"];
> B [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> C [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> D1 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> F [label="", color="#0000FF"];
> D2 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> G [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> H [label="", color="#0000FF"];
> I [label="", color="#0000FF"];
> D3 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> J [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> K [label="", color="#0000FF"];
>
> a [label="xj", fontcolor="#FF0000", color="#FF0000"];
> b [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> c [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
>
> A -> B [dir=back, arrowtail=o];
> B -> C [style=bold, dir=back, arrowtail=o, color="#B000B0"];
> C :ne -> D1 :w [dir=none];
> D1 -> E [style=bold, dir=none, color="#B000B0"];
> E -> F [dir=none];
> C :e -> D2 :nw [dir=back, arrowtail=o];
> D2 -> G [style=bold, dir=back, arrowtail=o, color="#B000B0"];
> G -> H [dir=back, arrowtail=o];
> G -> I [dir=none];
> C :se -> D3 :nw [dir=none];
> D3 -> J [style=bold, dir=back, arrowtail=o, color="#B000B0"];
> J -> K [dir=back, arrowtail=o];
>
> a -> b [dir=back, arrowtail=o];
> b -> c [style=bold, dir=back, arrowtail=o, color="#B000B0"];
> c :e -> D3 :sw [style=dashed, dir=back, arrowtail=o];
> }
> ```
乍看之下,BFS 森林不容易紀錄,因為樹之間可能重疊,有相同的子樹;
換句話說,就是像上圖一樣有虛邊。
不過我們只在意最短可擴增路徑,所以如果 $x_j$ 到交會點的長度,
大於 $x_i$ 到交會點的長度,就不用考慮它了;
因此這邊記錄每個節點的深度(也就是到路徑起點的距離),
搭配原先的圖,就可以推斷出 BFS 森林。
<hr class="dotted">
BFS 時,一樣以 $X$ 的點為基準,每次走一條非匹配邊,如果需要繼續就直接再走匹配邊;
而 DFS 時,因為從 $Y$ 的點開始回溯,一樣一次走一條非匹配邊,
如果要繼續回溯,就直接走匹配邊,只差在是以 $Y$ 的點為基準。
```cpp!
// Hopcroft–Karp algorithm - O(E√V)
class BipartiteMatching {
vector<vector<int>> adjy;
vector<int> dx, dy, mx, my;
bool dfs(const vector<vector<int>>& adjx, int y) {
for (auto& x : adjy[y]) {
if (dx[x] + 1 != dy[y]) continue;
if (mx[x] == -1 || dfs(adjx, mx[x])) {
my[y] = x, mx[x] = y;
dy[y] = -1; // remove from bfs forest
return true;
}
}
dy[y] = -1; // remove from bfs forest
return false;
}
bool bfs(const vector<vector<int>>& adjx) {
fill(dx.begin(), dx.end(), -1);
fill(dy.begin(), dy.end(), -1);
queue<int> qu{}, qu2{};
for (int x{0}; x < adjx.size(); ++x) // use all unmatched x's as roots
if (mx[x] == -1) qu.push(x), dx[x] = 0;
bool found{false};
while (!qu.empty() && !found) { // stop at the level found
while (!qu.empty()) {
auto x{qu.front()}; qu.pop();
for (auto& y : adjx[x])
if (dy[y] == -1 && my[y] != x) {
dy[y] = dx[x] + 1;
if (my[y] == -1) found = true;
else qu2.push(my[y]), dx[my[y]] = dy[y] + 1;
}
}
qu.swap(qu2);
}
return found;
}
public:
pair<vector<int>, vector<int>> operator()(const vector<vector<int>>& adjx, int ny) {
adjy.assign(ny, {});
for (int x{0}; x < adjx.size(); ++x)
for (auto& y : adjx[x]) adjy[y].push_back(x);
dx.resize(adjx.size()), dy.resize(ny), mx.assign(adjx.size(), -1), my.assign(ny, -1);
int c{0};
while (bfs(adjx)) {
for (int y{0}; y < ny; ++y)
if (my[y] == -1 && dy[y] != -1) c += dfs(adjx, y);
}
return {move(mx), move(my)};
}
};
```
<hr class="thin">
:::danger
(引理 1.)對於圖 $G$ 上的二個匹配 $M_a,M_b$,如果 $|M_a|\gt|M_b|$,則 $M_b$ 至少有 $|M_a|-|M_b|$ 條(點不重複的)可擴增路徑。
:::
考慮子圖 $H=M_a\;\triangle\; M_b$,$H$ 的連通部分不是路徑就是環,
而且一定是 $M_a$ 的邊與 $M_b$ 的邊交錯,因此連通部分有三種可能,
$M_a$ 的邊比 $M_b$ 的邊多 $1$;$M_a$ 的邊與 $M_b$ 的邊一樣多;$M_a$ 的邊比 $M_b$ 的邊少 $1$。
因為 $M_a$ 的匹配邊比 $M_b$ 多了 $|M_a|-|M_b|$ 條,所以至少**有這麼多的連通部分**,
$M_a$ 的邊會比 $M_b$ 的邊多 $1$,而這樣的連通部分,就**是 $M_b$ 的可擴增路徑**。
```graphviz
digraph {
rankdir=LR;
subgraph cluster_3 {
label="Mb 的邊多 1:";
fontsize=20;
labelloc=t;
labeljust=l;
J [label=""];
K [label=""];
L [style=invis];
M [style=invis];
J -> K [label="Mb", dir=both, arrowhead=ldiamond, arrowtail=rdiamond];
K -> L [style=invis];
L -> M [style=invis];
}
subgraph cluster_2 {
label="一樣多:";
fontsize=20;
labelloc=t;
labeljust=l;
E [label=""];
F [label=""];
G [label=""];
H [label=""];
I [style=invis];
E :s -> F [label="Ma", dir=both, arrowhead=rdiamond, arrowtail=ldiamond, color=lightgray];
F -> G :sw [label="Mb", dir=both, arrowhead=ldiamond, arrowtail=rdiamond];
E :n -> H [label="Mb", dir=both, arrowhead=ldiamond, arrowtail=rdiamond];
H -> G :nw [label="Ma", dir=both, arrowhead=rdiamond, arrowtail=ldiamond, color=lightgray];
G -> I [style=invis];
}
subgraph cluster_1 {
label="Ma 的邊多 1:";
fontsize=20;
labelloc=t;
labeljust=l;
A [label=""];
B [label=""];
C [label=""];
D [label=""];
A -> B [label="Ma", dir=both, arrowhead=rdiamond, arrowtail=ldiamond, color=lightgray];
B -> C [label="Mb", dir=both, arrowhead=ldiamond, arrowtail=rdiamond];
C -> D [label="Ma", dir=both, arrowhead=rdiamond, arrowtail=ldiamond, color=lightgray];
}
}
```
<hr class="thin">
**每次的最短可擴增路徑長度都比前一次大。**
> 匹配 $M1$,對 $k$ 條長度 $l$ 的最短可擴增路徑 $p_1\dots p_k$ 擴增,
> 變為匹配 $M2=M1\;\triangle\; (p_1\cup \dots \cup p_k)$,而對於 $M2$ 的任意一條可擴增路徑 $p$,
>
> > 如果 $p$ 與 $p_1\dots p_k$ 沒有任何重疊部分,
> > 因為我們已經擴增了「最短可擴增路徑的極大集合」,
> > $p$ 的長度自然就大於 $l$,以下討論重疊的情況。
>
> $M2$ 再對 $p$ 擴增後,變為匹配 $M3=M2\;\triangle\; p$,
> 根據引理 1.,$M3$ 比 $M1$ 多了 $k+1$ 個匹配數,
> 代表 $M1$ 至少要有 $k+1$ 條可擴增路徑,並且是在 $M1\;\triangle\; M3$ 中,
> 因為 $M1$ 的最短可擴增路徑長度為 $l$,$M1\;\triangle\; M3$ **至少有 $(k+1)\cdot l$ 條邊**;
> 而 $M1\;\triangle\; M3=(p_1\cup \dots \cup p_k)\;\triangle\; p$,$p$ 與 $p_1\cup \dots \cup p_k$ 有重疊的話,
> **最多只能有 $k\cdot l+|p|-1$ 條邊**,所以得到結論,**$|p|$ 大於 $l$**。
>
> $(k+1)\cdot l\le |M1\;\triangle\; M3|=|(p_1\cup \dots \cup p_k)\;\triangle\; p|\le k\cdot l+|p|-1\\\to|p|\gt l$
<hr class="thin">
**演算法在至多 $2\sqrt V$ 次後,達到最大匹配。**
> **經過 $\sqrt V$ 次**,此時的匹配為 $M'$,可擴增路徑**長度至少 $\sqrt V$**;
> 與最大匹配 $M$,$M\;\triangle\; M'$ 的連通部分為 $M'$ 的交錯路徑或交錯環,
> 而可擴增路徑長度至少為 $\sqrt V$,代表**至多有 $\sqrt V$ 條(點不重複的)可擴增路徑**;
> 這代表 **$M'$ 的匹配數最多比 $M$ 的匹配數少了 $\sqrt V$ 個**,
> 所以**最多再經過 $\sqrt V$ 次後,就會達到最大匹配**。
<hr class="thin">
每次一個 BFS 與一個 DFS,總共 $2\sqrt V$ 次,**複雜度為 $O(|E|\sqrt{|V|})$**。
<hr class="dashed">
## 一般圖最大匹配(Maximum Cardinality Matching in a general graph)
### Edmonds' Blossom Algorithm:bulb:
```graphviz
graph {
rankdir=LR;
S0 [label="root", color="#FF0000"];
S1 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
S2 [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
S3 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
C [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
D [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
}
S0 -- S1;
S1 -- S2 [style=bold, color="#B000B0"];
S2 -- S3;
S3 -- A [style=bold, color="#B000B0"];
A :se -- B;
B :e -- C [style=bold, color="#B000B0"];
A :ne -- D;
D :e -- E [style=bold, color="#B000B0"];
C -- E [style=dashed];
A :e -- C [style=invis];
A :e -- E [style=invis];
A :e -- D [style=invis];
}
```
> 前面說到,因為奇點到偶點只能走唯一的匹配邊,
> > 且到為匹配點的奇點時,會馬上走匹配邊到偶點,
> > 確保不會有奇點到奇點的 cross edge 出現。
>
> 所以 cross edge 可能是「偶點到奇點」或「偶點到偶點」的邊,
> 而偶點到奇點的邊,在二分圖時已經討論過了。
一般圖會出現偶點到偶點的 cross edge,形成一個奇圈(odd-length cycle),稱作花(blossom)。
```graphviz
digraph {
rankdir=LR;
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
C [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
D [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
G [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
}
S0 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [arrowhead=o];
B :e -> C [dir=none, style=bold, color="#B000B0"];
A :ne -> D [arrowhead=o];
D :e -> E [dir=none, style=bold, color="#B000B0"];
E -> F [arrowhead=o];
F -> G [dir=none, style=bold, color="#B000B0"];
C -> G [style=dashed, dir=none];
A :e -> C [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
}
```
<hr class="thin">
花上的奇點只要從另一側過來,就可以搖身一變成為偶點,繼續尋找可擴增路徑。
```graphviz
digraph {
rankdir=LR;
S0 :n -> H :s [style=invis];
H -> I :s [style=invis];
H [style=invis];
I [style=invis];
H -> D [dir=back, arrowtail=o];
F -> I [arrowhead=o];
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
C [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
D [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
G [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
}
S0 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [arrowhead=o];
B :e -> C [dir=none, style=bold, color="#B000B0"];
A :ne -> D [arrowhead=o];
D :e -> E [dir=none, style=bold, color="#B000B0"];
E -> F [arrowhead=o];
F -> G [dir=none, style=bold, color="#B000B0"];
C -> G [style=dashed, dir=none];
A :e -> C [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
J [style=invis];
J -> B :s [dir=back, arrowtail=o];
}
```
<hr class="thin">
因為花上都是偶點,所以可以進行縮花(blossom contraction),變為一個假點。
> 縮花是為了讓圖逐漸變小,減少運算量,也可以選擇不縮花。
```graphviz
digraph {
rankdir=LR;
S0 [label="root", color="#FF0000"];
S1 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
S2 [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
S3 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
S4 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
subgraph cluster_blossom {
label="Blossom";
fontsize=15;
labelloc=t;
labeljust=l;
A [shape=doublecircle, label="base", style=filled, fillcolor=lightgray, color="#FFA500"];
}
S0 -> S1;
S1 -> S2 [style=bold, color="#B000B0"];
S2 -> S3;
S3 -> A [style=bold, color="#B000B0"];
A -> S4;
}
```
<hr class="thin">
1. 依序以每個非匹配點當作樹根,進行步驟 $2,3$
2. 透過 BFS 尋找可擴增路徑
* 出現花就縮花,花上的奇點變成偶點,繼續尋找
* 找到可擴增路徑就結束搜尋
3. 沿著可擴增路徑拆花並擴增
> 正確性論證與 Augmenting path algorithm 相同。
<hr class="dotted">
實作上,花雖然是一個圈,但還是有方向性地分為二側,
因此我們對二側原本的偶點,分別記錄 cross edge,
以利之後可以繞花,到達另一側;
> 也可以對二側的所有點進行記錄。
```graphviz
digraph {
rankdir=LR;
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
C [label="c2", style=filled, fillcolor=lightgray, color="#FF0000"];
D [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
G [label="c1", style=filled, fillcolor=lightgray, color="#FF0000"];
}
S0 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=o];
B :e -> C [dir=none, style=bold, color="#B000B0"];
A :ne -> D [dir=back, arrowtail=o];
D :e -> E [dir=none, style=bold, color="#B000B0"];
E -> F [dir=back, arrowtail=o];
F -> G [dir=none, style=bold, color="#B000B0"];
C :n -> G [style=dashed, arrowhead=curve];
E -> G [style=dashed, arrowhead=curve];
E -> C [style=dashed, arrowhead=curve];
G :s -> C [style=dashed, arrowhead=curve];
A :e -> C [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
}
```
雖然花上的奇點都變為偶點,實際上還是需要知道它是哪種情況,
才能決定要不要繞花,或是照著原本的路徑回去就好。
<hr class="dotted">
```graphviz
digraph {
rankdir=LR;
H [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
I [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
J [label="", color="#0000FF"];
A -> H [dir=back, arrowtail=o];
H -> I [dir=none, style=bold, color="#B000B0"];
I -> J [dir=back, arrowtail=o];
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=15;
labelloc=t;
labeljust=l;
A [shape=doublecircle, label="base", style=filled, fillcolor=lightgray, color="#FFA500"];
}
S0 -> A [dir=none, style=bold, color="#B000B0"];
}
```
<hr class="thin">
先拆花。
```graphviz
digraph {
rankdir=LR;
H [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
I [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
J [label="", color="#0000FF"];
D -> H [dir=back, arrowtail=o];
H -> I [dir=none, style=bold, color="#B000B0"];
I -> J [dir=back, arrowtail=o];
J -> G [style=invis];
I -> F [style=invis];
H -> E [style=invis];
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
C [label="c2", style=filled, fillcolor=lightgray, color="#FF0000"];
D [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
G [label="c1", style=filled, fillcolor=lightgray, color="#FF0000"];
}
S0 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=o];
B :e -> C [dir=none, style=bold, color="#B000B0"];
A :ne -> D [dir=back, arrowtail=o];
D :e -> E [dir=none, style=bold, color="#B000B0"];
E -> F [dir=back, arrowtail=o];
F -> G [dir=none, style=bold, color="#B000B0"];
C -> G [style=dashed, dir=none];
A :e -> C [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
}
```
<hr class="thin">
到花之前正常進行擴增。
```graphviz
digraph {
rankdir=LR;
H [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
I [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
J [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
D -> H [dir=none, style=bold, color="#B000B0"];
H -> I [dir=none];
I -> J [dir=none, style=bold, color="#B000B0"];
J -> G [style=invis];
I -> F [style=invis];
H -> E [style=invis];
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
C [label="c2", style=filled, fillcolor=lightgray, color="#FF0000"];
D [label="m[y]", style=filled, fillcolor=lightgray, color="#FFA500"];
E [label="y", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
G [label="c1", style=filled, fillcolor=lightgray, color="#FF0000"];
}
S0 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=o];
B :e -> C [dir=none, style=bold, color="#B000B0"];
A :ne -> D [dir=back, arrowtail=o];
D :e -> E [dir=none, style=bold, color="#B000B0"];
E -> F [dir=back, arrowtail=o];
F -> G [dir=none, style=bold, color="#B000B0"];
C -> G [style=dashed, dir=none];
A :e -> C [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
}
```
<hr class="thin">
進入花的第一條匹配邊一定要變為非匹配邊。
```graphviz
digraph {
rankdir=LR;
H [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
I [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
J [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
D -> H [dir=none, style=bold, color="#B000B0"];
H -> I [dir=none];
I -> J [dir=none, style=bold, color="#B000B0"];
J -> G [style=invis];
I -> F [style=invis];
H -> E [style=invis];
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
C [label="c2", style=filled, fillcolor=lightgray, color="#FF0000"];
D [label="m[y]", style=filled, fillcolor=lightgray, color="#FF0000"];
E [label="y", color="#0080FF"];
F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
G [label="c1", style=filled, fillcolor=lightgray, color="#FF0000"];
}
S0 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=o];
B :e -> C [dir=none, style=bold, color="#B000B0"];
A :ne -> D [dir=back, arrowtail=o];
D :e -> E [dir=none];
E -> F [dir=back, arrowtail=o];
F -> G [dir=none, style=bold, color="#B000B0"];
C -> G [style=dashed, dir=none];
A :e -> C [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
}
```
<hr class="thin">
遇到花上原本的奇點,進行繞花;cross edge 會變為匹配邊。
> 前面是奇點暫時變為了偶點;
> 而現在這一側,奇點真正成為了偶點,偶點反而成為了奇點,
> 而對面一側,奇點則回歸奇點的身份。
```graphviz
digraph {
rankdir=LR;
H [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
I [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
J [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
D -> H [dir=none, style=bold, color="#B000B0"];
H -> I [dir=none];
I -> J [dir=none, style=bold, color="#B000B0"];
J -> G [style=invis];
I -> F [style=invis];
H -> E [style=invis];
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="p[c2]", color="#0000FF"];
C [label="c2", style=filled, fillcolor=lightgray, color="#FF0000"];
D [label="p[y]", style=filled, fillcolor=lightgray, color="#FF0000"];
E [label="y", color="#0080FF"];
F [label="p[c1]", color="#FF0000"];
G [label="c1", style=filled, fillcolor=lightgray, color="#0080FF"];
}
S0 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=o];
B :e -> C [dir=none];
A :ne -> D [style=invis];
D :e -> E [dir=none];
E -> F [dir=back, arrowtail=o];
F -> G [dir=none];
C -> G [style="dashed, bold", dir=none, color="#B000B0"];
A :e -> C [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
}
```
<hr class="thin">
將 cross edge 二側的交錯路徑分開,得到 $y\to p[c1]$ 與 $p[c2]\to root$ 二條可擴增路徑。
```graphviz
digraph {
rankdir=LR;
H [style=invis];
I [style=invis];
J [style=invis];
D -> H [style=invis];
H -> I [style=invis];
I -> J [style=invis];
J -> G [style=invis];
I -> F [style=invis];
H -> E [style=invis];
S0 [label="root", color="#FF0000"];
S1 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
S2 [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
S3 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="p[c2]", color="#0000FF"];
C [style=invis];
D [label="p[y]", style=dotted];
E [label="y", color="#0080FF"];
F [label="p[c1]", color="#FF0000"];
G [style=invis];
}
S0 -> S1 [dir=back, arrowtail=o];
S1 -> S2 [dir=none, style=bold, color="#B000B0"];
S2 -> S3 [dir=back, arrowtail=o];
S3 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=o];
B :e -> C [style=invis];
A :ne -> D [style=invis];
D :e -> E [dir=none, style=dotted];
E -> F [dir=back, arrowtail=o];
F -> G [style=invis];
C -> G [style=invis];
A :e -> C [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
}
```
<hr class="thin">
將 $y\to p[c1]$ 的起點看為 $y$,變成 $p[c1]\to y$,
就得到二條與原先 BFS 搜尋路徑方向一致的可擴增路徑。
```graphviz
digraph {
rankdir=LR;
H [style=invis];
I [style=invis];
J [style=invis];
D -> H [style=invis];
H -> I [style=invis];
I -> J [style=invis];
J -> G [style=invis];
I -> F [style=invis];
H -> E [style=invis];
S0 [label="root", color="#FF0000"];
S1 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
S2 [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
S3 [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="p[c2]", color="#0000FF"];
C [style=invis];
D [label="p[y]", style=dotted];
E [label="y", color="#FF0000"];
F [label="p[c1]", color="#0000FF"];
G [style=invis];
}
S0 -> S1 [dir=back, arrowtail=o];
S1 -> S2 [dir=none, style=bold, color="#B000B0"];
S2 -> S3 [dir=back, arrowtail=o];
S3 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=o];
B :e -> C [style=invis];
A :ne -> D [style=invis];
D :e -> E [dir=none, style=dotted];
E -> F [dir=back, arrowtail=o];
F -> G [style=invis];
C -> G [style=invis];
A :e -> C [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
}
```
<hr class="thin">
> 花中有花呢?
>
> ```graphviz
> digraph {
> rankdir=LR;
>
> H [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> I [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> J [label="", color="#0000FF"];
>
> D -> H [dir=back, arrowtail=o];
> H -> I [dir=none, style=bold, color="#B000B0"];
> I -> J [dir=back, arrowtail=o];
>
> J -> G [style=invis];
> I -> F [style=invis];
> H -> E [style=invis];
>
> S0 [style=invis];
>
> subgraph cluster_blossom {
> label="Blossom";
> fontsize=20;
> labelloc=t;
> labeljust=l;
>
> A [shape=doublecircle, label="base", style=filled, fillcolor=lightgray, color="#FFA500"];
> B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
> C [shape=doublecircle, label="c2", style=filled, fillcolor=lightgray, color="#FFA500"];
> D [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
> E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
> G [label="c1", style=filled, fillcolor=lightgray, color="#FF0000"];
> }
>
> S0 -> A [dir=none, style=bold, color="#B000B0"];
>
> A :se -> B [dir=back, arrowtail=o];
> B :e -> C [dir=none, style=bold, color="#B000B0"];
> A :ne -> D [dir=back, arrowtail=o];
> D :e -> E [dir=none, style=bold, color="#B000B0"];
> E -> F [dir=back, arrowtail=o];
> F -> G [dir=none, style=bold, color="#B000B0"];
>
> C -> G [style=dashed, dir=none];
>
> A :e -> C [style=invis];
> A :e -> E [style=invis];
> A :e -> F [style=invis];
> A :e -> G [style=invis];
> }
> ```
>
> 一樣進行拆花。
>
> ```graphviz
> digraph {
> rankdir=LR;
>
> H [label="", style=filled, fillcolor=lightgray, color="#0000FF"];
> I [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> J [label="", color="#0000FF"];
>
> D -> H [dir=back, arrowtail=o];
> H -> I [dir=none, style=bold, color="#B000B0"];
> I -> J [dir=back, arrowtail=o];
>
> J -> G [style=invis];
> I -> F [style=invis];
> H -> E [style=invis];
>
> S0 [style=invis];
>
> subgraph cluster_blossom {
> label="Blossom";
> fontsize=20;
> labelloc=t;
> labeljust=l;
>
> subgraph cluster_blossom2 {
> label="Blossom";
> fontsize=10;
> labelloc=t;
> labeljust=l;
>
> A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
> A1 [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
> A2 [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> A3 [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
> A4 [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
>
> A :se -> A1 [dir=back, arrowtail=o];
> A1 -> A2 [dir=none, style=bold, color="#B000B0"];
> A :ne -> A3 [dir=back, arrowtail=o];
> A3 -> A4 [dir=none, style=bold, color="#B000B0"];
> A2 -> A4 [style=dashed, dir=none];
> }
>
> B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
> subgraph cluster_blossom3 {
> label="Blossom";
> fontsize=10;
> labelloc=t;
> labeljust=l;
>
> C [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
> C1 [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
> C2 [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> C3 [label="c2", style=filled, fillcolor=lightgray, color="#FFA500"];
> C4 [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
>
> C :se -> C1 [dir=back, arrowtail=o];
> C1 :e -> C2 [dir=none, style=bold, color="#B000B0"];
> C :ne -> C3 [dir=back, arrowtail=o];
> C3 :e -> C4 [dir=none, style=bold, color="#B000B0"];
> C2 -> C4 [style=dashed, dir=none];
> }
> D [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
> E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
> F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
> G [label="c1", style=filled, fillcolor=lightgray, color="#FF0000"];
> }
>
> S0 -> A [dir=none, style=bold, color="#B000B0"];
>
> A4 :se -> B [dir=back, arrowtail=o];
> B :e -> C [dir=none, style=bold, color="#B000B0"];
> A3 :ne -> D [dir=back, arrowtail=o];
> D -> E [dir=none, style=bold, color="#B000B0"];
> E -> F [dir=back, arrowtail=o];
> F -> G [dir=none, style=bold, color="#B000B0"];
>
> C3 -> G [style=dashed, dir=none];
>
> A :e -> C [style=invis];
> A :e -> E [style=invis];
> A :e -> F [style=invis];
> A :e -> G [style=invis];
> }
> ```
>
> **$c1, c2$ 要是真實的點,才能拆花,進到花中。**
<hr class="dotted">
擴增時,從終點往起點回溯,
奇點到偶點是非匹配邊,一定是照著原本搜尋的反方向;
**偶點到奇點**則不然,**需走配對邊**(`m[]`),不一定是父節點(`p[]`),
否則**繞花的地方會出錯**。
> 回溯之時,會用到原本的配對邊,
> 因此在回溯完成後,也就是遞迴返回時,
> 再修改配對邊,較容易實作。
**縮花**並不是真的得生成一個新的點,只是**透過並查集,在某些地方將花視為一個整體**;
**拆花**也不是要把它分開,而**是不將花看作一個整體而已**。
```cpp!
// Edmonds' Blossom Algorithm - O(VEa(V))
class Matching {
int label{0};
vector<int> p{}, d{}, a{}, c1{}, c2{}, m{};
// (alternating tree), (unvisited: -1, even: 0, odd: 1), (auxiliary for lca), (cross edge)
/* DSU */
vector<int> dsu_p{}, dsu_sz{}, dsu_b{};
void dsu_reset() {
fill(dsu_p.begin(), dsu_p.end(), -1);
fill(dsu_sz.begin(), dsu_sz.end(), 1);
iota(dsu_b.begin(), dsu_b.end(), 0);
}
int find(int x) { // collapsing find
return dsu_p[x] == -1 ? x : dsu_p[x] = find(dsu_p[x]);
}
int base(int x) { return dsu_b[find(x)]; }
void contract(int x, int y) { // weighted union
auto rx{find(x)}, ry{find(y)};
if (rx == ry) return ;
auto b{dsu_b[rx]}; // treat x's base as new base
if (dsu_sz[rx] < dsu_sz[ry]) swap(rx, ry);
dsu_p[ry] = rx, dsu_sz[rx] += dsu_sz[ry], dsu_b[rx] = b;
}
/*******/
queue<int> qu{}; // only even vertices
void handle_one_side(int x, int y, int b) {
for (int v{base(x)}; v != b; v = base(p[v])) {
c1[v] = x, c2[v] = y, contract(b, v);
if (d[v] == 1) qu.push(v); // odd vertex -> even vertex
}
}
int lca(int u, int v) {
++label; // use next number in order not to clear a
while (true) {
if (a[u] == label) return u;
a[u] = label;
if (p[u] != -1) u = base(p[u]);
swap(u, v);
}
}
void augment(int r, int y) {
if (r == y) return ;
if (d[y] == 0) { // even vertex -> odd vertex
// check d[y] == 0 instead of d[p[y]] == 1, so (m[y], y) is in the blossom
augment(m[y], m[c1[y]]);
augment(r, m[c2[y]]);
m[c1[y]] = c2[y], m[c2[y]] = c1[y];
} else {
int x{p[y]};
augment(r, m[x]);
m[x] = y, m[y] = x;
}
}
bool bfs(const vector<vector<int>>& adj, int r) {
dsu_reset();
fill(d.begin(), d.end(), -1);
qu = {}, qu.push(r), d[r] = 0;
while (!qu.empty()) {
int x{qu.front()}; qu.pop();
for (auto& y : adj[x]) {
if (base(x) == base(y)) continue;
if (d[y] == -1) {
p[y] = x, d[y] = 1;
if (m[y] == -1) { // augmenting path
augment(-1, y);
return true;
} else {
p[m[y]] = y, d[m[y]] = 0;
qu.push(m[y]);
}
} else if (d[base(y)] == 0) { // blossom
int b{lca(base(x), base(y))};
handle_one_side(x, y, b);
handle_one_side(y, x, b);
}
}
}
return false;
}
public:
vector<int> operator()(const vector<vector<int>>& adj) {
label = 0, p.assign(adj.size(), -1), d.resize(adj.size()), a.resize(adj.size()), c1.resize(adj.size()), c2.resize(adj.size()), m.assign(adj.size(), -1);
dsu_p.resize(adj.size()), dsu_sz.resize(adj.size()), dsu_b.resize(adj.size());
int c{0};
for (int v{0}; v < adj.size(); ++v)
if (m[v] == -1 && bfs(adj, v)) ++c;
return move(m);
}
};
```
因為透過並查集進行縮花,每次找到節點所在的集合,或是合併節點,複雜度為 $O(\alpha(|V|))$。
> 如果是在縮花後的圖上操作,因為 `base()`,複雜度就要乘上 $O(\alpha(|V|))$。
<hr class="thin">
縮花時,找 LCA,分別讓 cross edge 的二個點每次各走一步,
次數最多不超過花上的節點數,剩餘縮花的運算量也等於花上的節點數;
每個節點最多被縮花一次,因此所有**縮花的總複雜度為 $O(|V|\alpha(|V|))$**。
<hr class="thin">
擴增時,可擴增路徑長度為 $O(|V|)$,複雜度為 $O(|V|)$。
<hr class="thin">
每次 BFS,不包含縮花與擴增,複雜度為 $O(|V|+|E|\alpha(|V|))$。
<hr class="thin">
總共進行 $O(|V|)$ 次,**演算法複雜度為 $O(|V||E|\alpha(|V|))$**。
<hr class="dashed">
### 另一種實現方式:bulb::bulb:
匹配邊本身就是雙向連通,偶點的前一個點就是對應的奇點,
因此可以把鏈結(link)拿去紀錄花上的反向邊,
透過匹配邊與鏈結,讓整個花不管順時針逆時針都可以正常經過。
> base 的部分沒辦法這樣做,因為有左右二條邊。
```graphviz
digraph {
rankdir=LR;
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
C [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
D [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
G [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
}
S0 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=o];
B :e -> C [dir=none, style=bold, color="#B000B0"];
A :ne -> D [dir=back, arrowtail=o];
D :e -> E [dir=none, style=bold, color="#B000B0"];
E -> F [dir=back, arrowtail=o];
E -> F [arrowhead=o];
F -> G [dir=none, style=bold, color="#B000B0"];
C -> G [style=dashed, dir=back, arrowtail=o];
C -> G [style=dashed, arrowhead=o];
A :e -> C [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
}
```
<hr class="thin">
擴增時要拆花,就會變得很簡單,只要隨著鏈結走就可以了。
```graphviz
digraph {
rankdir=LR;
H [label="", color="#0000FF"];
H -> D [arrowhead=vee];
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
C [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
D [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
G [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
}
S0 -> A [dir=back, arrowtail=vee, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=vee];
B :e -> C [dir=back, arrowtail=vee, style=bold, color="#B000B0"];
A :ne -> D [dir=back, arrowtail=o];
D :e -> E [arrowhead=vee, style=bold, color="#B000B0"];
E -> F [dir=back, arrowtail=o];
E -> F [arrowhead=vee];
F -> G [arrowhead=vee, style=bold, color="#B000B0"];
C -> G [style=dashed, dir=back, arrowtail=vee];
C -> G [style=dashed, arrowhead=o];
A :e -> C [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
}
```
<hr class="thin">
base 沒辦法直接記錄鏈結,但會有從 base 往非匹配邊走的情況嗎?
> 其實有喔!花中有花的時候。
```graphviz
digraph {
rankdir=LR;
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
D [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
G [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
subgraph cluster_blossom2 {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
a [shape=doublecircle, label="base", style=filled, fillcolor=lightgray, color="#FFA500"];
}
}
S0 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=o];
B :e -> a [dir=none, style=bold, color="#B000B0"];
A :ne -> D [dir=back, arrowtail=o];
D :e -> E [dir=none, style=bold, color="#B000B0"];
E -> F [dir=back, arrowtail=o];
F -> G [dir=none, style=bold, color="#B000B0"];
a -> G [style=dashed, dir=none];
A :e -> a [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
}
```
情況一:
> 因為直接從 base 連出去,所以不會進到花中。
```graphviz
digraph {
rankdir=LR;
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
D [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
G [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
subgraph cluster_blossom2 {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
a [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
b [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
c [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
d [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
e [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
}
}
a :se -> b [dir=back, arrowtail=o];
b :e -> c [dir=none, style=bold, color="#B000B0"];
a :e -> d [dir=back, arrowtail=o];
d :e -> e [dir=none, style=bold, color="#B000B0"];
c -> e [arrowhead=o, style=dashed];
c -> e [dir=back, arrowtail=o, style=dashed];
a :e -> c [style=invis];
a :e -> e [style=invis];
a :e -> d [style=invis];
S0 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=o];
B :e -> a [dir=none, style=bold, color="#B000B0"];
A :ne -> D [dir=back, arrowtail=o];
D :e -> E [dir=none, style=bold, color="#B000B0"];
E -> F [dir=back, arrowtail=o];
E -> F [arrowhead=o];
F -> G [dir=none, style=bold, color="#B000B0"];
a -> G [style=dashed, arrowhead=o];
a -> G [style=dashed, dir=back, arrowtail=o];
A :e -> a [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
H [label="", color="#0000FF"];
H -> B [arrowhead=o];
}
```
情況二:
當花再次被縮花時,就可以知道鏈結該往哪邊走。
> 取決於偶點到下個奇點的邊,或是偶點到偶點的 cross edge,到底是屬於花中的哪個點。
```graphviz
digraph {
rankdir=LR;
S0 [style=invis];
subgraph cluster_blossom {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
A [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
B [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
D [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
E [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
F [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
G [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
subgraph cluster_blossom2 {
label="Blossom";
fontsize=20;
labelloc=t;
labeljust=l;
a [label="base", style=filled, fillcolor=lightgray, color="#FF0000"];
b [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
c [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
d [label="", style=filled, fillcolor=lightgray, color="#FFA500"];
e [label="", style=filled, fillcolor=lightgray, color="#FF0000"];
}
}
a :e -> b [dir=back, arrowtail=o];
a :se -> b [arrowhead=o, style=bold, color="#FFD000"];
b :e -> c [dir=none, style=bold, color="#B000B0"];
a :ne -> d [dir=back, arrowtail=o];
d :e -> e [dir=none, style=bold, color="#B000B0"];
c -> e [arrowhead=o, style=dashed];
c -> e [dir=back, arrowtail=o, style=dashed];
a :e -> c [style=invis];
a :e -> e [style=invis];
a :e -> d [style=invis];
S0 -> A [dir=none, style=bold, color="#B000B0"];
A :se -> B [dir=back, arrowtail=o];
B :e -> a [dir=none, style=bold, color="#B000B0"];
A :ne -> D [dir=back, arrowtail=o];
D :e -> E [dir=none, style=bold, color="#B000B0"];
E -> F [dir=back, arrowtail=o];
E -> F [arrowhead=o];
F -> G [dir=none, style=bold, color="#B000B0"];
d -> G [style=dashed, arrowhead=o];
d -> G [style=dashed, dir=back, arrowtail=o];
A :e -> a [style=invis];
A :e -> E [style=invis];
A :e -> F [style=invis];
A :e -> G [style=invis];
H [label="", color="#0000FF"];
H -> B [arrowhead=o];
}
```
<hr class="thin">
這樣的問題在於,每次縮花調整鏈結時,需要拆花,
每次縮花複雜度就要 $O(|V|)$,最多縮花 $O(|V|)$ 次,
縮花總複雜度變為 $O(|V|^2)$,因此演算法複雜度為 $O(|V|^3)$。
> 不過實作可能簡單一點。
<hr class="dashed">
一般圖最大匹配,透過更複雜的 **Micali-Vazirani Algorithm**,可以達到 **$E\sqrt V$** 的複雜度。
---
# 最大權匹配(Maximum Weight Matching)
如果現在每條邊都有一個權重 $w$,匹配 $M$ 的權重則為 **$w(M)=\sum_{e\in M}w_e$**,
最大權匹配問題就是求一個權重最大的匹配。
<hr class="dashed">
## 二分圖最大權匹配(Maximum Weight Bipartite Matching)
### Hungarian Algorithm(Kuhn–Munkres Algorithm):bulb:
先考慮一個比較侷限的問題,在一個完全二分圖(complete bipartite graph)$K_{n,n}$ 上,求最大權完美匹配(Maximum weight perfect matching)。
> 完全二分圖:$X$ 中任意一點與 $Y$ 的所有點均有邊。
> $K_{n,n}$:$|X|=|Y|=n$ 的完全二分圖。
```graphviz
graph {
rankdir=LR;
subgraph cluster_X {
style=dashed;
label="X";
A [label="", fontcolor="#FF0000", style=filled];
B [label="", fontcolor="#FF0000", style=filled];
C [label="", fontcolor="#FF0000", style=filled];
}
subgraph cluster_Y {
style=dashed;
label="Y";
a[label="", fontcolor="#0000FF", style=filled];
b[label="", fontcolor="#0000FF", style=filled];
c[label="", fontcolor="#0000FF", style=filled];
}
A -- a [style=bold, color="#B000B0", label="8", fontcolor="#B000B0"];
A -- b;
A -- c;
B -- a;
B -- b [style=bold, color="#B000B0", label="5", fontcolor="#B000B0"];
B -- c;
C -- a;
C -- b;
C -- c [style=bold, color="#B000B0", label="7", fontcolor="#B000B0"];
}
```
先給每個節點一個重量標籤,如果重量標籤滿足 $lx_i+ly_j\ge w_{ij}\;\forall\; 1\le i,j\le n$,
稱它為二分圖的一個潛能(potential) $P$,潛能的大小為 $w(P)=\sum_{i=1}^{n}lx_i+\sum_{j=1}^{n}ly_j$。
<hr class="thin">
:::danger
(引理 2.)完全二分圖 $K_{n,n}$ 中,任意一個完美匹配 $M$ 與任意一個潛能 $P$,滿足 $w(M)\le w(P)$。
等式成立若且唯若 $w_{ij}=lx_i+ly_j\;\forall\; (i,j)\in M$,而此時的 $M$ 為最大完美匹配,$P$ 為最小潛能。
:::
所有 $M$ 中的邊 $(i,j)$,都有 $w_{ij}\le lx_i+ly_j$,
而 $M$ 是完美匹配,所以每個點都恰好是一條匹配邊的端點,
因此 $w(M)=\sum_{(i,j)\in M}w_{ij}\le\sum_{(i,j)\in M}lx_i+ly_j=P$;
若等式成立,則所有用到的條件式都必須相等,才會得到 $w(M)=w(P)$。
<hr class="thin">
:::danger
(定理 3.)一個完全二分圖 $K_{n,n}$ 中,最大完美匹配 $M$ 與最小潛能 $P$,滿足 $w(M)=w(P)$。
:::
根據引理 2.,如果存在某個 $M$ 與 $P$,使得 $w(M)=w(P)$,則它們分別是最大完美匹配與最小潛能;
因此需要證明,對任意完全二分圖 $K_{n,n}$,都存在 $M$ 與 $P$,使得 $w(M)=w(P)$。
> 這邊不討論過多的數學定理,透過演算法來證明其存在性。
<hr class="thin">
```graphviz
graph {
rankdir=LR;
A [label="5", fontcolor="#FF0000"];
B [label="2", fontcolor="#0000FF"];
A -- B [label="7"];
}
```
一條邊如果滿足$w_{ij}=lx_i+ly_j$,稱為「等邊」(equaility edge)。
因為要找全部是等邊的匹配,所以一開始就只找等邊匹配邊,
而可擴增路徑上一定都要是等邊,藉此慢慢增加等邊匹配邊的數量。
<hr class="dotted">
#### 演算法
1. 依序以每個 $X$ 非匹配點為起點
2. 透過等邊尋找可擴增路徑
* 如果找到可擴增路徑,進行擴增
* 如果找不到可擴增路徑,並且無法前進時,進入步驟 $3$
3. 調整重量標籤,以增加新的等邊,並回到步驟 $2$ 繼續尋找
因為任何時候的匹配邊一定是等邊,當路徑無法前進時,一定是停在某個偶點($X$ 的點),
```graphviz
graph {
rankdir=LR;
A [label="5", fontcolor="#FF0000"];
B [label="2", fontcolor="#0000FF"];
C [label="3", fontcolor="#FF0000"];
D [label="6", fontcolor="#0000FF"];
A -- B [label="7"];
B -- C [style=bold, color="#B000B0", label="5", fontcolor="#B000B0"];
C -- D [style=dashed, label="8"];
}
```
<hr class="thin">
將交錯樹上 $X$ 的點都減掉 $d$,$Y$ 點都加上 $d$;
對於一條邊 $(i,j)\mid i\in X,j\in Y$,
如果 $i,j$ 都在樹上,則 $lx_i+ly_j$ 不變,所以樹上還是都是等邊;
如果 $i,j$ 都在樹外,$lx_i+ly_j$ 不變;
如果 **$i$ 在樹外,$j$ 在樹上**,$lx_i+ly_j$ 變大,還是維持大於等於的性質,
並且**這樣的邊不是匹配邊**,所以不影響目前的匹配邊都是等邊的事實;
如果 **$i$ 在樹上,$j$ 在樹外**,$lx_i+ly_j$ 變為 $lx_i+ly_j-d$,有可能**變為等邊**,
```graphviz
graph {
rankdir=LR;
A [label="5-d", fontcolor="#FF0000"];
B [label="2+d", fontcolor="#0000FF"];
C [label="3-d", fontcolor="#FF0000"];
D [label="6", fontcolor="#0000FF"];
E [label="8", fontcolor="#0000FF"];
F [label="4", fontcolor="#0000FF"];
A -- B [label="7"];
B -- C [style=bold, color="#B000B0", label="5", fontcolor="#B000B0"];
C -- D [style=dashed, label="8"];
C -- E [style=dashed, label="9"];
A -- F [style=dashed, label="3"];
}
```
<hr class="thin">
為了讓重量標籤還是一個潛能(維持大於等於的性質),
令 $d$ 為所有 $i$ 在樹上,$j$ 在樹外的邊中,$lx_i+ly_j-w_{ij}$ 的最小值,
而每次調整重量標籤可以增加一條(以上)的等邊。
> **$d=\min_{i\in tree,j\notin tree}(lx_i+ly_j-w_{ij})$**
```graphviz
graph {
rankdir=LR;
A [label="4", fontcolor="#FF0000"];
B [label="3", fontcolor="#0000FF"];
C [label="2", fontcolor="#FF0000"];
D [label="6", fontcolor="#0000FF"];
E [label="8", fontcolor="#0000FF"];
F [label="4", fontcolor="#0000FF"];
A -- B [label="7"];
B -- C [style=bold, color="#B000B0", label="5", fontcolor="#B000B0"];
C -- D [style=dashed, label="8"];
C -- E [style=dashed, label="9"];
A -- F [style=dashed, label="3"];
}
```
<hr class="thin">
每次重新調整重量標籤,至少會有一條新的等邊,
因此在有限步內,一樣會搜尋完所有路徑。
> 雖然加了只能透過等邊搜尋的條件,不代表可擴增路徑會消失,
> 只是要等到重量標籤被調整後,才會慢慢形成等邊的可擴增路徑。
因為是完全二分圖,最終一定會得到一個**由等邊構成的完美匹配**。
<hr class="dotted">
每次重新調整完重量標籤後,都直接重新搜尋,得到一個複雜度較差的實作。
```cpp!
class WeightedBipartiteMatching {
int n;
vector<long long> lx, ly;
vector<bool> vx, vy;
vector<int> mx, my;
bool dfs(const vector<vector<long long>>& w, int x) {
vx[x] = true;
for (int y{0}; y < n; ++y)
if (!vy[y] && lx[x] + ly[y] == w[x][y]) {
vy[y] = true;
if (my[y] == -1 || dfs(w, my[y])) {
mx[x] = y, my[y] = x;
return true;
}
}
return false;
}
void relabel(const vector<vector<long long>>& w) {
long long d{numeric_limits<long long>::max()};
for (int x{0}; x < n; ++x)
if (vx[x]) {
for (int y{0}; y < n; ++y)
if (!vy[y]) d = min(d, lx[x] + ly[y] - w[x][y]);
}
for (int x{0}; x < n; ++x) if (vx[x]) lx[x] -= d;
for (int y{0}; y < n; ++y) if (vy[y]) ly[y] += d;
}
public:
pair<vector<int>, vector<int>> operator()(const vector<vector<long long>>& w) {
n = w.size(), lx.assign(n, 0), ly.assign(n, 0), vx.resize(n), vy.resize(n), mx.assign(n, -1), my.assign(n, -1);
for (int i{0}; i < n; ++i)
for (int j{0}; j < n; ++j)
lx[i] = max(lx[i], w[i][j]);
for (int x{0}; x < n; ++x) {
while (true) {
fill(vx.begin(), vx.end(), false);
fill(vy.begin(), vy.end(), false);
if (dfs(w, x)) break; // augmenting path with equaility edge
relabel(w);
}
}
return {move(mx), move(my)};
}
};
```
總共有 $V$ 個點,每個點最多重新標記 $O(|V|)$ 次後找到可擴增路徑,
DFS 複雜度為 $O(|E|)=O(|V|^2)$,重新標記複雜度為 $O(|V|^2)$,
**演算法複雜度為 $O(|V|^4)$**。
<hr class="dotted">
一開始每個 $i$ 與 $j$ 都在樹外,因為要計算的是,
「$i$ 在樹上,$j$ 在樹外的邊中,$lx_i+ly_j-w_{ij}$」的最小值,
每次拜訪到 $i$,所有在樹外的 $j$ 都符合條件;
而每次拜訪到 $j$ 時,所有 $j$ 的邊都不再在條件範圍中;
因此我們可以**分別對每個 $j$,紀錄其最小值,還有到底是與哪個 $i$ 的邊達到最小值的**。
> 每次拜訪 $i$ 時,就要更新所有 $j$ 紀錄的值;
> 每當 $j$ 被拜訪時,就不再考慮這個 $j$ 的邊。
```cpp!
// Hungarian Algorithm - O(V^3)
class WeightedBipartiteMatching {
int n;
vector<long long> lx{}, ly{};
vector<bool> vx{}, vy{};
queue<int> qu{}; // only X's vertices
vector<int> py{};
vector<long long> dy{}; // smallest (lx[x] + ly[y] - w[x][y])
vector<int> pdy{}; // & which x
vector<int> mx{}, my{};
void relax(const vector<vector<long long>>& w, int x) {
for (int y{0}; y < n; ++y)
if (lx[x] + ly[y] - w[x][y] < dy[y])
dy[y] = lx[x] + ly[y] - w[x][y], pdy[y] = x;
}
void augment(int y) {
while (y != -1) {
int x{py[y]}, yy{mx[x]};
mx[x] = y, my[y] = x;
y = yy;
}
}
bool bfs(const vector<vector<long long>>& w) {
while (!qu.empty()) {
int x{qu.front()}; qu.pop();
for (int y{0}; y < n; ++y) {
if (!vy[y] && lx[x] + ly[y] == w[x][y]) {
vy[y] = true, py[y] = x;
if (my[y] == -1) return augment(y), true;
int xx{my[y]};
qu.push(x), vx[xx] = true, relax(w, xx);
}
}
}
return false;
}
void relabel() {
long long d{numeric_limits<long long>::max()};
for (int y{0}; y < n; ++y) if (!vy[y]) d = min(d, dy[y]);
for (int x{0}; x < n; ++x) if (vx[x]) lx[x] -= d;
for (int y{0}; y < n; ++y) if (vy[y]) ly[y] += d;
for (int y{0}; y < n; ++y) if (!vy[y]) dy[y] -= d;
}
bool expand(const vector<vector<long long>>& w) { // expand one layer with new equality edges
for (int y{0}; y < n; ++y) {
if (!vy[y] && dy[y] == 0) {
vy[y] = true, py[y] = pdy[y];
if (my[y] == -1) return augment(y), true;
int xx{my[y]};
qu.push(xx), vx[xx] = true, relax(w, xx);
}
}
return false;
}
public:
pair<vector<int>, vector<int>> operator()(const vector<vector<long long>>& w) {
n = w.size(), lx.assign(n, 0), ly.assign(n, 0), vx.resize(n), vy.resize(n), py.resize(n), dy.resize(n), pdy.resize(n), mx.assign(n, -1), my.assign(n, -1);
for (int i{0}; i < n; ++i)
for (int j{0}; j < n; ++j)
lx[i] = max(lx[i], w[i][j]);
for (int x{0}; x < n; ++x) {
fill(vx.begin(), vx.end(), false);
fill(vy.begin(), vy.end(), false);
fill(dy.begin(), dy.end(), numeric_limits<long long>::max());
qu = {}, qu.push(x), vx[x] = true, relax(w, x);
while (!bfs(w)) {
relabel();
if (expand(w)) break;
}
}
return {move(mx), move(my)};
}
};
```
<hr class="dotted">
#### 二分圖最大權匹配(Maximum Weight Bipartite Matching)
將 $X$ 與 $Y$ 的點數量增加到一樣,不用新增邊,
接著將所有**不存在以及負權重的邊,設定為 $0$**,
就可以將問題轉化為完全二分圖 $K_{n,n}$ 的最大權完美匹配了。
> 權重設定為 $0$ 的意義是代表可以不匹配。
<hr class="dotted">
#### 二分圖最大權最大匹配(Maximum Weight Maximum Cardinality Bipartite Matching)
二分圖最大權最大匹配,是在最大匹配數量下,權重最大的匹配。
將所有**不存在的邊設為 $0$**,並將每條**原有的邊加上一個夠大的數字**,
就一樣可以將問題轉化,因為當每條邊都被加上一個很大的數字後,
除了達到最大數量的匹配以外,其他匹配的權重都會小得多。
> 令邊的權重絕對值最大值為 $W$,$2nW+1$ 就是一個足夠大的值,
> > 任意匹配 $M'$,$-nW\le w(M')\le nW$,因此二個匹配權重差距至多 $2nW$。
>
> 因為只要某個匹配少了一條匹配邊,其他匹配邊原有權重再怎麼取,
> 還是會比達到最大匹配數量的匹配小。
<hr class="dotted">
#### 二分圖最小權最大匹配(Minimum Weight Maximum Cardinality Bipartite Matching)
要求最小權的話,為了套用最大權的匈牙利演算法,
可以將權重進行變號,這樣最小權就會變為最大權了。
至於最大匹配的部分,另一種方式是將不存在的邊設為一個極大的負數,
這樣就會盡量取到最大匹配,也就是減少取不存在的邊作為匹配邊。
> 亦或是可以思考為,為了求最小權最大匹配,我們應該是先將不存在的邊設為一個極大的正數,
> 但是為了套用最大權的匈牙利演算法,這些極大的正數也要跟著進行變號。
<hr class="dotted">
匈牙利演算法,透過對完全二分圖求解,確保二分圖一定有著完美匹配;
**相關問題,可利用上述一些技巧,進行問題轉化,得到答案後再行調整**。
<hr class="dashed">
## 一般圖最大權匹配(Maximum Weight Matching):bulb::bulb:
Edmonds' Blossom Algorithm 搭配 Hungarain Algorithm 的概念,
就可以解一般圖最大權匹配,不過因為花的出現,重量標籤處理上會更為複雜。
---
# References
* 張鎮華、蔡牧村(2020)。演算法觀點的圖論(修訂版)。
* 演算法筆記 - [Matching](http://web.ntnu.edu.tw/~algo/Matching.html)
* Wikipedia - [Hopcroft–Karp algorithm](https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm)
* Wikipedia - [Blossom algorithm](https://en.wikipedia.org/wiki/Blossom_algorithm)
* BRILLIANT - [Blossom Algorithm](https://brilliant.org/wiki/blossom-algorithm/)
* Wikipedia - [Hungarian algorithm](https://en.wikipedia.org/wiki/Hungarian_algorithm)
---
# 練習題
| Problem | Tags |
|:-:|:- |
| []() | |
<style>
hr.thin {
height: 0.5px;
}
hr.dotted {
border-top: dotted;
height: .0em;
color: #777777;
background-color: #ffffff;
}
hr.dashed {
border-top: dashed;
height: .0em;
color: #777777;
background-color: #ffffff;
}
/* Gradient transparent - color - transparent */
hr.gradient {
border: 0;
height: 1px;
background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
}
/* Flaired edges, by Tomas Theunissen */
hr.flaired {
overflow: visible;
height: 30px;
border-style: solid;
border-color: black;
border-width: 1px 0 0 0;
border-radius: 20px;
background-color: #ffffff;
}
hr.flaired:before { /* Not really supposed to work, but does */
display: block;
content: "";
height: 30px;
margin-top: -31px;
border-style: solid;
border-color: black;
border-width: 0 0 1px 0;
border-radius: 20px;
}
</style>