一般图最大匹配
我们知道,二分图匹配有个著名的匈牙利算法(ntr算法 ),用于处理二分图的最大匹配关系。同样的,对于一般图,我们也有个常用的算法来处理一般图的匹配问题,这就是著名的带花树算法
一般图和二分图的区别
二分图是一个可以分为两个顶点集,并且这两个顶点集内部没有相互关系,只在一个集合和另一个集合之间存在关联
但一般图就是存在顶点集内部有关联的情况
二分图
一般图
这时候我们就不能用匈牙利算法,因为匈牙利算法便无法保证最优,这时带花树算法就基于匈牙利算法的基础上进行了处理环的情况
带花树算法
带花树算法下面几个博客讲的非常好,本文稍微补充一下算法过程中的一些细节方面
博客如下
https://www.cnblogs.com/victorique/p/9179650.html#autoid-0-0-0
https://blog.csdn.net/birdmanqin/article/details/100160999
https://www.cnblogs.com/BAJimH/p/10569418.html
第一点:算法的流程?
带花树算法会先选出一个点作为起始点,我们加入默认这个起始点为黑色。然后我们要通过这个黑点(通过将黑点加入队列,通过bfs达到每次抽出的点都是黑点)来寻找增广路。
所以,我们在一般图里面可能遇到这几种情况
1.我们遇到一个没有染色的点
这时候我们是通过黑点找到这个点,所以我们将它染成白色,此时我们多了一条增广路,从算法中返回
2.我们遇到一个被染色了的点
这时候,我们又要分几种情况
(1)这个点在本次算法运行中有没有被访问过
首先,我们要分一下这个点
First:这个点可能是在本次算法运行中没有被访问过
Second:这个点可能是在本次算法运行中已经被访问过
我们记得匈牙利算法中,我们用一个used数组来标记一个顶点在一次dfs中有没有被查询过,查询过就不需要再次查询,因为这是白费力气。
同样,在带花树算法中,我们也是同样这样进行
所以,对于第一点,我们将“挪动”来找一下有没有一个新的增广路
具体的话,就是我们把这个点(无论是黑点还是白点)染成白色与查询的这个黑点配对,然后通过pre数组(记录点的匹配点,相当于取反)找到这个点的匹配点,并且把它染成黑色丢到队列中去寻找其他的增广路
对于第二点,我们需要还需要分成这两种情况
1.这个点是白点
2.这个点是黑点
1.首先是白点的情况
如果我们遇到一个白点,这说明我们一定遇到了一个偶环
(可以去画一下图)
偶环就代表,在这个环中,一定是黑点和白点成对出现
而且,这个点在之前被查询过但是却没有更好的情况(增加一条新的增广路)出现。所以,如果我们按照上面对待没有匹配的点的方法对这个点同样进行这个操作,仍然是多余的,我们并不会多一条增广路
解释一下为什么这样做是多余的。这很简单,我们遇到这个点是通过把它染白和查询的黑点配对,并且把它原来的配对点染黑后拿去寻找增广路。
所以寻找增广路的是它的匹配点,而且是黑色,但是它已经被拿去匹配了,我们重复进行这个操作是没有意义的
所以,这种情况,我们跳过
2.如果是黑点
遇到白点是偶环,那么黑点就是奇环
奇环的出现,代表着我们在查询过程中遇到了黑点查询到了黑点的情况
而且在这个环中,我们会在原本的配对数上增加一个多余的点
对于奇环的处理,就是这个算法的重中之重,我们称之为开花(或者压点)
开花
对于这个过程的正确性证明十分的麻烦,这里就叙述下算法对于这个过程的处理
我们因为里面是成对的偶环加一个多余的点,所以我们就把这个奇环压缩成一个偶环加上一个点(这个点可以是黑点也可以是白点),然后我们把所有点染成黑色丢到队列中去寻找新的增广路
这里解释下为什么要把所有点染成黑色丢到队列中去寻找增广路
我们在压缩奇环的过程中,无法保证使用哪一个点去寻找新的增广路是最优的,这时候我们把每个点都拿去寻找,保证开花后仍然是最优
(这一切的基础在于一个证明:开花前和开花后是否有增广路的情况相同,有兴趣的可以去看一下这个的证明)
然后我们把这个“环”变成一个真正的环:我们把这两个黑点连在一起,并且把前置节点标记为相互,并且通过lca查询最近公共祖先,将这个环的根节点连到那个公共祖先上(通过并查集实现)
这样做的原因是为了通过类似于路径压缩的方式,把一个小的花(环)连接到大的花(环)上,这样做的原因是为了之后遇到两个点已经在花中的情况提供高效的查询
(2)如果两个点已经在一个花中
我们通过并查集查询到他们在一个已经被压缩的花中,这时候我们自然是跳过(不做重复查询)
最后,通过以上的过程,我们得到了一个“树”,这个树带着一个个被压缩的花(奇环)。
这大概就是带花树名字的由来吧。对于整个算法正确性证明,估计要去看一看acm的一些论文
2.带花树算法板子
对于带有代码解析的板子,我推荐去看一下这个博客(上面三个博客中第二个)
https://blog.csdn.net/birdmanqin/article/details/100160999
首先是洛谷的板子题
P6113 【模板】一般图最大匹配
https://www.luogu.com.cn/problem/P6113
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <string>
#include <vector>
using namespace std;
typedef long long ll;
const int N = 2e3 + 10;
const int M = 5e5 + 10;
struct node {
int to, nxt;
} g[M];
int head[N], cnt;
int vis[N], match[N], f[N], pre[N], Id, id[N];
// vis[i]: 0(未染色) 1(黑色) 2(白色)
// match[i]: i的匹配点
// f[i]: i在带花树中的祖先
// pre[i]: i的非匹配边的另一点
// id: 找LCA用
// g数组: 存关系
int n, m, ans, u, v;
queue<int> q;
void init() {
Id = ans = cnt = 0;
for (int i = 1; i <= n; i++) {
head[i] = -1, id[i] = match[i] = 0;
}
}
void add(int u, int v) { g[cnt].to = v, g[cnt].nxt = head[u], head[u] = cnt++; }
int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }
int lca(int x, int y) {
for (++Id;; swap(x, y)) {
if (x) {
x = find(x);
if (id[x] == Id)
return x;
else
id[x] = Id, x = pre[match[x]];
}
}
}
void blossom(int x, int y, int l) { // l为x,y的最近公共祖先(同一个花)
while (find(x) != l) {
pre[x] = y, y = match[x];
if (vis[y] == 2) vis[y] = 1, q.push(y);
if (find(x) == x) f[x] = l;
if (find(y) == y) f[y] = l;
x = pre[y];
}
}
bool aug(int s) {
for (int i = 1; i <= n; i++) {
vis[i] = pre[i] = 0;
f[i] = i;
}
while (!q.empty()) q.pop();
q.push(s), vis[s] = 1;
while (!q.empty()) {
u = q.front();
q.pop();
for (int i = head[u]; ~i; i = g[i].nxt) {
v = g[i].to;
if (find(u) == find(v) || vis[v] == 2) continue;
if (!vis[v]) {
vis[v] = 2, pre[v] = u;
if (!match[v]) {
for (int x = v, last; x; x = last)
last = match[pre[x]], match[x] = pre[x], match[pre[x]] = x;
return true;
}
vis[match[v]] = 1, q.push(match[v]);
} else {
int LCA = lca(u, v);
blossom(u, v, LCA), blossom(v, u, LCA);
}
}
}
return false;
}
int main() {
cin >> n >> m;
init();
while (m--) {
cin >> u >> v;
add(u, v);
add(v, u);
}
for (int i = 1; i <= n; i++) {
if (!match[i] && aug(i)) ans++;
}
cout << ans << endl;
for (int i = 1; i <= n; i++) cout << match[i] << ' ';
return 0;
}
3.一个例题
牛客2020暑假算法训练营第一场I题 1or2
https://ac.nowcoder.com/acm/contest/5666/I
题解待更
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <string>
#include <vector>
using namespace std;
typedef long long ll;
const int N = 2e3 + 10;
const int M = 5e5 + 10;
struct node {
int to, nxt;
} g[M];
int head[N], cnt;
int vis[N], match[N], f[N], pre[N], Id, id[N];
// vis[i]: 0(未染色) 1(黑色) 2(白色)
// match[i]: i的匹配点
// f[i]: i在带花树中的祖先
// pre[i]: i的非匹配边的另一点
// id: 找LCA用
// g数组: 存关系
int n, m, ans, u, v;
queue<int> q;
int U[N], V[N];
int h;
void init() {
Id = ans = cnt = 0;
for (int i = 1; i <= (n+m)*2; i++) {
head[i] = -1, id[i] = match[i] = 0;
}
}
void add(int u, int v) { g[cnt].to = v, g[cnt].nxt = head[u], head[u] = cnt++; }
int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }
int lca(int x, int y) {
for (++Id;; swap(x, y)) {
if (x) {
x = find(x);
if (id[x] == Id)
return x;
else
id[x] = Id, x = pre[match[x]];
}
}
}
void blossom(int x, int y, int l) { // l为x,y的最近公共祖先(同一个花)
while (find(x) != l) {
pre[x] = y, y = match[x];
if (vis[y] == 2) vis[y] = 1, q.push(y);
if (find(x) == x) f[x] = l;
if (find(y) == y) f[y] = l;
x = pre[y];
}
}
bool aug(int s) {
for (int i = 1; i <= h; i++) {
vis[i] = pre[i] = 0;
f[i] = i;
}
while (!q.empty()) q.pop();
q.push(s), vis[s] = 1;
while (!q.empty()) {
u = q.front();
q.pop();
for (int i = head[u]; ~i; i = g[i].nxt) {
v = g[i].to;
if (find(u) == find(v) || vis[v] == 2) continue;
if (!vis[v]) {
vis[v] = 2, pre[v] = u;
if (!match[v]) {
for (int x = v, last; x; x = last)
last = match[pre[x]], match[x] = pre[x], match[pre[x]] = x;
return true;
}
vis[match[v]] = 1, q.push(match[v]);
} else {
int LCA = lca(u, v);
blossom(u, v, LCA), blossom(v, u, LCA);
}
}
}
return false;
}
int k;
int d[N];
vector<int> e[N];
void creat() {
for (int i = 1; i < N; i++) e[i].clear();
h = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= d[i]; j++) {
e[i].push_back(++h);
}
}
//h--;
for (int i = 1; i <= m; i++) {
h++;
for (int j = 0; j < e[U[i]].size(); j++) {
add(e[U[i]][j], h);
add(h, e[U[i]][j]);
}
h++;
for (int j = 0; j < e[V[i]].size(); j++) {
add(e[V[i]][j], h);
add(h, e[V[i]][j]);
}
add(h - 1, h);
add(h, h - 1);
}
}
int main() {
while (cin >> n >> m) {
for (int i = 0; i <= cnt; i++) g[i].to = 0, g[i].nxt = 0;
memset(match, 0, sizeof(match));
memset(id, 0, sizeof(id));
memset(f, 0, sizeof(f));
memset(pre, 0, sizeof(pre));
memset(head, 0, sizeof(head));
memset(vis, 0, sizeof(vis));
ans = 0;
k = 0;
init();
for (int i = 1; i <= n; i++) cin >> d[i];
int a = m;
while (a--) {
cin >> u >> v;
U[++k] = u;
V[k] = v;
/*add(u, v);
add(v, u);*/
}
creat();
for (int i = 1; i <= h; i++) {
if (!match[i] && aug(i)) ans++;
}
if (ans == h/2)
cout << "Yes";
else
cout << "No";
cout << endl;
}
return 0;
}