[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

定义: 边的集合,满足任何两条边都没有公共点,二分图最大匹配是这个集合的最大值.

求法: 跑一边匈牙利即可.

用途:

对于满足如下性质的问题给出答案

  1. 每个选择都有两种条件

  2. 两种条件至少选择一个时,选择有贡献

  3. 求所有选择的最大贡献

解决这类问题时,通常先找出选项的两个条件(可能不止两个,也可能会有变形),然后对每个选项的条件连有向边.

#4.2 最小顶点覆盖 题库 C

定义: 点的集合,使得图中每条边在集合中都有公共点,最小顶点覆盖为该集合最小值.

求法: 数值上等于二分图最大匹配

用途:

对于满足如下性质的问题给出答案:

  1. 每个选择都有两种条件

  2. 两种条件至少选择一个时,选择成立

  3. 要求全部选择成立,求最少满足条件总数

4.1 4.2 判断方法 : 一个是最少,一个是最多,在题面上体现.

4.3 最大独立集 题库 G H I

定义: 点的集合,使得集合内任何两点都没有相连的边.

求法: 节点总数-二分图最大匹配

用途:

“没有相连的边”启示我们去寻找冲突. 没有相连的边就是没有冲突,那么按照题意,没有冲突的就是答案. 因此这类题的基本步骤是这样的:

  1. 寻找冲突

  2. 对冲突的对象连边(注意不是对冲突本身连边). 如 A 选 x,不选 y,B 选 y,不选 x,那么需要在 A 和 B 连边,而不是 x 和 y 连边.

  3. 以下两种写法是等价的:

#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 都要初始化,很费时间. 为此我们可以开一个整型的数组来记录,而不是布尔类型,每次搜索都赋一个时间戳,假若当前记录数组不等于时间戳就说明当前没访问过,这样就不用初始化了.

posted @ 2024-05-12 19:10  HaneDaniko  阅读(42)  评论(0编辑  收藏  举报