Bipartite graph, 又称二部图
定义:如果一张无向图的\(N\)个节点可以分成两个没有相同点的非空集合\(A\), \(B\),且存在一种分法使得同一个集合内的点没有相连的边,那么这个图为二分图,\(A\), \(B\), 分别为此二分图的左部和右部。
判定:
定理:一个图是二分图当且仅当图中不存在奇环(长度为奇数的环)
更具该定理,我们可以通过染色法进行二分图判定:
尝试用两种颜色标记节点,如果有一个点的染色产生了冲突,那么这个图就不是二分图
染色结束之后,\(A\), \(B\)集合中的点分别为黑色点集以及白色点集
复杂度:\(O(N +M)\)
在二分图中选择尽量多的边,使得每一个点至多被一条边覆盖
或者说:二分图的一组匹配 \(S\) 是最大匹配,当且仅当图中不存在 \(S\) 的增广路
这是一种不常用的算法
对于任意一组匹配 \(S\) (一个边集), 属于 \(S\) 的边被称为“匹配边“,不属于 \(S\) 的边称为”非匹配边“,匹配边的端点为“匹配点”,其他节点为“非匹配点”。如果二分图中存在一条连接两个非匹配点的路径,使得“匹配边”和“非匹配边”交替出现,那么称这条路径为增广路,也称交错路
听着很抽象,我们换一种讲法
总流程:遍历所有点,为每一个点寻找一个匹配点,两点互相匹配,且一个点有且只有一个匹配
遍历每一个结点
尝试当前结点 \(s\) 和一个点 \(t\) 匹配,如果 \(t\) 以及被匹配,尝试更换 \(t\) 的匹配。注意:在最外层遍历时,每一次每一个点只能被匹配一次,总共最多会匹配 \(O(n)\) 次
重复上述步骤,使得所有点都被遍历到
算法的正确性基于能反悔的贪心策略。每一次贪心最多只会遍历图一次,为\(O(M)\),所以,算法的时间复杂度为 \(O(NM)\)
下面给出一种参考:
int match[N], vis[N], vt = 0;
bool dfs(int x) {
// 采用链式前向星的写法
for (int y, i = head[x]; i; i = edge[i].next)
// 在每一次遍历中,一个点只会访问一次
if (!vis[(y = edge[i].to)]) {
vis[y] = vt;
if (!match[y] || dfs(match[y]) {
match[y] = x; return true;
}
}
return false;
}
int main() {
// ....
int matchCount = 0;
// vt: visit time, 用于避免memset的使用
for (vt = 1; vt <= n; ++vt) {
if (dfs(vt)) ++matchCount;
}
// ....
return 0;
}
值得一提的是,如果要寻找最大二分匹配,不仅仅可以使用最简单的匈牙利算法,使用网络流也可以做到类似的效果,并且效率更高。这一部分我放在网络流部分讲述
下面给出的定义会非常的迷惑,请仔细阅读于理解
最小边覆盖:你可以认为是用最少的边覆盖所有的点。选择尽量少的边,使得任意一个点相连的边中至少有一条边被选中
最小点覆盖: 同理,用最少的点覆盖所有的边。选择尽量少的点,使得任意一条边的两个端点至少有一个被选中
最大点独立集:在不重复覆盖一条边的情况下,选择最多的点。选择尽量多的边,使得任意一条边的两端至多有一个点被选
最大边独立集:在不重复覆盖一个点的情况下选择最多的边。这不就是最大二分匹配?
其实这四者是有数量关系的,由于证明相对复杂,可以参考《算法竞赛进阶指南》图论二分图部分
总而言之就是:
放弃用mermaid挣扎,还是画图好用