二分图入门
Bipartite Graph
引入
二分图是一类特殊的图, 它可以划分为俩个集合,且每个集合内部的点互不相连,例如:
那么对于二分图显然有俩个性质
性质
- 如果俩个集合中的所有点分别染成红色和黑色,可以发现,每一条边都是连接一个黑点和一个白点
- 二分图不存在长度为奇数的环
对于性质二的证明:
因为一共有俩个集合,所以每次从一个点出发(无论哪个集合),每次走,都是从一个集合到另一个集合(由性质一得),显然只有走偶数次才可以回到同一个集合
证毕。
判定
转化问题为:我们需要知道是否可以将图中的顶点分成两个满足条件的集合?
显然,直接枚举答案集合的话实在是太慢了,我们需要更高效的方法考虑上文提到的性质,我们可以使用 \(DFS\) 或者 \(BFS\) 来遍历这张图。如果发现了奇环,那么就不是二分图,否则是,非常非常的简单哈
应用详解
这里随便写点,主要是二分图这个东西可以延展出来很多东西,这个玩意儿还可以转换为网络流模型,真的是好东西哈。
二分图最大匹配
最小点覆盖问题
定义: 最小点覆盖是选最少的点(作为一个集合),满足图中每条边至少有一个端点在被选的点集中
使用 \(könig\) 定理:二分图中的最大匹配数等于这个图中的最小点覆盖数,即二分图中,最小点覆盖 \(=\) 最大匹配
证明:
将二分图点集分成左右两个集合,使得所有边的两个端点都不在一个集合。
考虑如下构造:从左侧未匹配的节点出发,按照匈牙利算法中增广路的方式走,即先走一条未匹配边,再走一条匹配边。由于已经求出了最大匹配,所以这样的增广路一定以匹配边结束。在所有经过这样「增广路」的节点上打标记。则最后构造的集合是:所有左侧未打标记的节点和所有右侧打了标记的节点。
首先,易证这个集合的大小等于最大匹配。打了标记的节点一定都是匹配边上的点,一条匹配的边两侧一定都有标记(在增广路上)或都没有标记,所以两个节点中必然有一个被选中。
其次,这个集合是一个点覆盖。一条匹配边一定有一个点被选中,而一条未匹配的边一定是增广路的一部分,而右侧端点也一定被选中。
同时,不存在更小的点覆盖。为了覆盖最大匹配的所有边,至少要有最大匹配边数的点数。(引自 \(OI-Wiki\))
证毕。
二分图最大独立集
定义:最大独立集是选最多的点,满足两两之间没有边相连
解法:
因为在最小点覆盖中,任意一条边都被至少选了一个顶点,所以对于其点集的补集,任意一条边都被至多选了一个顶点,所以不存在边连接两个点集中的点,且该点集最大
因此二分图中:最大独立集 \(=\) 最小点覆盖。
KM算法
给定一个二分图,两边的点数都为 \(n\) ,给出若干条边,每条边有一个权值,求最大的完美匹配的值
20230510
又是临近学考的一天,终于领悟了 \(KM\) 的精髓了,太妙了!!!
#include <bits/stdc++.h>
using namespace std;
const int maxn = 500;
int n;
int lx[maxn]; // 记录左顶点的值
int ly[maxn]; // 记录右顶点的值
int link[maxn]; // 记录每轮右顶点匹配的左顶点
int w[maxn][maxn]; // 邻接矩阵 记录边权
bool s[maxn]; // 记录每轮匹配过的左顶点
bool t[maxn]; // 记录每轮匹配过的右顶点
bool check(int x) { // check 某个左顶点是非匹配过
s[x] = 1;
for (int i = 1; i <= n; i++) {
if (lx[x] + ly[i] == w[x][i] && !t[i]) {
t[i] = 1;
if (!link[i] || check(link[i])) {
link[i] = x;
return 1;
}
}
}
return 0;
}
void update() {
int a = 1 << 30;
for (int i = 1; i <= n; i++) {
if (s[i]) {
for (int j = 1; j <= n; j++) {
if (!t[j]) {
a = min(a, lx[i] + ly[j] - w[i][j]);
}
}
}
}
for (int i = 1; i <= n; i++) {
if (s[i]) lx[i] -= a;
if (t[i]) ly[i] += a;
}
}
void km() {
for (int i = 1; i <= n; i++) {
link[i] = lx[i] = ly[i] = 0;
for (int j = 1; j <= n; j++) {
lx[i] = max(lx[i], w[i][j]);
}
}
for (int i = 1; i <= n; i++) {
while (1) {
for (int j = 1; j <= n; j++) s[j] = t[j] = 0;
if (check(i)) break;
else update();
}
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> w[i][j];
}
}
km();
int ans1 = 0;
int ans2 = 0;
for (int i = 1; i <= n; i++) {
ans1 += lx[i] + ly[i];
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
w[i][j] *= -1;
}
}
km();
for (int i = 1; i <= n; i++) {
ans2 += lx[i] + ly[i];
}
cout << -ans2 << '\n' << ans1;
return 0;
}
注意:\(KM\) 算法需要左部和右部完全匹配
补充的定理:
最大匹配数:最大匹配的匹配边的数目
最小点覆盖数:选取最少的点,使任意一条边至少有一个端点被选择
最大独立数:选取最多的点,使任意所选两点均不相连
最小路径覆盖数:对于一个 \(DAG\)(有向无环图),选取最少条路径,使得每个顶点属于且仅属于一条路径。路径长可以为 \(0\)(即单个点)。
定理1:最大匹配数 \(=\) 最小点覆盖数(这是 \(Konig\) 定理)
定理2:最大匹配数 \(=\) 最大独立数
定理3:最小路径覆盖数 \(=\) 顶点数 \(-\) 最大匹配数