二分图复习
相关概念
-
匹配:
一个图是一个匹配,是指这个图中任意两条边都没有公共顶点,每个顶点至多连出一条边,而每一条边都将一对顶点相匹配。
-
极大匹配
图\(G\)的一个极大匹配是这样的一个匹配,他不可能是图\(G\)的任何一个匹配的真子图。如果\(M\)是\(G\)的一个极大匹配,则\(G\)中每一条边都和\(M\)中的一条边相邻,否则就能将该边加入\(M\)。
-
最大匹配
最大匹配中的是指边数最多的一组匹配,最大匹配可能不止一个,但是最大匹配的边数是确定的,并且不可能超过图中顶点的一半(因为每个不存在公共边,而不同边对应的公共顶点不同), 最大匹配中的顶点数称为图的配对数, 一般记作\(v(G)\).
最大匹配显然都是极大匹配,但是极大匹配不一定是最大匹配, 下图就是极大匹配,但不是最大匹配。
-
完美匹配
图\(G\)的一个完美匹配是一个包括了图\(G\)中原来的所有顶点的匹配,是最大匹配的一种。下述图中左右两个为最大匹配,而中间的是完美匹配,完美匹配同时也是原图的最小边数的边覆盖(即是用最少的边包括所有顶点的子图)。
-
交替路径(Alternating Path)
路径中每一条边交替的属于或不属于\(M\), 比如第1,3, 5条边属于\(M\), 而第2,4,6条边不属于\(M\)等等。
-
增广路径 (Augumenting Path)
从\(M\)中没有用到的顶点开始,并从\(M\)中没有用到的顶点结束的交替路径。
-
-
覆盖
-
顶点覆盖
图\(G\)的一个顶点覆盖是一个顶点集合\(V\), 使得\(G\)中的每一条边都接触\(V\)中的至少一个顶点。我们称集合\(V\)覆盖了\(G\)的边
-
最小顶点覆盖
用最小的顶点去覆盖所有的边。
-
边覆盖
边覆盖是一个边集合\(E\), 使得图\(G\)中的每一个顶点都接触\(E\)中的至少一条边
如果只说覆盖,通常指点覆盖。
-
定义
二分图,又称二部图,英文名为Bipartite graph
节点由两个集合组成,且两个集合内部没有边的图
换言之,存在一种方案,将节点划分成满足以上性质的 集合
性质
- 如果两个集合中的点分别染成黑色和白色,可以发现二分图中的每一条边都一定是连接一个黑色点和一个白色点
- 二分图不存在长度为 奇数 的环 (二分图中每条边都从一个集合走向另一个集合,从一个集合返回该集合只能经过偶数个边)
当然,对于长度不存奇数的环,我们也可以通过染色,将所有点划分为两个集合,就形成了二分图。
二分图的判断 (染色法) \(O(V + E)\)
例题: 关押罪犯
#include <bits/stdc++.h>
using i64 = long long;
const int N = 2e4 + 10, M = 2e5 + 10;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
int color[N]; // 0没有染色, 1,2两个不同的颜色
/*
check(mid)
对于积怨值大于mid的两个罪犯分别关到不同的监狱内
等价于:除去积怨值小于等于mid的边,子图能否构成二分图
显然能构成二分图的子图也能构成二分图, 所以满足二段性
即[不满足,满足]两段,那么结果就是第一个满足的mid
*/
void add(int a, int b, int c) {
ne[idx] = h[a], h[a] = idx, e[idx] = b, w[idx ++] = c;
}
bool dfs(int u, int c, int mid) {
color[u] = c;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (w[i] <= mid) continue;
if (color[v]) {
if (color[v] == c) return false;
} else if (!dfs(v, 3 - c, mid)) return false;
}
return true;
}
bool check(int mid) {
memset(color, 0, sizeof color);
for (int i = 1; i <= n; i ++)
if (color[i] == 0)
if (!dfs(i, 1, mid)) return false;
return true;
}
int main() {
memset(h, -1, sizeof h);
std::cin >> n >> m;
while (m --) {
int a, b, c;
std::cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
int l = 0, r = 1e9;
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
std::cout << l;
}
最大匹配 (匈牙利算法)\(O(V_左E)\)
注意:标题复杂度中的\(V\)为左边点数。
对于二分图下列的概念:
匹配: 一个图是一个匹配(或称独立边集)是指这个图之中,任意两条边都没有公共的顶点。这时每个顶点都至多连出一条边,而每一条边都将一对顶点相匹配。
最大匹配:边数最多的一组匹配
匹配点:在匹配当中的点, 非匹配点同理
增广路径:从一个非匹配点走, 经过非匹配边和匹配边 这样交替,最后经过非匹配边到非匹配点
\(最大匹配 \rightleftharpoons 不存在增广路径\)
例题:棋盘覆盖
#include <bits/stdc++.h>
using i64 = long long;
typedef std::pair<int, int> PII;
const int N = 110;
int n, m;
int g[N][N], st[N][N];
PII match[N][N];
bool find(int x, int y) {
static int dx[] = {0, -1, 1, 0}, dy[] = {1, 0, 0, -1};
for (int i = 0; i <= 3; i ++) {
int a = x + dx[i], b = y + dy[i];
if (a < 1 || a > n || b < 1 || b > n) continue;
if (st[a][b] || g[a][b]) continue;
st[a][b] = true;
PII t = match[a][b];
if (t.first == -1 || find(t.first, t.second)) {
match[a][b] = {x, y};
return true;
}
}
return false;
}
int main() {
memset(match, -1, sizeof match);
std::cin >> n >> m;
while (m --) {
int x, y;
std::cin >> x >> y;
g[x][y] = true;
}
int res = 0;
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= n; j ++) {
if ((i + j) % 2 && !g[i][j]) {
memset(st, 0, sizeof st);
if (find(i, j)) res ++;
}
}
}
std::cout << res;
return 0;
}
最小点覆盖
点覆盖,在图论中点覆盖的概念定义如下:对于图\(G=<V,E>\)中的一个点覆盖是一个集合 \(S⊆V\)使得每一条边至少有一个端点在\(S\)中。
在二分图中,有这样一个结论: \(最小点覆盖 = 最大匹配数\)
证明:
最小点覆盖\(\ge\)最大匹配数
在一组匹配中,匹配的两条边没有公共点,所以每条匹配边至少要选定一个点,最大匹配为\(m\), 则至少要选定\(m\)个点
最小点覆盖\(=\)最大匹配数 (构造)
1.先求出最大匹配,从左部每个非匹配点出发,做一遍增广并标记所有经过的点
2.左部所有未被标记的点和右部所有被标记的点
左部所有非匹配点一定都被标记
右部所有非匹配点一定没有被标记
对于每一个匹配边,左右两点要么同时被标记,要么同时不被标记
#include <bits/stdc++.h>
using i64 = long long;
const int N = 110;
int n, m, k;
bool g[N][N], st[N];
int match[N];
bool find(int x) {
for (int i = 1; i < m; i ++) {
if (!st[i] && g[x][i]) {
st[i] = true;
int t = match[i];
if (!t || find(t)) {
match[i] = x;
return true;
}
}
}
return false;
}
int main() {
while (std::cin >> n, n) {
std::cin >> m >> k;
memset(g, 0, sizeof g);
memset(match, 0, sizeof match);
while (k --) {
int t, a, b;
std::cin >> t >> a >> b;
if (!a || !b) continue;
g[a][b] = true;
}
int res = 0;
for (int i = 1; i < n; i ++) {
memset(st, 0, sizeof st);
if (find(i)) res ++;
}
std::cout << res << "\n";
}
}
最大独立集
对于图\(G<V,E>\) 选出最大的点集\(S\), 使得集合\(S\)内的任何两个点之间都没有边。
最大团
对于图\(G<V, E>\), 选出最大的点集\(S\), 使得集合\(S\)内的任何两个点之间都有边 。
原图的最大独立集是补图的最大团。
在二分图中,求最大独立集 等价于求 去掉最少的点 = m,将所有边都破坏掉 等价于 找最小点覆盖 = m 等价于 找最大匹配 = m。
最终最大独立集就等于去掉最少的点使集合内的点都没有边,所以等于\(n(总点数) - m\)
#include <bits/stdc++.h>
#define x first
#define y second
using i64 = long long;
const int N = 110;
typedef std::pair<int, int> PII;
int n, m, k;
PII match[N][N];
bool g[N][N], st[N][N];
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
bool find(int x, int y) {
for (int i = 0; i < 8; i ++) {
int a = x + dx[i], b = y + dy[i];
if (a < 1 || a > n || b < 1 || b > m) continue;
if (g[a][b]) continue;
if (st[a][b]) continue;
st[a][b] = true;
PII t = match[a][b];
if (t.x == 0 || find(t.x, t.y)) {
match[a][b] = {x, y};
return true;
}
}
return false;
}
int main() {
std::cin >> n >> m >> k;
for (int i = 0; i < k; i ++) {
int x, y;
std::cin >> x >> y;
g[x][y] = true;
}
int res = 0;
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= m; j ++) {
if ((i + j) % 2 || g[i][j]) continue;
memset(st, 0, sizeof st);
if (find(i, j)) res ++;
}
}
std::cout << n * m - k - res;
}
最小路径点覆盖
针对一个有向无环图, 用最少的互补相交的路径(点不重复),将所有点覆盖。
最小路径重复点覆盖
- 先求传递闭包 \(G'\), 原图\(G\)的最小路径重复点覆盖=\(G’\)的最小路径点覆盖
- 在\(G'\) 的最小路径点覆盖
#include <bits/stdc++.h>
using i64 = long long;
const int N = 210, M = 30010;
int n, m;
bool d[N][N], g[N][N], st[N];
int match[N];
bool find(int x) {
for (int i = 1; i <= n; i ++) {
if (d[x][i] && !st[i]) {
st[i] = true;
int t = match[i];
if (t == 0 || find(t)) {
match[i] = x;
return true;
}
}
}
return false;
}
int main() {
std::cin >> n >> m;
while (m --) {
int a, b;
std::cin >> a >> b;
d[a][b] = true;
}
// 传递闭包
for (int k = 1; k <= n; k ++)
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
d[i][j] |= d[i][k] & d[k][j];
int res = 0;
for (int i = 1; i <= n; i ++) {
memset(st, false, sizeof st);
if (find(i)) res ++;
}
std::cout << n - res;
}