首先介绍一下什么是二分图:二分图又称作二部图,是图论中的一种特殊模型,设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。
官方定义有点抽象,其实就是我们可以把一个图分成两个集合,集合内部是没有变的,只有两个集合间才有边,也就是像这样的:
你可能看不出来,其实这是个二分图,我们变形一下:
现在再看,是不是清晰多了,我们将图划分成了两个集合,集合内部是没有边的,而在集合之间存在边。
二分图的一个等价定义是:不含奇数条边的环,为什么呢?其实很简单:
上图就是一个奇数环,我们对其染色,如果我们的这个是二分图,那我们由一条边相连的两个点的颜色应该是不同的,但是这里有了冲突,那么它就不是二分图。
这就引出了我们如何判断一个图是否为二分图:染色法
染色法判断二分图
我们对第一个例子做一个模拟:
先把第一个点染成真实一点的颜色:
然后邻边染上不同的颜色:
继续
最后
至此,我们的染色结束,在染色的过程中,我们并没有发生冲突,那就说明此图确实是二分图,如果且一旦发生冲突,那就说明这个图不是二分图,下面看一道模板题:
直接给出模板:
//染色法判断二分图
#include<iostream>
#include<cstring>
using namespace std;
int n, m;
const int N = 1e6;
int h[N], e[N], ne[N], idx;
int color[N];
void add(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
bool dfs(int cur, int c) { //给当前点染上c这种颜色,并判断是否冲突,冲突返回false,否则返回true
color[cur] = c; //染色
for(int i = h[cur]; i != -1; i = ne[i]) { //枚举每条邻边
int j = e[i];
if(!color[j]) { //如果还没染色
if(!dfs(j, 3 - c)) return false; //把1变成2,把2变成1,并递归判断是否冲突
} else if(color[j] == c) { //如果染了颜色,但是已经和现有的冲突了,直接返回false
return false;
}
}
return true; //如果本次染色没有发生冲突,返回true
}
int main() {
memset(h, -1, sizeof h);
cin >> n >> m;
while( m -- ) {
int u, v;
cin >> u >> v;
add(u, v);
add(v, u);
}
bool flag = true;
for(int i = 1; i <= n; ++i) {
if(!color[i]) { //如果没有染过颜色
if(!dfs(i, 1)) { //把这个点染成第一种颜色
flag = false; //如果在染色过程中发生了冲突
break;
}
}
}
if(flag) cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}
二分图的最大匹配(匈牙利算法)
首先要明白匹配的概念:“任意两条边都没有公共端点” 的边的集合被称为图的一组匹配。
而所谓的最大匹配:包含边数最多的一组被称为二分图的最大匹配,二分图的最大匹配其实就是特殊的最大流问题
增广路径:该算法也被称为增广路算法,每次从左部的点找与右部匹配的点时,我们需要去找一条增广路径,如果该路径存在,说明可以将目前的所有点进行匹配,反之则右部则没有与之左部相匹配的点。
我们引入有趣的游戏来模拟一下这个过程,假设下面这张图中,左边是男生,右边是妹子(自古红蓝出CP嘛),黑色的连线代表他们之间互有好感,我们的任务就是,让尽可能多的男生和女生匹配,我们就来模拟一下匈牙利算法的过程
首先第一步,一号男生和三号女生有好感,撮合他们,将他们用月老的红线连起来:
然后二号男生和一号女生有好感,将他们连起来
下一步,三号男生对二号女生有好感,但是问题来了,人家二号女生有对象了!!那怎么办呢?三号男生依旧不依不挠,本着坚持不退缩的勇气!他发现二号女生的对象——一号男生,居然还对四号女生有好感,啧啧,这机会不就来了吗,于是他对一号男生说,”兄弟,我觉得你和4号挺有缘的,要不你把你的女朋友让给我呗“,一号男生一听,好像有那么点道理,而且自己和二号女生相处这么久确实也腻了,于是就嘿嘿嘿。
于是经过一番骚操作之后,就变成了:一号男生把二号妹子绿了,然后和四号妹子牵手了,这时候三号男生终于实现了人生理想,和二号妹子牵手了。
下一步,四号男生对三号妹子互有好感,且三号妹子是单身,直接红线安排:
好了,红线连接的,就是二分图的最大匹配了。hhh
下面来看一道模板题,同时我给出代码板子:
代码模板:
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1e6;
int h[N], ne[N], e[N], idx;
int match[N];
bool vis[N];
//vis数组用来保证本次匹配过程中,第二个集合中的每个点只被遍历一次,防止死循环。
//match存的是第二个集合中的每个点当前匹配的点是哪个,但就算某个点当前已经匹配了某个点,也有可能被再次遍历,所以不能起到判重的作用。
int ans;
void add(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++ ;
}
bool find(int x) {
for(int i = h[x]; i != -1; i = ne[i]) { //遍历这个男生有好感的每个妹子
int j = e[i]; //j就是妹子的编号
if(!vis[j]) { //如果这个妹子还是单身
vis[j] = true; //通过先赋值vis[j]为true,这样在find(match[j])时,不会重复再找该女生
if(match[j] == 0 || find(match[j])) { //如果这个妹子没牵过手,或者说他现在的对象可以把她绿掉
match[j] = x; //那么就牵手成功
return true;
}
}
}
return false; //如果一次都没成功,那就失败了
}
int main() {
memset(h, -1, sizeof h);
int n1, n2, m;
cin >> n1 >> n2 >> m;
while(m -- ) {
int u, v;
cin >> u >> v;
add(u, v); //因为我们每次都是从男生开始选择女生,所以只加单向边
}
for(int i = 1; i <= n1; ++i) {
memset(vis, false, sizeof vis); //不管你有没有对象,我都要试试!
if(find(i)) { //如果帮i找到了对象,匹配数+1
ans ++ ;
}
}
cout << ans << endl;
return 0;
}
二分图的染色判定和最大匹配都是非常简单而且实用的算法,如果代码有地方看不懂的话欢迎留言哦。
结尾彩蛋:
一定要记得尝试,一定不要退缩
很多时候,让我们后悔的事并不是做错,而是错过
Comments NOTHING