二分图
二分图:可以将一个图中的所有点吗,分成左右两个部分,使得图中的所有边,都是从左边集合中的点连到右边集合中的点。而左右两个集合内部没有边。
再通俗点理解:一个图是二分图当且仅当图中不含奇数环(奇数环:边数为奇数的环)
1.染色法–判断二分图
对每一个点进行染色操作,只用黑白两种颜色;用dfs和bfs两种方式去实现,对图进行遍历并染色。时间复杂度为O(n+m)
定理:二分图一定不含奇数环,不含奇数环的图一定为二分图。
充分性:如果图中存在奇数环(构成环的顶点数量是奇数),那一定不是二分图),下图可以看到,依次选一个点,进行染色(原则是相邻的点要染于该点不同色),奇数环的染色结果会出现矛盾;
必要性:如果没有奇数环,那么剩下的点的关系就是:偶数环or单链。这两种情况都能保证同一条边上相邻顶点在不同集合中,所以也是成立的;
综上:只要在染色过程中不存在矛盾(这里用黑白进行染色,即一个点不能即为黑色,又为红色),整个图遍历完成之后,所有顶点都顺利染上色。就说明这是一个二分图。
板子:
//时间复杂度是 O(n+m) n表示点数,m表示边数
int n; // n表示点数
int h[N], e[M], ne[M], idx; // 邻接表存储图
int color[N]; // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色
// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c)
{
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (color[j] == -1)
{
if (!dfs(j, !c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
bool check()
{
memset(color, -1, sizeof color);
bool flag = true;
for (int i = 1; i <= n; i ++ )
if (color[i] == -1)
if (!dfs(i, 0))
{
flag = false;
break;
}
return flag;
}
Acwing 860 .染色法判定二分图
实现思路:这是使用深度优先遍历DFS实现(代码比BFS短点)
- 使用邻接表存储图;
- 使用一个数组color代表当前点的染色,若为-1则未染色,若为0则为白色,若为1则为黑色;
- 设置一个标志变量flag判断是否矛盾。然后循环遍历各点,若未染色则进行染色2(染为0或1),对该点进行深度优先遍历,对其连通的点进行判断。
-
- 若未染色,则染上与该点相反的颜色;若已染色,判断是否与当前点的颜色相同;
-
-
- 若相同则出现矛盾,提前返回false,得到结果不是二分图,否则继续遍历判断、染色
-
- 直到循环结束,判断标志变量flag,为真则为二分图。
具体实现代码(详解版):
#include <iostream>
#include <cstring> // 用于 memset 函数
#include <algorithm> // 可选的库,实际代码中未使用
using namespace std;
const int N = 100010, M = 200010; // N 为最大节点数,M 为最大边数的两倍(无向图需要存储双向边)
int h[N], e[M], ne[M], idx; // h[] 是邻接表的头节点数组,e[] 存储边的终点,ne[] 存储下一条边的下标,idx 为边的下标
int color[N]; // 颜色数组,-1 表示未染色,0 和 1 表示两种颜色
int n, m; // n 为节点数,m 为边数
// 添加一条从节点 a 到节点 b 的边
void add(int a, int b) {
e[idx] = b; // 将 b 节点存储在边数组 e[] 中
ne[idx] = h[a]; // 将边的下一条边设置为 h[a] 所指向的边
h[a] = idx++; // 更新 h[a] 为新加入的这条边的下标,idx 自增
}
// 深度优先搜索函数,u 为当前遍历的节点,c 为当前节点的颜色
// 若图中出现矛盾(即相邻节点颜色相同),返回 false
bool dfs(int u, int c) {
color[u] = c; // 给当前节点 u 染上颜色 c
// 遍历所有与节点 u 相连的边
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i]; // j 为与 u 相连的节点
if (color[j] == -1) { // 如果节点 j 未被染色
if (!dfs(j, !c)) return false; // 递归对 j 进行染色,颜色与 u 相反
}
else if (color[j] == c) return false; // 如果 j 已染色且颜色与 u 相同,则图中有矛盾
}
return true; // 没有发现矛盾,返回 true
}
int main() {
cin >> n >> m; // 输入节点数 n 和边数 m
memset(h, -1, sizeof h); // 初始化邻接表头节点数组 h[],全设为 -1
memset(color, -1, sizeof color); // 初始化颜色数组,所有节点初始为未染色状态
// 读取 m 条边
while (m--) {
int a, b;
cin >> a >> b;
add(a, b); // 添加从 a 到 b 的边
add(b, a); // 因为是无向图,也需要添加从 b 到 a 的边
}
bool flag = true; // 用来标记是否是二分图
// 遍历每个节点,检查是否有未染色的节点
for (int i = 1; i <= n; i++) {
if (color[i] == -1) { // 如果当前节点未被染色
if (!dfs(i, 1)) { // 尝试以颜色 1 开始对该节点进行染色
flag = false; // 如果染色过程中发现矛盾,说明不是二分图
break; // 不需要继续检查,直接退出循环
}
}
}
// 输出结果
if (flag) puts("Yes"); // 如果 flag 仍为 true,则图是二分图
else puts("No"); // 如果 flag 为 false,则图不是二分图
return 0;
}
2.匈牙利法–二分图的最大匹配
- 匹配(本质是一个边的集合!)
-
- 给定一个二分图S,在S的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配;
- 极大匹配
-
- 极大匹配是指在当前已完成的匹配下,无法再通过增加未完成匹配的边的方式来增加匹配的边数。(也就是说,再加入任意一条不在匹配集合中的边,该边肯定有一个顶点已经在集合的边中了)
- 最大匹配
-
- 所有极大匹配当中边数最多的开匹配;
下图是一个最大匹配(黄色边):
- 所有极大匹配当中边数最多的开匹配;
通俗一点的理解:
现实 | 比喻 |
---|---|
me | 月老 |
左侧蓝色集合点 | 男嘉宾组 |
右侧红色集合点 | 女嘉宾组 |
中间的边(考虑所有) | 有恋爱的可能(可以成为一对) |
匹配 | 一个男嘉宾只能牵手一位女嘉宾(女嘉宾也只能选中一个男嘉宾);一对 一 |
最大匹配 | 根据场上的恋爱可能关系,最多牵手多少对! |
匈牙利算法:两个集合中都存在一些点,以左集合出发,查找两个集合之中匹配成功的点的个数。如果左集合的某一个点发现与自己相连的节点已经被占有,则查询占有该节点的左集合的点是否有其他可配对的点,若有则两全其美,否则继续寻找,若仍未找到,则配对失败。时间复杂度理论上是O(nm),但实际运行时间一般远小于O(nm)。
通俗点说:从左边开始,左边a喜欢右边b(即两者有连线),若b此时没有已经匹配的人或者即便有匹配的人,那个匹配的对象可以找到其他人(下家)来配对,即把b让给a,则a与b匹配成功。若两种情况下都不满足,则a只能找下一个有好感的人尝试匹配,直至匹配成功或已经没有喜欢的了。进行下一个人的匹配,最终得到的匹配对数即为二分图的最大匹配对。
板子:
int n1, n2; // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx; // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N]; // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N]; // 表示第二个集合中的每个点是否已经被遍历过
bool find(int x)
{
for (int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true;
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ )
{
memset(st, false, sizeof st);
if (find(i)) res ++ ;
}
Acwing 861.二分图的最大匹配
实现思路:
- 使用邻接表存储图;
- 设置一个判重数组s[],s[i]为真表示i节点已经被当前节点尝试过,每次换一个人都要把数组s[]重置为false;设置一个匹配数组match[],为0表示当前节点还没有匹配的对象,否则为匹配对象的编号;
- find函数(找匹配对象):每次对相连的点尝试匹配,若当前连接点未尝试过,则尝试;若该点没有匹配对象或者它的匹配对象可以把该节点让给我,则与之匹配,退出匹配成功,下一个继续。
具体实现代码(详解版):
#include <cstring> // 用于 memset 函数
#include <iostream> // 用于输入输出
using namespace std;
const int N = 510, M = 100010; // N 表示左侧集合中的最大点数,M 表示边的最大数量
int h[N], ne[M], e[M], idx; // 邻接表的实现
int match[N]; // 匹配数组,记录右侧集合中的点匹配到的左侧集合中的点
bool s[N]; // 判重数组,用于标记在每次匹配中右侧的节点是否已经被访问过
int n1, n2, m; // n1: 左侧集合中的点数,n2: 右侧集合中的点数,m: 边的数量
// 添加一条从 a 到 b 的边
void add(int a, int b) {
e[idx] = b; // b 是与 a 相连的节点
ne[idx] = h[a]; // 当前 a 的邻接表头节点为 h[a]
h[a] = idx++; // 将 b 添加到 a 的邻接表中,并将 idx 自增
}
// 寻找左侧点 x 的匹配对象
bool find(int x) {
// 遍历与 x 相连的所有点
for (int i = h[x]; i != -1; i = ne[i]) {
int j = e[i]; // j 是 x 相连的右侧集合中的一个点
if (!s[j]) { // 如果右侧点 j 没有被访问过
s[j] = true; // 标记右侧点 j 已访问
// 如果 j 还没有匹配,或者 j 的现有匹配对象可以重新匹配
if (match[j] == 0 || find(match[j])) {
match[j] = x; // 将 j 匹配到 x
return true; // 匹配成功
}
}
}
return false; // 匹配失败
}
int main() {
cin >> n1 >> n2 >> m; // 输入左侧集合的点数、右侧集合的点数和边的数量
memset(h, -1, sizeof h); // 初始化邻接表,h[i] 为 -1 表示该点没有相连的边
// 输入所有边
while (m--) {
int a, b;
cin >> a >> b;
add(a, b); // 在邻接表中添加边 a -> b
}
int cnt = 0; // 记录最大匹配数
// 遍历左侧集合中的每个点,尝试为其找到匹配
for (int i = 1; i <= n1; i++) {
memset(s, false, sizeof s); // 每次尝试为一个左侧点匹配时,重置访问标记数组 s
if (find(i)) cnt++; // 如果找到匹配,匹配数加 1
}
// 输出最大匹配数
cout << cnt << endl;
return 0;
}
下面是二分图简单的一个总结:
类别 | 描述 |
---|---|
定义 | 二分图是可以将节点划分为两个不相交的子集,使得图中所有边都连接两个不同子集的节点。 |
性质 | - 无奇数长度的环 |
判定方法 | - 染色法:通过DFS/BFS染色判断相邻节点颜色是否相同,若相邻节点颜色相同则不是二分图。 |
算法 | - 染色法:DFS或BFS遍历图,给节点染色。 - 匈牙利算法:解决二分图最大匹配问题。 |
应用场景 | - 最大匹配问题:从左集和右集之间找到最多的配对。 - 任务分配:将工人与任务匹配。 - 网络流问题:通过最大流求解。 |
判定代码示例 | 使用深度优先搜索(DFS)进行染色判定是否是二分图。 |