吐槽
这两天的训练赛接二连三地出这种又冷门又难的算法。
不得不硬刚。。。。
本博客默认阅读者对匈牙利算法有一定理解。
带花树算法
带花树算法,英文: B l o s s o m a l g o r i t h m Blossom \ algorithm Blossom algorithm,或 E d m o n d s M a t c h i n g A l g o r i t h m Edmonds \ Matching \ Algorithm Edmonds Matching Algorithm。是 Jack Edmonds 发表于 1965 年,用于解决一般图最大匹配问题的算法。经过修改后可以得到“一般图最大权匹配算法”。这个算法也证明了,一般图最大匹配有多项式复杂度解法。
从二分图到一般图
大多数人比较熟悉二分图最大匹配的过程,所以理解带花树算法最好还是从匈牙利算法入手。
我们习惯上,把二分图的点分成左右两部分,从每一个左部点(或者右,无所谓)出发,尝试寻找增广路,如果成功找到增广路说明最大匹配可以更新了,同时需要更改左右部点之间的匹配关系。这就是匈牙利算法的大概流程。代码如下所示。
int Hungarian() //匈牙利最最常见但并不通用的一种写法
{
int ans = 0;
memset(link, -1, sizeof(link));
for(int i=1;i<=N;i++) //这层循环实际上是枚举二分图其中一个点集的所有点
{
memset(vis, 0, sizeof(vis));
if(dfs(i)) //dfs就是寻找增广路的过程,这里省略细节
ans++;
}
return ans;
}
如果事先不知道二分图两个集合的结点编号,也就是说我找不到左部点或右部点,可以想到先利用BFS对二分图进行黑白染色,然后从一种颜色出发,向另一种颜色寻找匹配(染色和匈牙利算法可以同时进行)。由于二分图不存在奇环,因此这种方法一定可行。
换句话说:二分图的性质决定了,它天然地将所有结点分好了类。而对于一般图,我们也可以“人为地”,将结点分成两类,从其中一类出发向另一类寻找匹配,然而一般图有可能存在奇环,使得染色出现矛盾的情况。因此我们现在可以得到带花树算法的核心思想:对图中没有奇环的部分按照正常的匈牙利算法处理,并加入对奇环的特殊讨论。
while(!q.empty())
{
int u = q.front();
q.pop();
for(每个可达点)
{
int v = edge[i].to;
if(!vis[v]) //v点未被访问
{
if(!match[v]) //v点没有匹配点 说明找到增广路了
{
agu(v) //进行增广
return 1; //增广成功更新答案
}
//此处v点有匹配点,则更新交替路
}
else //判断是否找到奇环,随后写怎么处理
{
}
}
}
对环的处理
根据BFS的性质可知,我们会得到一棵黑白点交替的树结构。我们规定所有匹配边的入点为“白色”,出点为“黑色”。下面来讨论对奇环的处理。
BFS过程中如果发现奇环,设染色冲突的结点为
u
u
u 和
v
v
v,那么
u
u
u 和
v
v
v的最近公共祖先一定在这个奇环上,且这个祖先节点一定是“黑色点”,因为匹配边和两种颜色都交替出现,这个结点的子孙结点能形成环说明他自己有不止一个子节点,而这多于一个的子节点必不可能同时为“黑点”,否则祖先节点就同时连了两条匹配边。具体见图。
对于这个奇环,我们把它缩成一个点(这个点叫做 “花”,这里是这个算法名字的由来),将这个新的点作为一个黑点,向外寻找匹配。我们可以证明缩点之后的图,最大匹配数不变。也就是说:设原图为
G
G
G,缩点后的图为
G
′
G^{\prime}
G′:
- 若 G G G 存在增广路, G ′ G^{\prime} G′也存在。
- 若 G ′ G^{\prime} G′ 存在增广路, G G G 也存在。
前面说过偶环内部必有完美匹配,那么奇环内部则一定有一个点可以向环外匹配,环内的每个点都有可能成为这个点,因此缩点之后要把环中的点全部染成黑色并加入队列。
实际实现上,缩点并不需要改变结点间的边关系,只需用并查集维护一个结点是在哪个结点为根的奇环中即可。
完整代码:(洛谷模板)
#include <cstdio>
#include <cstring>
#include <queue>
#define MAXN 1010
using namespace std;
struct t_Edge {
int next;
int to;
};
t_Edge edge[MAXN*MAXN*2];
int head[MAXN], num_edge;
int N, M, a, b, ans;
int father[MAXN], pre[MAXN];
int match[MAXN];
int color[MAXN], vis[MAXN];
int clock, top[MAXN], rinedge[MAXN];
queue<int> Q;
void add(int from, int to)
{
edge[++num_edge].next = head[from];
edge[num_edge].to = to;
head[from] = num_edge;
}
int find(int x)
{
if(father[x]!=x)
father[x] = find(father[x]);
return father[x];
}
int LCA(int x, int y) //找到最近公共祖先
{
clock++;
while(x)
{
rinedge[x] = clock;
x = find(top[x]);
}
x = y;
while(rinedge[x]!=clock)
x = find(top[x]);
return x;
}
void shrink(int x, int y, int lca) //将奇环缩成点 并把环中所有点重新染色
{
while(find(x)!=find(lca))
{
pre[x] = y;
y = match[x];
color[y] = 0;
Q.push(y);
father[find(x)] = lca;
father[find(y)] = lca;
x = pre[y];
}
}
int blossom(int s)
{
for(int i=1;i<=N;i++)
{
top[i] = pre[i] = color[i] = vis[i] = 0;
father[i] = i;
}
while(!Q.empty())
Q.pop();
vis[s] = 1;
Q.push(s);
while(!Q.empty())
{
int u = Q.front();
Q.pop();
for(int i=head[u];i;i=edge[i].next)
{
int v = edge[i].to;
if(!vis[v]) //v点未被访问
{
top[v] = u;
pre[v] = u;
color[v] = 1;
vis[v] = 1;
if(!match[v]) //v点没有匹配点 说明找到增广路了
{
int j = v;
while(j)
{
int x = pre[j];
int y = match[x];
match[j] = x;
match[x] = j;
j = y;
}
return 1;
}
vis[match[v]] = 1; //v点有匹配点 需要从匹配点出发继续寻找增广路
top[match[v]] = v;
Q.push(match[v]);
}
else if(find(u)!=find(v)&&color[v]==0) //找到奇环 缩点
{
int lca = LCA(u, v);
shrink(u, v, lca);
shrink(v, u, lca);
}
}
}
return 0;
}
int main()
{
scanf("%d%d", &N, &M);
for(int i=1;i<=M;i++)
{
scanf("%d%d", &a, &b);
add(a, b);
add(b, a);
}
for(int i=1;i<=N;i++)
{
if(!match[i])
ans += blossom(i);
}
printf("%d\n", ans);
for(int i=1;i<=N;i++)
printf("%d ", match[i]);
printf("\n");
return 0;
}
例题
模板:
洛谷P6113 【模板】一般图最大匹配.
也算模板,建图方式不同:
洛谷P4258 [WC2016]挑战NPC.
一般图最大独立集:
2021年度训练联盟热身训练赛第一场 B - Code Names.