目录
二分图概念
二分图应用场景
如何判定一个图是否可以划分成二分图
证明
染色法
原理步骤
时间复杂度
算法思路
例子
匈牙利算法
概念
匈牙利使用算法前提,场景
时间复杂度
算法思路
例子
二分图概念
二分图是图论中的一个重要概念,指的是一个图的顶点集可以被分为两个互不相交的子集,并且图中的每条边都连接两个不同子集中的顶点。换句话说,如果一个图是二分图,那么可以将图中的所有顶点分为两组,使得每条边的两个端点分别属于不同的组。
二分图应用场景
-
匹配问题:在二分图中,匹配问题是指找到一种边的子集,使得图中每个顶点都与子集中的某条边相邻。在实际应用中,可以用于匹配求职者和工作岗位、学生和导师等场景。
-
任务分配:在二分图中,任务分配问题是指将一组任务分配给一组工人,使得每个任务只被分配给一个工人,每个工人最多只能完成一个任务。这在实际生活中可以用于工作排班、资源分配等问题。
-
流网络:在网络流问题中,二分图可以表示一种特殊的流网络,其中顶点集分为源点集和汇点集,边表示从源点到汇点的流量路径,用于建模输送网络中的流量分配和优化问题。
-
电路布线:在电路布线问题中,可以将待连接的元器件和连接线分别看作二分图中的顶点和边,通过最小化连接线的长度或者最小化连接的总成本来优化电路的布线。
-
等等
如何判定一个图是否可以划分成二分图
二分图当且仅当图中不含有奇数环
证明
- 充分性:(不含有奇数环的图可以划分成二分图)
使用一种方法(染色法),随意选择 图中一个点 ,将其分到集合 ,然后将其邻接的所有点分到集合 ,再将分到 的点的邻接点分到 。以此类推,因为图中不含有奇数环,所以分类过程中一定没有矛盾。
如何证明没有矛盾:
反证:假设图中不含有奇数环,但分类过程中出现了矛盾。
假设有一个偶环 (任意相邻两点有边连接,且 和 之间有一条边相邻)。该环中出现了矛盾,使用染色法时, 和 被分到同一个类别 。
首先,我们先对该环按顺序依次进行分类, 分到 , 分到 , 分到 。
以此类推,可知编号为奇数的都属于 集合,编号为偶数的都属于 集合。因此 , 被分到 。并且, 相邻 ,也应该分到 。
与假设不符,因此假设错误,说明图中不含有奇数环,分类过程中一定没有矛盾。
- 必要性:(二分图不含有奇数环)
反证:假设该二分图含有一个奇数环,任意相邻两点有边连接,且 和 之间有一条边相邻。
假设 集合,则 ,。以此类推,可知编号为奇数的都属于 集合,编号为偶数的都属于 集合。
那么 和 都属于 集合, 和 之间有一条边。与二分图同一个集合中的点不相连矛盾,因此假设错误。二分图不存在奇数环。
染色法
判断一个图是不是二分图。
原理步骤
- 选择一个起始顶点,将其染成一种颜色(比如红色)。
- 将与该顶点相邻的顶点染成另一种颜色(比如蓝色)。
- 依次对与 已染色的顶点 相邻的未染色顶点进行染色,要求相邻顶点的颜色不能相同。
- 如果在染色的过程中,发现有相邻的两个顶点被染成了相同的颜色,则说明该图不能划分成二分图;否则,当所有顶点都被染色后,该图可以划分成二分图。
时间复杂度
O(m+n)
算法思路
可以使用bfs,dfs,并查集遍历图。
伪代码 BFS版:
main:
Queue q
循环n次 // 有可能有多个连通图
如果当前点 t 没被访问过,q <- t
对 t 进行染色
进行bfs()
bfs():
while q 不空:
t <- 取出队头
遍历 t 的邻接的点:
如果邻接的点的色与 t 的颜色相同或者是个自环 说明不能构成一个二分图
否则:将邻接点染色,并加入队列
例子
860. 染色法判定二分图 - AcWing题库
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环。
请你判断这个图是否是二分图。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 u 和 v,表示点 u 和点 v 之间存在一条边。
输出格式
如果给定图是二分图,则输出
Yes
,否则输出No
。数据范围
1≤n,m≤10^5
输入样例:
4 4 1 3 1 4 2 3 2 4
输出样例:
Yes
dfs:AcWing 860. 染色法判定二分图 - JAVA -DFS - AcWing
并查集:AcWing 860. 看不懂dfs和bfs可以来康康并查集 - AcWing
使用BFS + 数组模拟邻接表:
import java.io.*;
import java.util.*;class Main{static int N = 100010;static int n,m,idx;static int[] h = new int[N];static int[] e = new int[2*N];static int[] ne = new int[2*N];static int[] w = new int[N]; // 染色记录static boolean[] st = new boolean[N]; // 标记是否染过色static Queue<int[]> q = new LinkedList<>();public static void main(String[] args) throws IOException{BufferedReader in = new BufferedReader(new InputStreamReader(System.in));String[] s = in.readLine().split(" ");n = Integer.parseInt(s[0]);m = Integer.parseInt(s[1]);Arrays.fill(h,-1);int flag = 1;while(m-->0){s = in.readLine().split(" ");int a = Integer.parseInt(s[0]);int b = Integer.parseInt(s[1]);add(a,b);add(b,a); }for(int i=1;i<=n;i++) { // 有可能有多个连通图,因此每个点都需要遍历if(!st[i]) { // 如果没被染色q.add(new int[]{i,1}); // 染色为1st[i] = true;if(color()==-1) flag = 0; // 如果有奇数环,返回-1}}if(flag==0) System.out.println("No");else System.out.println("Yes");}// 染色public static int color(){while(!q.isEmpty()){int[] t = q.poll();int u = t[0]; // 点int weight = t[1]; // 被染色的值for(int i=h[u];i!=-1;i=ne[i]){ // 对邻接点进行染色int j = e[i];if(w[j]==weight||j==u) return -1; // 如果该点的颜色与邻接点相同或者有自环,直接返回if(!st[j]){ // 如果该点没被染色w[j] = 3-weight; // 染色q.add(new int[]{j,w[j]}); st[j] = true; // 表示为已经被访问过} }}return 1;}// 添加边public static void add(int a,int b){e[idx] = b;ne[idx] = h[a];h[a] = idx++;}
}
匈牙利算法
概念
- 匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点。
- 最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。
- 完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。
- 交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
- 增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替 路称为增广路(agumenting path)。
匈牙利使用算法前提,场景
已知该图是二分图,求最大匹配数。
可以想象成:二分图,左边集合是女生,右边是男生,两个集合之间男生和女生连接了线,表明这两人可以成为恋爱关系。但是一个男生只能和一个女生谈恋爱,求最多能成为恋爱关系的情侣数量。
-
任务分配:在任务与执行者之间存在偏好关系时,可以使用匈牙利算法将任务分配给执行者,使得每个执行者最多承担一个任务,并且尽可能多的任务得到执行。
-
最大流问题:匈牙利算法可以用作最大流算法的一部分,用于寻找增广路径,从而找到最大流量。
-
资源分配:在资源有限的情况下,可以使用匈牙利算法来优化资源的分配,以满足不同资源需求的任务或需求者。
时间复杂度
O(mn),实际一般小于mn
算法思路
该二分图中,一个集合 中有 个顶点,另一个集合 中有 个顶点。
st[ ] :
伪代码:
// 用 匹配 ,只需要记录 到 的单向边
mian():
循环 n1 次:
对该点进行匹配
:
遍历 点所有能匹配上的 中的所有点:
如果 当前匹配到的点 没有匹配过 或者 点被匹配了,但是匹配到的 中的那个点可以匹配 中另外的点
那么点 和 点匹配成功
遍历完还没匹配到则匹配失败
例子
给定一个二分图,其中左半部包含 n1 个点(编号 1∼n1),右半部包含 n2 个点(编号 1∼n2),二分图共包含 m 条边。
数据保证任意一条边的两个端点都不可能在同一部分中。
请你求出二分图的最大匹配数。
二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
输入格式
第一行包含三个整数 n1、 n2 和 m。
接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。
输出格式
输出一个整数,表示二分图的最大匹配数。
数据范围
1≤n1,n2≤500,
1≤u≤n1,
1≤v≤n2,
1≤m≤10^5输入样例:
2 2 4 1 1 1 2 2 1 2 2
输出样例:
2
// 通过n1集合匹配n2集合,只需记录n1到n2的边
import java.io.*;
import java.util.*;class Main{static int N = 510, M = 100010;static int n1,n2,m,idx,res;static int[] h = new int[N];static int[] e = new int[M];static int[] ne = new int[M];static boolean[] st = new boolean[N]; // n2中的点是否匹配了static int[] match = new int[N]; // 记录n2集合中的点匹配的是n1中的哪个点public static void main(String[] args) throws IOException{BufferedReader in = new BufferedReader(new InputStreamReader(System.in));String[] s = in.readLine().split(" ");n1 = Integer.parseInt(s[0]);n2 = Integer.parseInt(s[1]);m = Integer.parseInt(s[2]);Arrays.fill(h,-1);while(m-->0){s = in.readLine().split(" ");int a = Integer.parseInt(s[0]);int b = Integer.parseInt(s[1]);add(a,b); // 只需要添加单向边}for(int i=1;i<=n1;i++){Arrays.fill(st,false); // 被考虑过的n2集合中的点也可以重新被下一个n1中的点考虑if(find(i)) res++; // 找n2中能匹配的}System.out.println(res);}// 进行匹配public static boolean find(int u){for(int i=h[u];i!=-1;i=ne[i]){int j = e[i];if(!st[j]){st[j] = true; // 被当前点考虑到了if(match[j]==0||find(match[j])){ // 如果该点没有匹配或者该点的上一个匹配的点可以匹配到另外一个match[j] = u; // 将j匹配给ireturn true; // 匹配成功 } }}return false;}// 添加边public static void add(int a,int b){e[idx] = b;ne[idx] = h[a];h[a] = idx++;}
}