二分图经常用来研究两种不同类型对象之间的关系
关于二分图主要参考二分图|维基百科 Wiki
定义二分图的顶点可以分成两个互斥的独立点集 U U U 和 V V V ,使得所有的边都是由 U U U 中一个顶点和 V V V 中一个顶点构成的
图片来自Wiki
特点二分图的每个集合内部,没有边相连
二分图中的每个环都有偶数个顶点
一个图为二分图 ⟺ \iff ⟺ 当且仅当图中不含有奇数环
奇数环:边数为奇数的环路
证明
「必要性」:⇒ \Rightarrow ⇒
假设图中存在一个奇数环Circle-1
,对其顶点进行编号,
graph LR
A((1))---B((2))
B---C((1))---D((2))---E((...))
起始点的编号一定是矛盾的。因为奇数次编号后,若开始是1,最后就成了2,开始是2,最后就是1,矛盾
「充分性」:⇐ \Leftarrow ⇐
由于图中不存在奇数环,所以染色过程一定没有矛盾
一个图为二分图 ⟺ \iff ⟺ 当且仅当图的着色数为2
如果图中没有环,例如:树,一定是二分图
染色法判定二分图染色过程无矛盾,则可以判定为二分图
算法流程1 2 3 4 5 遍历所有的顶点 如果当前的顶点没有被染色 DFS(i, color) // 深度优先搜索进行染色
代码实现核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 boolean 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] == 0 ) { if (!dfs(j, 3 -c)) return false ; } else if (color[j] == c) return false ; } return true ; } boolean flag = true ;for (int i = 1 ; i <= n; ++i) { if (color[i] == 0 ) { if (!dfs(i, 1 )) { flag = false ; break ; } } } if (flag) log.write("Yes" );else log.write("No" );
java版完整代码
acwing 860. 染色法判定二分图
给定一个 n n n 个点 m m m 条边的无向图,图中可能存在重边和自环。
请你判断这个图是否是二分图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import java.util.*;import java.io.*;class Main { static final int INF = 0x3f3f3f3f ; static final int N = 100010 ; static final int M = 200010 ; static int [] h = new int [N]; static int [] e = new int [M]; static int [] ne = new int [M]; static int [] color = new int [N]; static int idx; static int n, m; static BufferedWriter log = new BufferedWriter (new OutputStreamWriter (System.out)); static BufferedReader cin = new BufferedReader (new InputStreamReader (System.in)); static void add (int a, int b) { e[idx] = b; ne[idx] = h[a]; h[a] = idx++; } static boolean 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] == 0 ) { if (!dfs(j, 3 -c)) return false ; } else if (color[j] == c) return false ; } return true ; } public static void main (String[] args) throws Exception { String[] str = cin.readLine().split(" " ); n = Integer.parseInt(str[0 ]); m = Integer.parseInt(str[1 ]); Arrays.fill(h, -1 ); while (m-- > 0 ) { str = cin.readLine().split(" " ); int a = Integer.parseInt(str[0 ]); int b = Integer.parseInt(str[1 ]); add(a, b); add(b, a); } boolean flag = true ; for (int i = 1 ; i <= n; ++i) { if (color[i] == 0 ) { if (!dfs(i, 1 )) { flag = false ; break ; } } } if (flag) log.write("Yes" ); else log.write("No" ); log.flush(); log.close(); cin.close(); } }
二分图的最大匹配 相关基础概念@Renfei Song
匹配假定 G G G 是一个二分图,由 G G G 中一组没有公共顶点的边 组成的集合 E ′ E' E ′ ,称作一个匹配
注意:
「匹配」首先是一个边的集合 ∀ e 1 , e 2 ∈ E ′ , e 1 ∩ e 2 = ∅ \forall \ e_1, e_2 \in E', e_1 \cap e_2 =\empty ∀ e 1 , e 2 ∈ E ′ , e 1 ∩ e 2 = ∅ 最大匹配图 G G G 所有匹配中,所含边数最多的匹配,称为最大匹配。
完美匹配如果一个匹配中,包含了 G G G 所有的顶点,则称之为完美匹配。
图片来自Renfei Song
Fig.4 表示的匹配 M p e r f e c t = { E < 1 , 7 > , E < 2 , 5 > , E < 3 , 6 > , E < 4 , 8 > } M_{perfect}=\{E<1,7>,E<2,5>,E<3,6>,E<4,8>\} M p e r f e c t = { E < 1 , 7 > , E < 2 , 5 > , E < 3 , 6 > , E < 4 , 8 > } 就是个完美匹配
图 G G G 中所有顶点都是 M p e r f e c t M_{perfect} M p e r f e c t 中的匹配点
显然,完美匹配一定是最大匹配,最大匹配不一定是完美匹配
匈牙利算法 原理分析匈牙利算法,就是用来求解最大匹配问题的
交替路 :从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路
图片来自Renfei Song
增广路 :从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路(agumenting path)
例如,Fig.5中的一条增广路如Fig.6所示(图中的匹配点均用红色标出)
图片来自Renfei Song
增广路有一个重要特点:「非匹配边比匹配边多一条 」
增广路中的匹配节点不存在其他相连的匹配边,因此交换匹配边和非匹配边的身份 ,不会破坏匹配的性质
交换后,图中的匹配边就比原来多了一条,达成改进匹配 的目的
通过不停寻找增广路来增加匹配边,找不到增广路时,达到最大匹配(增广路定理)
这也是匈牙利算法的做法
可以将集合 U U U 和集合 V V V 中顶点匹配的过程视作,男女找对象的过程。
一个匹配集合,保证结成一对(匹配)的男生和女生,彼此只有对方一个对象(匹配集合中任意两条边都没有公共顶点 ),禁止脚踏两只船
代码实现核心代码,解析见注释
DFS
更适合稠密图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int res = 0 ; for (int i = 1 ; i <= n1; ++i) { Arrays.fill(st, false ); if (find(i)) res++; } boolean 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 ; }
最坏时间复杂度 O ( n m ) \Omicron(nm) O ( n m )
java版本完整代码
acwing 861. 二分图的最大匹配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import java.util.*;import java.io.*;class Main { static final int INF = 0x3f3f3f3f ; static final int N = 1010 ; static final int M = 100010 ; static int [] h = new int [N]; static int [] e = new int [M]; static int [] ne = new int [M]; static int [] match = new int [N]; static int idx; static boolean [] st = new boolean [N]; static int n1, n2, m; static BufferedWriter log = new BufferedWriter (new OutputStreamWriter (System.out)); static BufferedReader cin = new BufferedReader (new InputStreamReader (System.in)); static void add (int a, int b) { e[idx] = b; ne[idx] = h[a]; h[a] = idx++; } public static void main (String[] args) throws Exception { String[] str = cin.readLine().split(" " ); n1 = Integer.parseInt(str[0 ]); n2 = Integer.parseInt(str[1 ]); m = Integer.parseInt(str[2 ]); int a, b; Arrays.fill(h, -1 ); while (m-- > 0 ) { str = cin.readLine().split(" " ); a = Integer.parseInt(str[0 ]); b = Integer.parseInt(str[1 ]); add(a, b); } int res = 0 ; for (int i = 1 ; i <= n1; ++i) { Arrays.fill(st, false ); if (find(i)) res++; } log.write(String.valueOf(res)); log.flush(); log.close(); cin.close(); } static boolean 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 ; } }
补充匈牙利算法还被用来解决「最小点覆盖」问题
存在一个点集 V m i n V_{min} V m i n ,二分图中所有的边都至少有一个端点 ∈ V m i n \isin V_{min} ∈ V m i n ,即 V m i n V_{min} V m i n 覆盖到了二分图的所有边
关于如何求 V m i n V_{min} V m i n 中的点个数?
有一个 König 定理,1913年由匈牙利数学家柯尼希(D.Konig)提出,大致意思是:
『一个二分图的最大匹配数等于最小点覆盖个数』
证明略。暂时不会,待以后补充
参考资料