====== 一般图最大匹配 ======
===== 带花树算法 (Blossom Algorithm) =====
开花算法 (Blossom Algorithm,也被称为带花树)可以解决一般图最大匹配问题 (maximum cardinality matchings)。此算法由 Jack Edmonds 在 1961 年提出。经过一些修改后也可以解决一般图最大权匹配问题。此算法是第一个给出证明说最大匹配有多项式复杂度。
一般图匹配和二分图匹配 (bipartite matching) 不同的是,图可能存在奇环。
以下图为例,若直接取反(匹配边和未匹配边对调),会使得取反后的 $M$ 不合法,某些点会出现在两条匹配上,而问题就出在奇环。
下面考虑一般图的增广算法。从二分图的角度出发,每次枚举一个未匹配点,设出发点为根,标记为 **“o”**,接下来交错标记 **“o”** 和 **“i”**,不难发现 **“i”** 到 **“o”** 这段边是匹配边。
假设当前点是 $v$,相邻点为 $u$。
case 1: $u$ 未拜访过,当 $u$ 是未匹配点,则找到增广路径,否则从 $u$ 的配偶找增广路。
case 2: $u$ 已拜访过,遇到标记 **“o”** 代表需要 **缩花**,否则代表遇到偶环,跳过。
遇到偶环的情况,将他视为二分图解决,故可忽略。**缩花** 后,在新图中继续找增广路。
{{https://oi-wiki.org/topic/graph-matching/images/general-matching-2.png| general-matching-2}}
设原图为 $G$,**缩花** 后的图为 $G'$,我们只需要证明:
- 若 $G$ 存在增广路,$G'$ 也存在。
- 若 $G'$ 存在增广路,$G$ 也存在。
{{https://oi-wiki.org/topic/graph-matching/images/general-matching-3.png| general-matching-3}}
设非树边(形成环的那条边)为 $(u,v)$,定义花根 $h=LCA(u,v)$。奇环是交替的,有且仅有 $h$ 的两条邻边类型相同,都是非匹配边。那么进入 $h$ 的树边肯定是匹配边,环上除了 $h$ 以外其他点往环外的边都是非匹配边。
观察可知,从环外的边出去有两种情况,顺时针或逆时针。
{{https://oi-wiki.org/topic/graph-matching/images/general-matching-4.png| general-matching-4}}
于是 **缩花** 与 **不缩花** 都不影响正确性。
实际上找到 **花** 以后我们不需要真的 **缩花**,可以用数组记录每个点在以哪个点为根的那朵花中。
==== 复杂度分析 Complexity Analysis ====
每次找增广路,遍历所有边,遇到 **花** 会维护 **花** 上的点,$O(|E|^2)$。枚举所有未匹配点做增广路,总共 $O(|V||E|^2)$。
==== 代码 ====
// graph
template
class graph {
public:
struct edge {
int from;
int to;
T cost;
};
vector edges;
vector > g;
int n;
graph(int _n) : n(_n) { g.resize(n); }
virtual int add(int from, int to, T cost) = 0;
};
// undirectedgraph
template
class undirectedgraph : public graph {
public:
using graph::edges;
using graph::g;
using graph::n;
undirectedgraph(int _n) : graph(_n) {}
int add(int from, int to, T cost = 1) {
assert(0 <= from && from < n && 0 <= to && to < n);
int id = (int)edges.size();
g[from].push_back(id);
g[to].push_back(id);
edges.push_back({from, to, cost});
return id;
}
};
// blossom / find_max_unweighted_matching
template
vector find_max_unweighted_matching(const undirectedgraph &g) {
std::mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
vector match(g.n, -1); // 匹配
vector aux(g.n, -1); // 时间戳记
vector label(g.n); // "o" or "i"
vector orig(g.n); // 花根
vector parent(g.n, -1); // 父节点
queue q;
int aux_time = -1;
auto lca = [&](int v, int u) {
aux_time++;
while (true) {
if (v != -1) {
if (aux[v] == aux_time) { // 找到拜访过的点 也就是LCA
return v;
}
aux[v] = aux_time;
if (match[v] == -1) {
v = -1;
} else {
v = orig[parent[match[v]]]; // 以匹配点的父节点继续寻找
}
}
swap(v, u);
}
}; // lca
auto blossom = [&](int v, int u, int a) {
while (orig[v] != a) {
parent[v] = u;
u = match[v];
if (label[u] == 1) { // 初始点设为"o" 找增广路
label[u] = 0;
q.push(u);
}
orig[v] = orig[u] = a; // 缩花
v = parent[u];
}
}; // blossom
auto augment = [&](int v) {
while (v != -1) {
int pv = parent[v];
int next_v = match[pv];
match[v] = pv;
match[pv] = v;
v = next_v;
}
}; // augment
auto bfs = [&](int root) {
fill(label.begin(), label.end(), -1);
iota(orig.begin(), orig.end(), 0);
while (!q.empty()) {
q.pop();
}
q.push(root);
// 初始点设为 "o", 这里以"0"代替"o", "1"代替"i"
label[root] = 0;
while (!q.empty()) {
int v = q.front();
q.pop();
for (int id : g.g[v]) {
auto &e = g.edges[id];
int u = e.from ^ e.to ^ v;
if (label[u] == -1) { // 找到未拜访点
label[u] = 1; // 标记 "i"
parent[u] = v;
if (match[u] == -1) { // 找到未匹配点
augment(u); // 寻找增广路径
return true;
}
// 找到已匹配点 将与她匹配的点丢入queue 延伸交错树
label[match[u]] = 0;
q.push(match[u]);
continue;
} else if (label[u] == 0 && orig[v] != orig[u]) {
// 找到已拜访点 且标记同为"o" 代表找到"花"
int a = lca(orig[v], orig[u]);
// 找LCA 然后缩花
blossom(u, v, a);
blossom(v, u, a);
}
}
}
return false;
}; // bfs
auto greedy = [&]() {
vector order(g.n);
// 随机打乱 order
iota(order.begin(), order.end(), 0);
shuffle(order.begin(), order.end(), rng);
// 将可以匹配的点匹配
for (int i : order) {
if (match[i] == -1) {
for (auto id : g.g[i]) {
auto &e = g.edges[id];
int to = e.from ^ e.to ^ i;
if (match[to] == -1) {
match[i] = to;
match[to] = i;
break;
}
}
}
}
}; // greedy
// 一开始先随机匹配
greedy();
// 对未匹配点找增广路
for (int i = 0; i < g.n; i++) {
if (match[i] == -1) {
bfs(i);
}
}
return match;
}
===== 习题 =====
> [[https://uoj.ac/problem/79|UOJ #79. 一般图最大匹配]]
#include
using namespace std;
// graph
template
class graph {
public:
struct edge {
int from;
int to;
T cost;
};
vector edges;
vector > g;
int n;
graph(int _n) : n(_n) { g.resize(n); }
virtual int add(int from, int to, T cost) = 0;
};
// undirectedgraph
template
class undirectedgraph : public graph {
public:
using graph::edges;
using graph::g;
using graph::n;
undirectedgraph(int _n) : graph(_n) {}
int add(int from, int to, T cost = 1) {
assert(0 <= from && from < n && 0 <= to && to < n);
int id = (int)edges.size();
g[from].push_back(id);
g[to].push_back(id);
edges.push_back({from, to, cost});
return id;
}
};
// blossom / find_max_unweighted_matching
template
vector find_max_unweighted_matching(const undirectedgraph &g) {
std::mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
vector match(g.n, -1); // 匹配
vector aux(g.n, -1); // 时间戳记
vector label(g.n); // "o" or "i"
vector orig(g.n); // 花根
vector parent(g.n, -1); // 父节点
queue q;
int aux_time = -1;
auto lca = [&](int v, int u) {
aux_time++;
while (true) {
if (v != -1) {
if (aux[v] == aux_time) { // 找到拜访过的点 也就是LCA
return v;
}
aux[v] = aux_time;
if (match[v] == -1) {
v = -1;
} else {
v = orig[parent[match[v]]]; // 以匹配点的父节点继续寻找
}
}
swap(v, u);
}
}; // lca
auto blossom = [&](int v, int u, int a) {
while (orig[v] != a) {
parent[v] = u;
u = match[v];
if (label[u] == 1) { // 初始点设为"o" 找增广路
label[u] = 0;
q.push(u);
}
orig[v] = orig[u] = a; // 缩花
v = parent[u];
}
}; // blossom
auto augment = [&](int v) {
while (v != -1) {
int pv = parent[v];
int next_v = match[pv];
match[v] = pv;
match[pv] = v;
v = next_v;
}
}; // augment
auto bfs = [&](int root) {
fill(label.begin(), label.end(), -1);
iota(orig.begin(), orig.end(), 0);
while (!q.empty()) {
q.pop();
}
q.push(root);
// 初始点设为 "o", 这里以"0"代替"o", "1"代替"i"
label[root] = 0;
while (!q.empty()) {
int v = q.front();
q.pop();
for (int id : g.g[v]) {
auto &e = g.edges[id];
int u = e.from ^ e.to ^ v;
if (label[u] == -1) { // 找到未拜访点
label[u] = 1; // 标记 "i"
parent[u] = v;
if (match[u] == -1) { // 找到未匹配点
augment(u); // 寻找增广路径
return true;
}
// 找到已匹配点 将与她匹配的点丢入queue 延伸交错树
label[match[u]] = 0;
q.push(match[u]);
continue;
} else if (label[u] == 0 &&
orig[v] !=
orig[u]) { // 找到已拜访点 且标记同为"o" 代表找到"花"
int a = lca(orig[v], orig[u]);
// 找LCA 然后缩花
blossom(u, v, a);
blossom(v, u, a);
}
}
}
return false;
}; // bfs
auto greedy = [&]() {
vector order(g.n);
// 随机打乱 order
iota(order.begin(), order.end(), 0);
shuffle(order.begin(), order.end(), rng);
// 将可以匹配的点匹配
for (int i : order) {
if (match[i] == -1) {
for (auto id : g.g[i]) {
auto &e = g.edges[id];
int to = e.from ^ e.to ^ i;
if (match[to] == -1) {
match[i] = to;
match[to] = i;
break;
}
}
}
}
}; // greedy
// 一开始先随机匹配
greedy();
// 对未匹配点找增广路
for (int i = 0; i < g.n; i++) {
if (match[i] == -1) {
bfs(i);
}
}
return match;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
int n, m;
cin >> n >> m;
undirectedgraph g(n);
int u, v;
for (int i = 0; i < m; i++) {
cin >> u >> v;
u--;
v--;
g.add(u, v, 1);
}
auto blossom_match = find_max_unweighted_matching(g);
vector ans;
int tot = 0;
for (int i = 0; i < blossom_match.size(); i++) {
ans.push_back(blossom_match[i]);
if (blossom_match[i] != -1) {
tot++;
}
}
cout << (tot >> 1) << "\n";
for (auto x : ans) {
cout << x + 1 << " ";
}
}
===== 参考链接 =====
[[https://oi-wiki.org/topic/graph-matching/general-match/|OI Wiki]]