这是AcWing算法分享系列的第一篇文章,我们先从图论的知识下手(因为我觉得图论的只是好理解些)。
这次我们主要讲的就是二分图,二分图这次我们主要讲的就是最基础的两个板块:
- 二分图的判定(染色法)
- 二分图的完美匹配(匈牙利算法)
我们这一篇文章先从二分图的概念开始入手吧。
二分图(偶图)
概念
概念:在一个图中,如果能够把全部点分到两个集合中,且每一个集合都没有边与同一个集合中的点联通,这样的图就是二分图(也叫偶图)。
tips:二分图这个性质通常用于无向图,但是有些有向图也有二分图的性质。
不懂,下面这张图就是一个二分图:
我们可以发现,两个集合中就没有边的相连,所以,这张图就是一个二分图。
性质
- 二分图中不可能含有奇数环,因为每一次的一个点需要走偶数次才能走回自己所在的集合,所以二叉树中不能有奇数环
- 不含有奇数环的一定是二分图,这个引理是可以证明的,如果想知道可以上度娘看一下,笔者这里也不过多证明了(
是我不会)。
染色法
染色法是用来判断这个图是不是二分图的。
原理
所谓染色法,只不过就是爆搜,其原理特别简单,如下。
染色法:把一个图中的点涂成黑色或白色两种颜色,如果这个点涂白色,那么与它相邻的点就涂黑色,反之涂白色,然后我们只需要判断其中是否有一个点有两种颜色,如果这个点有两种颜色的话就证明了这个图不是一个二分图。
实现
染色法就是用dfs或者是dfs扫一遍,给每个颜色都涂个色就可以了,思路都已经放在上面了,代码的实现就很简单了。
int n, h[100005], e[100005], ne[10005], idx; // 邻接表存储图,n表示点数
int color[100005];// 表示每个点的颜色,-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;
}
匈牙利算法
概念
匈牙利算法主要是求解二分图的最大匹配数问题和最小点覆盖数问题的。
在这里,我们目前只需要讲一下最大匹配数问题,那么什么叫做最大匹配数问题呢?
最大匹配数
最大匹配数是什么呢?我们来看一下。
两个集合的的点互相匹配的最大数就是这个二分图的最大匹配数,互相匹配指的是两个集合中的点在已有边的情况下之后的连边,每一个点只能匹配一个另一个集合的点,不能一个点匹配多个点。
如果你觉得很难懂,那请你们换一方法想。如果吧左边的集合划分成男生,右边的集合划分成女生,男女左右两边连接的先就代表这两个男生和女生之间有关系(不要想偏了,这就是匈牙利算法被戏称为恋爱算法的原因)
当然,男生和男生的集合里边不能有关系,女生和女生之间也不能有关系(这样就不叫二分图了)。
最大匹配数的精髓就在于:如果你是一名红娘,你就要在男生和女生的集合中选出,一共有多少对情侣(情侣之间必须要有关系,并且一个男生或者是一个女生最多有一个对象).
思想
接着,我们来看匈牙利算法是怎么进行运作的。
还是看上面这个图,我们一般都是从左边的集合,也就是男生开始遍历的。
先来看:第一个男生喜欢第二个和第四个女生,我们就暂且把他和最近的一个女生(第二名女生)做成一对情侣。
接着看第二位男生,他喜欢第二个女生,可是另一个女生已经有另一半了,怎么办呢?我们追根溯源,看这个女生所配对的男生有没有另外一个喜欢的女生,答案是肯定的,这样答案就完美了,我们把第一个男生和第四个女生相连接,这样第二个男生就有另一半了,就把他和第二个女生相连(好羡慕他们啊)qwq
那么加入另一个男生也很专一怎么办呢?那没办法了只能做单身狗了qwq。
实现
其实匈牙利算法的本质思想不难,实现也很简单,只要懂了思路,基本上都会做了。
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专栏的第一篇博客,这一个专栏主讲一些高级的数据结构和算法,希望大家多多支持。