问题类型
如果一个图的点集可以分为两部分,且所有边的两个端点都在不同的部分中,则这个图是一个二分图。
有很多关于多选一的问题都可以建模成二分图来解决。
这里我们先讲关于二分图最大匹配的问题。
解决方案
首先我们来了解一些定义:
1.点覆盖集:即一个点集,使得所有边至少有一个端点在集合里。(“点”覆盖了所有“边”)
2.边覆盖集:即一个边集,使得所有点都与集合里的边邻接。(“边” 覆盖了所有“点”)
3.点独立集:即一个点集,满足集合中任两个结点不相邻。(导出的子图是零图(没有边)的点集)
4.边独立集:即一个边集,满足边集中的任两边不邻接。(就是匹配)
5.点支配集:即一个点集,使得所有其他点至少有一个相邻点在集合里。(一部分的“点”支配了所有“点”)。
6.边支配集:即一个边集,使得所有边至少有一条邻接边在集合里。(一部分的“边”支配了所有“边”)。
7.DAG的最小路径覆盖:是“路径” 覆盖“点”,即用尽量少的不相交简单路径覆盖有向无环图G的所有顶点,每个顶点严格属于一条路径。路径的长度可能为0(单个点)。
在二分图中,有以下性质:
1.最大匹配数 = 最大边独立数。
2.最大边独立数 = 最小点覆盖数 = 最大匹配数。
3.最大点独立数 = 最小边覆盖数 = n(节点个数) - 最大匹配数。
4.一个DAG的最小路径覆盖 = n(节点个数) - 转化为二分图后的最大匹配数
证明:
1.由定义即可知:最大匹配数 = 最大边独立数。
2.我们对最大匹配的每对匹配边都选取一个点出来组成点覆盖集。
证最小点覆盖≤最大匹配:如果仍有一些边未被这个集合覆盖,那么就说明匹配里可以加上这些未被覆盖的边,这就不是最大匹配了。
证最小点覆盖≥最大匹配:如果最大匹配里有一条边的两个点都不选,那么这条边就未被覆盖,不符合点覆盖的定义。
所以 最大边独立数 = 最小点覆盖数 = 最大匹配数。
3.我们先证明 最大点独立数 = n - 最大匹配数
最大点独立集其实就是最小点覆盖集关于整个二分图点集的补集。
先证明这是个点独立集:这些点之间如果还有边使独立集中两点相邻,则这条边就没被最小点覆盖集“覆盖”到,矛盾。
再证明这是个最大的点独立集:如果还能再加入点,这个点必定是最小点覆盖集中的一个点,而必定会有一条边联通这个点和原来的这个点独立集中的点,不符合点独立集定义。
所以就有 最大点独立数 = n - 最大匹配数。
我们再来证明 最小边覆盖 = n - 最大匹配数
假设最大匹配数为k。
我们贪心的考虑尽量用边去覆盖多的点,所以我们先把最大匹配都选了,已选k条边。
剩下n - 2 k个点都用一条边去覆盖,所以总共选了n - 2 k + k = n - k。
所以 最小边覆盖 = n - 最大匹配数。
4.我们先讲一下如何把一个DAG转化为一个二分图。
如果在DAG上u -> v有一条边,那我们在二分图上Xu -> Yv连一条边。
因为DAG的最小路径覆盖要求不能相交,所以每一个点最多只会有一个后继。
所以我们求出最大匹配后,那些在匹配里的就是有后继的节点,所以会有(n - 最大匹配数)个点没有后继节点。
又因为一条路径只会有一个点也就是最后一个点没有后继节点,所以 一个DAG的最小路径覆盖 = n - 转化为二分图后的最大匹配数。
有了以上性质,很多问题都可以转化为求二分图的最大匹配。接下来我们来讲讲如何求一个二分图的最大匹配。
所有求最大匹配的算法都是基于增广路定理的:一个匹配是最大匹配当且仅当没有增广路。这个定理适用于任意图。
我们先来讲一讲增广路的定义:增广路是一条起点为未匹配点,终点为未匹配点,且相邻两条边肯定满足一条在匹配里,一条不在匹配里的路径。
所以增广路有以下性质:
1.有奇数条边,有偶数个点。
2.起点在二分图的左半边,终点在右半边(一个左半边,一个右半边)。
3.路径上的点一定是一个在左半边,一个在右半边,交替出现。
4.整条路径上没有重复的点。
5.起点和终点都是目前还没有配对的点,而其它所有点都是已经配好对的。
6.路径上的所有第奇数条边都不在原匹配中,所有第偶数条边都出现在原匹配中。
7.把增广路径上的所有第奇数条边加入到原匹配中去,并把增广路径中的所有第偶数条边从原匹配中删除(这个操作称为增广路径的取反),则新的匹配数就比原匹配数多了1个。(求最大匹配的关键)
我们先来讲讲用匈牙利算法求二分图的最大匹配:
我们先给每个点都置为未匹配。每次我们都寻找到一条增广路的时候,答案+1即可。
那么如何寻找增广路呢?
我们先从随意一个未被匹配的点开始找起,遍历它的出边u -> v。
如果v没有匹配或者可以从v的匹配开始继续找到增广路(递归实现)那么就把v的匹配改成u(给匹配取反)。
因为增广路不能相交,所以一个点在同一个寻找增广路的过程中被扫过多次就没有意义了。用数组标记以下即可。
所以每次寻找增广路时间复杂度为O(m)。又因为最大匹配不超过n,即最多找n次增广路,所以总时间复杂度为O(n * m)。
Hopcroft-Karp算法就是对匈牙利算法进行了优化:匈牙利是每次找一条增广路,Hopcroft-Karp是每次找多条增广路。
我们用bfs来寻找还有没有增广路,顺便处理一个点到X点集中还未匹配过的点的最短距离。
所以我们dfs增广的时候只要对dis[v] = dis[u] + 1的边进行增广就可以了。
算法流程类似下图:
其中红色边为增广路,黄色边为已匹配的边。
好像可以证明时间复杂度是O(n ^ 0.5 * m)(但我不会啊)
代码演示
匈牙利算法的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25int Find(int u)
{
travel(i, u)
{
int v = edge[i].vet;
if (vis[v]) continue;
vis[v] = 1;
if (!match[v] || Find(match[v]))
{
match[v] = u;
return 1;
}
}
return 0;
}
int main()
{
Init();
rep(i, 1, n)
{
if (match[i]) continue;
rep(j, 1, n) vis[j] = 0;
if (Find(i)) ans++;
}
}
Hopcroft-Karp算法的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56int bfs()
{
rep(i, 1, n)
{
Disx[i] = Disy[i] = -1;
if (!Matchx[i]) Q.push(i), Disx[i] = 0;
}
Dis = INF;
while (!Q.empty())
{
int u = Q.front(); Q.pop();
if (Disx[u] > Dis) continue;
travel(i, u)
{
int v = edge[i].vet;
if (~Disy[v]) continue;
Disy[v] = Disx[u] + 1;
if (!Matchy[v]) Dis = Disy[v];
else
{
Disx[Matchy[v]] = Disy[v] + 1;
Q.push(Matchy[v]);
}
}
}
return Dis != INF;
}
int dfs(int u)
{
travel(i, u)
{
int v = edge[i].vet;
if (!vis[v] && Disy[v] == Disx[u] + 1)
{
vis[v] = 1;
if (!Matchy[v] || dfs(Matchy[v]))
{
Matchy[v] = u, Matchx[u] = v;
return 1;
}
}
}
return 0;
}
int MaxMatch()
{
int Match = 0;
while (bfs())
{
rep(i, 1, n) vis[i] = 0;
rep(i, 1, n) if (!Matchx[i]) Match += dfs(i);
}
return Match;
}