[OI] 二分图
1 定义
二分图为一个无向图,满足:这个无向图可以被分成两部分,其中每部分内不存在边.
2 判定
奇环法:一张图是二分图,当且仅当图中不存在奇环.
染色法:一张图是二分图,当且仅当图能被染成两种颜色,且没有相邻的点颜色相同.
一般我们判断二分图利用的是染色法.
BFS 染色法
bool check(int m){
queue<int> q;
int c[20001]={};
for(int i=1;i<=n;++i){
if(!c[i]){
q.push(i);
c[i]=1;
while(!q.empty()){
int u=q.front();
q.pop();
for(edge i:e[u]){
if(i.w>=m){
if(!c[i.to]){
q.push(i.to);
if(c[u]==1) c[i.to]=2;
else c[i.to]=1;
}
else if(c[i.to]==c[u]) return false;
}
}
}
}
}
return true;
}
3 匈牙利算法
假如我们在一个二分图中找到了若干边(称为匹配边),并且全部匹配边没有公共点,那么我们就称其为二分图的一个匹配.
增广路指的是按未匹配边、匹配边、未匹配边.... 的顺序,最终到达一个未匹配点的路径. 按这样的路径,依次将路径中的匹配边与未匹配边取反,最终匹配边个数会多一个.
匈牙利算法即是根据增广路性质写成的. 与增广路不同的是,匈牙利算法不管找什么边都是固定从某一侧开始,这也就决定了这个算法不会被环路卡死.
代码:
bool dfs(int now){
for(int i:e[now]){
if(!vis[i]){
vis[i]=1;
if(!match[i]||dfs(match[i])){
match[i]=now;
return true;
}
}
}
return false;
}
int mat(){
memset(match,0,sizeof match);
int ans=0;
for(int i=1;i<=n;++i){
memset(vis,0,sizeof vis);
if(dfs(i)) ans++;
}
return ans;
}
4 二分图最大匹配,最小顶点覆盖,最大独立集
前置条件:是一个二分图
4.1 二分图最大匹配 题库 B D E F
定义: 边的集合,满足任何两条边都没有公共点,二分图最大匹配是这个集合的最大值.
求法: 跑一边匈牙利即可.
用途:
对于满足如下性质的问题给出答案
-
每个选择都有两种条件
-
两种条件至少选择一个时,选择有贡献
-
求所有选择的最大贡献
解决这类问题时,通常先找出选项的两个条件(可能不止两个,也可能会有变形),然后对每个选项的条件连有向边.
#4.2 最小顶点覆盖 题库 C
定义: 点的集合,使得图中每条边在集合中都有公共点,最小顶点覆盖为该集合最小值.
求法: 数值上等于二分图最大匹配
用途:
对于满足如下性质的问题给出答案:
-
每个选择都有两种条件
-
两种条件至少选择一个时,选择成立
-
要求全部选择成立,求最少满足条件总数
4.1 4.2 判断方法 : 一个是最少,一个是最多,在题面上体现.
4.3 最大独立集 题库 G H I
定义: 点的集合,使得集合内任何两点都没有相连的边.
求法: 节点总数-二分图最大匹配
用途:
“没有相连的边”启示我们去寻找冲突. 没有相连的边就是没有冲突,那么按照题意,没有冲突的就是答案. 因此这类题的基本步骤是这样的:
-
寻找冲突
-
对冲突的对象连边(注意不是对冲突本身连边). 如 A 选 x,不选 y,B 选 y,不选 x,那么需要在 A 和 B 连边,而不是 x 和 y 连边.
-
以下两种写法是等价的:
#1
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
if(like[i]==dislike[j]||dislike[i]==like[j]){
e[i].push_back(j);
}
}
}
#2
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
if(like[i]==dislike[j]){
e[i].push_back(j);
e[j].push_back(i);
}
}
}
5.一些小问题
5.1 建单向边还是双向边
还是那个问题,上面那两种代码是等价的,这是我们形式意义上的单向边或者双向边,虽然看起来不一样,但实际效果相同.
但是实际效果建单向边也是可行的. 因为我们在跑匈牙利的时候避免了这样的问题,而采用了每次都从左边寻找前往右边的点的这样一种方法,因此不会造成什么影响. 但这样会导致边数变成了原来的二倍,反过来跑的时候,两个对称节点就都满足条件了,因此答案会变成之前的二倍,所以真正结果是要除以二的. 相反,对真正的单向边则不需要这样.
假如你真的不知道,就多输出几个数,看看哪个能对上大样例.
5.2 能避免初始化的方法
匈牙利里每次 DFS 都要初始化,很费时间. 为此我们可以开一个整型的数组来记录,而不是布尔类型,每次搜索都赋一个时间戳,假若当前记录数组不等于时间戳就说明当前没访问过,这样就不用初始化了.