- 二分匹配
- 1. 算法分析
- 1.1 几个重要概念
- 1.2 二分图判定
- 1.3 二分图点覆盖、独立集和最小路径点覆盖
- 1.3.1 二分图的点覆盖
- 1.3.2 二分图的独立集
- 1.3.3 DAG的最小路径点覆盖
- 1.3.4 DAG的最小路径可重复点覆盖
- 2. 模板
- 2.1 染色法判断是否为二分图
- 2.2 匈牙利算法找最大匹配
- 3. 典型例题
- 3.1 染色问题
- 3.2 二分匹配问题
- 3.3 二分图的点覆盖集
- 3.4 二分图的点独立集
- 3.5 DAG的最小路径覆盖
- 1. 算法分析
二分匹配
1. 算法分析
1.1 几个重要概念
1.交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边....形成的路径叫交替路
2.增广路:从一个未匹配的点出发,走交替路,如果途径另一个未匹配点,则这条交替路称为增广路
1.2 二分图判定
一张图是二分图当且仅当图中没有奇数环,二分图必然不含有奇数环
1.3 二分图点覆盖、独立集和最小路径点覆盖
最大匹配数=最小点覆盖=总点数-最大独立集=总点数-最小路径点覆盖
1.3.1 二分图的点覆盖
最小点覆盖: 求一个最小的点集S,使得图中任意一条边都有至少一个端点属于S。
定理: 最小覆盖集中点数目等于二分图的最大匹配包含的边数
1.3.2 二分图的独立集
定义: 一个独立集内的点没有连边
定理: 一个二分图的最大独立集内点个数等于总个数n-最大匹配个数
最大团: 一个点集的所有点间都有边相连,一张图G的最大独立集的补集就是最大团
1.3.3 DAG的最小路径点覆盖
DAG的最小路径点覆盖是描述: 给定一张有向无环图,要求用尽量少的不相交的简单路径,覆盖有向无环图的所有顶点(也就是每个顶点恰好被覆盖一次)。
拆点: 把原图中的每个点拆分成两个点,比如i点拆成i点和i'点,把原来的连边和新点连接起来,比如说原图i->j,那么拆点后,把i和j'连边(i和j不用连边),这样的方式组成一张新图
定理: 最小路径点覆盖的数目等于原先所有的点数n-新图最大匹配的数目
1.3.4 DAG的最小路径可重复点覆盖
1.利用传递闭包把DAG变成一张没有重复点的图
2.套用上述求DAG的最小路径覆盖的算法
2. 模板
2.1 染色法判断是否为二分图
#include
using namespace std;
int const N = 1e5 + 10;
int e[N * 2], ne[N * 2], h[N], idx, color[N]; // color记录每个点颜色
int n, m;
// 建立邻接表
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 判断是否u号点可以打上c的颜色
int dfs(int u, int c) {
color[u] = c; // 给u号点打上c的颜色
for (int i = h[u]; i != -1; i = ne[i]) { // 遍历所有与i号点相连的点
int j = e[i]; // 看j号点
if (!color[j]) { // 如果j号点没有染色
if (!dfs(j, 3 - c)) return 0; // 如果j号点染色3-c过程中失败
}
if (color[j] == c) return 0; // 如果j号点也染色c颜色
}
return 1; // 如果u号点的所有邻点都没有染色失败
}
int main() {
memset(h, -1, sizeof h); // 初始化h
cin >> n >> m; // 输入顶点数和边数
for (int i = 0; i < m ; ++i) { // 读入边信息
int a, b;
scanf("%d %d", &a, &b);
add(a, b), add(b, a); // 无向边
}
int flg = 1; // flg记录染色是否成功,成功为1,失败为0
for (int i = 1; i <= n; ++i) { // 从1号点开始枚举
if (!color[i]) { // 如果i号点为染色
if (!dfs(i, 1)) { // 如果i号点染色失败
flg = 0; // flg 打上失败标记
break;
}
}
}
if (flg) cout << "Yes\n";
else cout << "No\n";
return 0;
}
2.2 匈牙利算法找最大匹配
#include
using namespace std;
int const N = 5e2 + 10, M = 1e5 + 10;
int e[M], ne[M], idx, h[N], match[N], st[N]; // match[j] = x: 右半部分的j匹配左半部分的x,st[j] = 1:右半部分的j匹配到人了
int n1, n2, m;
// 建立邻接表
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 找左半部分的x是否能够在右半部分找到匹配的对象
bool find(int x) {
for (int i = h[x]; i != -1; i = ne[i]) { // 遍历所有与x之间相连的点
int j = e[i]; // 点为j
if (!st[j]) { // 如果j没有匹配过
st[j] = 1; // 记录j匹配过
if (!match[j] || find(match[j])) { // 如果右半部分的j没有匹配到左半部分的人或者j匹配到的可以去匹配其他右半部分的人
match[j] = x; // 记录右半部分的j和左半部分的x匹配
return true; // 找到x的匹配对象
}
}
}
return false; // 全部都遍历仍然没有成功,返回false
}
int main() {
memset(h, -1, sizeof h); // 初始化h
cin >> n1 >> n2 >> m; // 读入左半部分、右半部分数目和边数
for (int i = 0; i < m; ++i) { // 读入边信息
int a, b;
scanf("%d %d", &a, &b);
add(a, b); // 只需要add一次即可,因为二分匹配时只需要从左半部分向右半部分匹配就行
}
int res = 0; // 记录结果数目
for (int i = 1; i <= n1; ++i) { // 从左半部分向右半部分匹配
memset(st, 0, sizeof st); // 每次匹配时对右半部分的女生情况匹配情况
if (find(i)) res++; // 如果能够找到,res加一
}
cout << res << endl;
return 0;
}
3. 典型例题
3.1 染色问题
acwing257关押罪犯
题意: 任意两名罪犯间有怒气值c,如果两名怨气值为 c 的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并造成影响力为 c 的冲突事件。问如何安排罪犯,将其分到两个监狱内,使得造成的最大影响力的冲突事件影响力最小。N≤20000,M≤100000
题解: 本题的罪犯可以分成两块,要求每块中最大的怒气值最小。
最大最小问题可以考虑使用二分来处理,二分枚举答案,如果任意两个人的怒气值大于这个答案值,那说明这两个人只能属于不同的两块,所有我们只要不断判断大于这个答案值的所有边能否构成一个二分图即可
代码:
#include
using namespace std;
int n, m;
int const N = 2e4 + 10, M = 2e5 + 10;
int e[M], ne[M], h[N], w[M], idx;
int color[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// 染色法判断二分图
bool dfs(int u, int c, int limit) {
color[u] = c;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (w[i] <= limit) continue;
if (!color[j])
if (!dfs(j, 3 - c, limit)) return false;
if (color[j] == c) return false;
}
return true;
}
// 判断能够二分图
bool check(int limit) {
memset(color, 0, sizeof color);
int flg = 1;
for (int i = 1; i <= n; ++i)
if (!color[i])
if (!dfs(i, 1, limit))
return false;
return true;
}
int main() {
// 建图
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 1; i <= m; ++i) {
int a, b, c;
scanf("%d %d %d", &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;
}
cout << l << endl;
return 0;
}
3.2 二分匹配问题
acwing372 棋盘覆盖
题意: 给定一个N行N列的棋盘,已知某些格子禁止放置。求最多能往棋盘上放多少块的长度为2、宽度为1的骨牌,骨牌的边界与格线重合(骨牌占用两个格子),并且任意两张骨牌都不重叠。1≤N≤100
题解: i+j为偶数看为白格子,i+j为奇数看为黑格子,那么一张棋盘就可以划分为一张二部图,每次只要枚举白格子向黑格子连边即可
代码:
#include
using namespace std;
typedef pairPII;
int const N = 1e2 + 10;
int g[N][N], st[N][N]; // g记录是否不能走,st记录每轮内是否匹配过
pair match[N][N]; // match记录每个点匹配的点
int n, m;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
// 找最大匹配
bool find(PII t) {
// 遍历四周的点
for (int i = 0; i < 4; i ++ ) {
int a = t.first + dx[i], b = t.second + dy[i];
// 如果不在界限内
if (!a || a > n || !b || b > n || g[a][b] || st[a][b]) continue;
// 标记这个点
st[a][b] = true;
// 如果能够找到增广路(该点没有匹配或者和这个点匹配的点还能和其他点匹配)
if (match[a][b].first == 0 || find(match[a][b])) {
match[a][b] = t; // 记录匹配
return true;
}
}
return false;
}
int main () {
cin >> n >> m;
// 读入棋盘情况
for (int i = 1; i <= m; ++i) {
int a, b;
scanf("%d %d", &a, &b);
g[a][b] = 1;
}
// 遍历棋盘上每个点
int ans = 0;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) {
if ((i + j) % 2 == 0 || g[i][j] == 1) // 只选白色或者黑色的遍历且不允许这个位置被禁止
continue;
memset(st, 0, sizeof st); // 该点在这一轮没有匹配过
if (find({i, j})) ans ++; // 如果能够找到匹配
}
cout << ans << endl;
return 0;
}
3.3 二分图的点覆盖集
acwing376 机器任务
题意: 有两台机器 A,B 以及 K 个任务。机器 A 有 N 种不同的模式(模式0~N-1),机器 B 有 M 种不同的模式(模式0~M-1)。两台机器最开始都处于模式0。每个任务既可以在A上执行,也可以在B上执行。对于每个任务 i,给定两个整数 a[i] 和 b[i],表示如果该任务在 A 上执行,需要设置模式为 a[i],如果在 B 上执行,需要模式为 b[i]。任务可以以任意顺序被执行,但每台机器转换一次模式就要重启一次。求怎样分配任务并合理安排顺序,能使机器重启次数最少。
N,M<100,K<1000,0≤a[i]
代码:
#include
using namespace std;
int const N = 110, M = 1010;
int e[M], ne[M], idx, h[N], match[N], st[N]; // match[j] = x: 右半部分的j匹配左半部分的x,st[j] = 1:右半部分的j匹配到人了
int n, m, k;
// 建立邻接表
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 找左半部分的x是否能够在右半部分找到匹配的对象
bool find(int x) {
for (int i = h[x]; i != -1; i = ne[i]) { // 遍历所有与x之间相连的点
int j = e[i]; // 点为j
if (!st[j]) { // 如果j没有匹配过
st[j] = 1; // 记录j匹配过
if (!match[j] || find(match[j])) { // 如果右半部分的j没有匹配到左半部分的人或者j匹配到的可以去匹配其他右半部分的人
match[j] = x; // 记录右半部分的j和左半部分的x匹配
return true; // 找到x的匹配对象
}
}
}
return false; // 全部都遍历仍然没有成功,返回false
}
int main()
{
while (scanf("%d %d %d", &n, &m, &k) != EOF && n != 0) {
memset(h, -1, sizeof h); // 初始化h
memset(e, 0, sizeof e);
memset(ne, 0, sizeof ne);
memset(match, 0, sizeof match);
idx = 0;
for (int i = 0; i < k; ++i) { // 读入边信息
int a, b, c;
scanf("%d %d %d", &c, &a, &b);
if (!a || !b) continue; // 起始点是0,不用覆盖
add(a, b); // 只需要add一次即可,因为二分匹配时只需要从左半部分向右半部分匹配就行
}
int res = 0; // 记录结果数目
for (int i = 1; i <= n; ++i) { // 从左半部分向右半部分匹配
memset(st, 0, sizeof st); // 每次匹配时对右半部分的女生情况匹配情况
if (find(i)) res++; // 如果能够找到,res加一
}
cout << res << endl;
}
return 0;
}
3.4 二分图的点独立集
acwing378 骑士放置
题意: 给定一个 N * M 的棋盘,有一些格子禁止放棋子。问棋盘上最多能放多少个不能互相攻击的骑士(国际象棋的“骑士”,类似于中国象棋的“马”,按照“日”字攻击,但没有中国象棋“别马腿”的规则)。
1≤N,M≤100
题解: 只需要枚举左部图即可,因此i+j为偶数可以continue。然后每个骑士间连一条边,需要求一个点独立集。因此是n * m-最大匹配数目
代码:
#include
using namespace std;
int const N = 1e2 + 10;
int g[N][N], vis[N][N];
pair match[N][N];
int n, m, t;
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
// 匹配
int find(pair t) {
int x = t.first, y = t.second;
// 枚举8个方向
for (int i = 0; i < 8; ++i) {
int nextx = x + dx[i];
int nexty = y + dy[i];
if (nextx < 1 || nextx > n || nexty < 1 ||nexty > m || g[nextx][nexty] || vis[nextx][nexty]) continue;
vis[nextx][nexty] = 1;
if (!match[nextx][nexty].first || find(match[nextx][nexty])) {
match[nextx][nexty] = t;
return true;
}
}
return false;
}
int main()
{
cin >> n >> m >> t;
// 读入禁止点
for (int i = 1; i <= t; ++i) {
int x, y;
scanf("%d %d", &x, &y);
g[x][y] = 1;
}
// 遍历每个点
int ans = 0;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) {
if (g[i][j] || (i + j) % 2 == 0) continue; // 优化时间,只需要取一半的点就可以遍历到所有的情况
memset(vis, 0, sizeof vis);
if (find({i, j})) ans++; // 如果能够匹配到
}
cout << n * m - t - ans << endl;
return 0;
}
3.5 DAG的最小路径覆盖
acwing379 捉迷藏
题意: Vani和cl2在一片树林里捉迷藏。这片树林里有N座房子,M条有向道路,组成了一张有向无环图。树林里的树非常茂密,足以遮挡视线,但是沿着道路望去,却是视野开阔。如果从房子A沿着路走下去能够到达B,那么在A和B里的人是能够相互望见的。现在cl2要在这N座房子里选择K座作为藏身点,同时Vani也专挑cl2作为藏身点的房子进去寻找,为了避免被Vani看见,cl2要求这K个藏身点的任意两个之间都没有路径相连。为了让Vani更难找到自己,cl2想知道最多能选出多少个藏身点。N≤200,M≤30000
题解: 仔细分析可知,藏身地选择每条简单路径的尾部,那么一张图可以被许多条可重复路径的有向边覆盖,因此问题转化为求DAG的最小可重复点路径覆盖。先传递闭包,然后建图,然后求最大匹配
代码:
#include
using namespace std;
int n, m;
int const N = 2e2 + 10, M = 3e4 + 10;
int match[N], vis[N], d[N][N];
// 二分匹配
int find(int x) {
for (int i = 1; i <= n; ++i) {
if (vis[i] || !d[x][i]) continue;
vis[i] = 1;
if (!match[i] || find(match[i])) {
match[i] = x;
return true;
}
}
return false;
}
int main()
{
cin >> n >> m;
// 读入边
for (int i = 1; i <= m ;++i) {
int a, b;
scanf("%d %d", &a, &b);
d[a][b] = 1;
}
// 传递闭包
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 ans = 0;
// 二分匹配,这里把原来的边i复用为新的边i',因为二分图内左半部分内部不会有连线
for (int i = 1; i <= n; ++i) {
memset(vis, 0, sizeof vis);
if (find(i)) ans++;
}
cout << n - ans << endl;
return 0;
}