0. 前言
重点在于代码实现。
一些好的博文总结:
图论中二分图指的是我们可以将所有点划分成两个集合,集合内部没有边,所有边起终点均在不在同一个集合。 基于图论中一个重要性质:一个图是二分图当且仅当图中不含奇数环(环中的边的数量为奇数)。
一般有两种算法最为常用,如下:
- 染色法:基于 d f s dfs dfs,线性时间复杂度 O ( n + m ) O(n +m) O(n+m)
- 匈牙利算法:用来求解二分图的最大匹配,理论最坏时间复杂度为 O ( n m ) O(nm) O(nm),但实际运行时间远小于 O ( n m ) O(nm) O(nm)。
1. 染色法判定二分图
思路:
- d f s dfs dfs 染色,用 1 和 2 区分不同颜色,0 表示未染色
- 遍历所有点(没有说明是连通图),对未染色点进行 d f s dfs dfs,一点颜色确定,则该点所在的连通块中的所有点颜色均确定。默认颜色无所谓。
- 当两个点颜色相同则染色失败
d f s dfs dfs 代码:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5+5;
int n, m;
int h[N], ne[N*2], e[N*2], idx; // 无向图存边数量是两倍
int color[N];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
bool dfs(int u, int c) { // u当前点,c当前点的颜色
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i]) { // 遍历当前点的所有邻点
int j = e[i];
if (!color[j]) { // 如果该邻点未被染色
if (!dfs(j, 3 - c)) return false; // 给它染色,1号点变成2,2号点变成1,则统一为3-c
}
else if (color[j] == c) return false; // 如果当前邻点已被染色,仅判断是否与自己颜色相同即可
}
return true;
}
int main() {
memset(h, -1, sizeof h);
cin >> n >> m;
while (m --) {
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
bool flag = true;
for (int i = 1; i <= n; ++i) { // 图可能不连通,每个点从前往后染色即可
if (!color[i]) { // 如果当前点未被染色
if (!dfs(i, 1)) { // dfs返回false则染色出现问题,在此初始颜色随便定义
flag = false;
break;
}
}
}
if (flag) puts("Yes");
else puts("No");
return 0;
}
b f s bfs bfs 思路:
- 颜色 1 和 2 表示不同颜色, 0 表示 未染色
- 定义
queue
是存PII
,表示 <点编号, 颜色> - 同理,遍历所有点(防止不连通), 将未染色的点都进行
bfs
- 队列初始化将第
i
个点入队, 默认颜色可以是 1 或 2while
(队列不空)- 每次获取队头
t
, 并遍历队头t
的所有邻边- 若邻边的点未染色则染上与队头
t
相反的颜色,并添加到队列 - 若邻边的点已经染色且与队头
t
的颜色相同,则返回false
- 若邻边的点未染色则染上与队头
b f s bfs bfs 代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e5+5;
int n, m;
int e[N*2], ne[N*2], h[N], idx;
int st[N];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
bool bfs(int u) { // 相当于染一个连通块
int hh = 0, tt = 0;
PII q[N]; // 定义队列,<点编号,颜色>
q[0] = {u, 1};
st[u] = 1;
while (hh <= tt) { // 队列不空
auto t = q[hh ++];
int ver = t.first, c = t.second;
for (int i = h[ver]; i != -1; i = ne[i]) { // 遍历队头临边,染色
int j = e[i];
if (!st[j]) {
st[j] = 3 - c;
q[++ tt] = {j, 3 - c};
}
else if (st[j] == c) return false;
}
}
return true;
}
int main() {
cin >> n >> m;
memset(h, -1, sizeof h);
while (m --) {
int a, b;
cin >> a >> b;
add(a, b), add(b, a); // 无向图
}
int flag = true;
for (int i = 1; i <= n; ++i) { // 遍历所有点,防止不连通
if (!st[i]) {
if (!bfs(i)) {
flag = false;
break;
}
}
}
if (flag) puts("Yes");
else puts("No");
return 0;
}
2. 匈牙利算法求二分图最大匹配
二分图的匹配:给定一个二分图
G
,在G
的一个子图M
中,M
的边集{E}
中的任意两条边都不依附于同一个顶点,则称M
是一个匹配。二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
可以将其想象成搞对象,左边集合男生,右边集合女生,存在可能男女 1 对多有备胎的情况。若是大家都能男女全部牵手成功则是一个完美匹配,若你喜欢的妹子已经心有所属,那么你可以和那个哥们协商,在他有备胎的情况下换一个,然后你俩都皆大欢喜,匹配数量加 1。可能,那位兄弟的备胎妹子也已经心有所属,那么递归的协商就可以了。在尝试了所有可能都无法匹配了,那就放弃。
时间复杂度: O ( n m ) O(nm) O(nm),所有男生,最坏情况每个男生遍历所有边。实际时间复杂度远小于此。
注意:
- 枚举男生点集的话,那么边的存储就只是从男生指向女生。因为只会找男生对应的所有女生,而不会寻找女生对应的所有男生。
- 即 ,虽然是无向边,但是也仅存一个方向。
st
数组用来保证本次匹配过程中,第二个集合中的每个点只被遍历一次,防止死循环。match
存的是第二个集合中的每个点当前匹配的点是哪个。- 枚举每次男生时都需要重新将
st
数组置为false
。此时match
数组可能已经有值了,即女孩已经有男友了,但又有什么关系呢?你跟他和跟我不都是只贡献了一条匹配数吗?所以可以直接将其预定了,然后如果她没男友则直接匹配成功,她有男友,你需要给那位兄弟重新找个备胎,但是由于当前这个妹子已经被你预定了,所以那位兄弟只能另寻他人,如果找到了则皆大欢喜,如果找不到那么你就赶紧滚蛋,别缠着这妹子了。 尝试完所有的心上人妹子,都不行,那么你就拜拜了~返回false
即可。 - 所以,每次枚举男生时初始化
st
数组是因为每个男生预定的妹子都不一样,且保证在调整时,每个妹子只能被选一次,不能重复选择。也可以理解为,后选的人优先级会高,他可以先预定了这妹子,管她有没有男友。没男友,直接拿下。有男友,让男友去递归的预定他的备胎,重复这个过程。所以每次初始化是为了在递归过程中保证每个女生只被选一遍。
代码:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 505, M = 1e5+5;
int n1, n2, m;
int h[N], e[M], ne[M], idx;
int match[N]; // 妹子牵手的男生
bool st[N]; // 判重不要重复搜一个点
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
// 将男生x加入,如果x来加入匹配,是否能让匹配数增多
bool find(int x) {
for (int i = h[x]; i != -1; i = ne[i]) { // 遍历自己的妹子
int j = e[i]; // j为妹子编号
if (!st[j]) { // 如果妹子j还没匹配
st[j] = true; // x预定j成功,调整时前男友就别看了
if (match[j] == 0 || find(match[j])) { // 如果j没男友,或者可以给那个男生找到备胎,则匹配成功
match[j] = x; // 即便匹配失败,也不更改结果,匹配总数不减小
return true;
}
}
}
return false;
}
int main() {
cin >> n1 >> n2 >> m;
memset(h, -1, sizeof h);
while (m --) {
int a, b;
cin >> a >> b;
add(a, b);
}
int res = 0; // 当前匹配数量
for (int i = 0; i <= n1; ++i) { // 分析下每个男生应该找哪个妹子
memset(st, false, sizeof st); // 每次的预定情况都不一样,所有每轮都清空,保证在递归过程中每个女生只能被选一次
if (find(i)) res ++;
}
cout << res << endl;
return 0;
}
3. 总结
二分图问题也是图论中的秀儿问题,很多题目建图时藏匿的很深,尤其是二分图。很不好抽象和转化。
但是,万事开头难。理解算法思想,背好模板,从简单题入手,增加熟练度是最为重要的。
染色法比较简单,很容易理解,相邻不相同。
匈牙利也很形象,我抢一手,你让一手,我们皆大欢喜。