小撸--二分图
讲二分图 之前 我先提下关于它的各种基础概念 在了解一个新的算法 应该有必要将关于它的概念 有所了解
1.什么是二分图?
:二分图 又叫做二部图 是图论中的一种特殊模型 设G=(V,E)是一个无向图 如果顶点V可以分割为两个互不相交的子集(A,B) 并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这个不同的顶点集(i in A j in B) 则称G为一个二分图(这是官方的定义 感觉也是最详细的)
2.什么是匹配?
匹配是 基于二分图上提出的概念。 给定一个二分图G M为G边集的一个子集 如果M满足当中的任意两条边都不依附于同一个顶点 则称M是一个匹配
3.什么是二分图 极大匹配?
是指在当前已完成的匹配下,无法再通过增加未完成匹配的边的方式来增加匹配的边数
4.什么是二分图 最大匹配?
所有极大匹配当中边数最大的一个匹配 同通俗来说就是----在匹配的基础上选择边数最大的子集称为最大匹配
5.增广路(增广轨 交错轨)是什么?
若P是图G中一条连通两个未匹配顶点的路径 并且属于M的边喝不属于M的边(即已匹配和未匹配的边)在P上交替出现 则称P为相对于M的一条增广路径
(sample: 有A B集合 增广路由A中一个点通向B中一个点 再由B中这个点通向A中一个点 就是这样交替进行下去.)
由增广路的定义引申出的三个结论:
1.P的路径长度必定为奇数 第一条边喝最后一条边都不属于M
2.不断寻找增广路可以得到一个更大的匹配M‘ 直到找不到更多的增广路
3.M为G的最大匹配当且仅当不存在M的增广路径
6.最小点覆盖数 是什么?
即要求用最少的点(A B任意一集合)让每条边至少和其中一个点相关联
可以证明: 最小点覆盖数 == 最大匹配数(证明过程这里不给出 希望读者自行查阅)
7.独立集是什么?
图G中两两互不相邻的顶点构成的集合
可以证明: 最大独立集 == 顶点数 - 二分图最大匹配(同上 证明过程不予给出 请自行查阅)
8.最小路径覆盖是什么?
简而言之-就是找出最小的路径条数 使之成为P的一个路径覆盖
一个PXP的有向图中 路径覆盖就是在图中找一些路径使之覆盖了图中所有的顶点 且任何一个顶点有且只有一条路径与之关联(如果把这些路径中的每条路径从它们起始点走到它的终点 那么恰好经过图中的每个顶点一次仅且一次) 如果不考虑图中存在回路 那么每条路径就是一个弱连通子集
得出的结论:
1.一个单独的顶点是一条路径
2.如果存在一路径p1,p2,......pk 其中p1 为起点 pk为终点 那么在覆盖图中顶点p1,p2,......pk不再与其它的顶点之间存在有向边
可以证明: 最小路径覆盖 == |P|-二分图最大匹配 (同上)
看完上面这么一大堆概念 是不是和我一样头晕了。。。 还是先去撸一把吧?
继续... 这里我们给出一道例题来更好的理解:
题目链接 : http://acm.nyist.net/JudgeOnline/problem.php?pid=239
这题 应该是最简单的 二分图最大匹配应用题 题意也很好转换 即是求 二分图最大匹配
同样 对于图的信息存储 这里同样可以用邻接表 或 邻接矩阵
先去上课了~~
耽搁了一天了 继续。。。。
这边 我介绍一种 二分图中常用的经典算法---匈牙利算法 还有一种算法是-Hopcroft-Carp 它的时间复杂度比前者更小 更适合大数据
它的实现方式还是很简单的 如果 有2个顶点集 分别为A B
我们只需要通过遍历A中的所有顶点 将它与B集合进行匹配 这个过程 我们可以通过DFS来实现。
因为 它的代码很容易理解 除了其中的反向查找增广路的代码片段 但是 你可以自己在纸上模拟演算下 会便于你理解很多
我这边给出关于它的 邻接表 与 邻接矩阵的实现方式 并附上注释
这边 值得一提的是: 我用邻接矩阵 TLE 可能是写的太渣了吧 如果你AC了 不妨留言告诉我。
1 #include <cstdio> 2 #include <cstring> 3 using namespace std; 4 5 #define num 520 6 int n; 7 int matrix[num][num];//建立邻接矩阵来存储信息 8 bool vis[num];//标记点是否被访问过 9 int linker[num];//标记该点的前一个节点是那个 10 11 bool dfs(int u)//从U集合开始向V集合寻找匹配 12 { 13 for(int v=1;v<=n;v++) 14 { 15 if( !vis[v] && matrix[u][v] )//V集合的点与U集合该点有边相邻 并且这个点不能被访问过 16 { 17 vis[v]=true; 18 if( !linker[v] || dfs( linker[v] ) )//反向找增广路 如果能成功 就可以使匹配边+1 19 { 20 linker[v]=u; 21 return true; 22 } 23 } 24 } 25 return false; 26 } 27 28 int getSum() 29 { 30 int count=0; 31 memset( linker,0,sizeof(linker) );// 这里 与下面不同 只需要初始化一次 32 for(int u=1;u<=n;u++) 33 { 34 memset( vis , false , sizeof(vis) );//这里 注意 每一次A集合中的顶点去与B集合去匹配的过程 都需要更新点的访问 35 if( dfs(u) ) 36 { 37 count++; 38 } 39 } 40 return count; 41 } 42 43 int main() 44 { 45 int t; 46 int m; 47 int x,y; 48 while(~scanf("%d",&t)) 49 { 50 while(t--) 51 { 52 scanf("%d %d",&n,&m); 53 for(int i=0;i<=n;i++) 54 { 55 for(int j=0;j<=n;j++) 56 { 57 matrix[i][j]=0; 58 } 59 } 60 while(m--) 61 { 62 scanf("%d %d",&x,&y); 63 matrix[x][y]=matrix[y][x]=1;//无向图 2点相邻 64 } 65 printf("%d\n",getSum() ); 66 } 67 } 68 }
1 #include <cstdio> 2 #include <cstring> 3 #include <vector> 4 using namespace std; 5 6 //如果你不清楚 下面有关vector的函数 请去查询资料 这是一个非常重要的容器 并且它的作用真的很大 7 #define num 520 8 vector<int>mp[num];//邻接表存储 9 int linker[num];//记录该点的前驱. 10 bool vis[num];//标记是否被访问 11 int n; 12 13 void inital(int n)//这里不能遗忘 每次需要清空邻接表 14 { 15 for(int i=1;i<=n;i++) 16 { 17 mp[i].clear(); 18 } 19 } 20 bool dfs(int u) 21 { 22 for(int i=0;i<mp[u].size();i++)//第u行共有的元素数量 23 { 24 if( !vis[ mp[u][i] ] ) //该点未被匹配 25 { 26 vis[ mp[u][i] ]=true; //标记 27 if( !linker[ mp[u][i] ] || dfs( linker[ mp[u][i] ] ) ) 28 { 29 linker[ mp[u][i] ]=u; //找增广路 反向 30 return true; 31 } 32 } 33 } 34 return false; 35 } 36 37 int getSum() 38 { 39 int count=0; 40 memset( linker ,0, sizeof(linker) ); 41 for(int u=1; u<=n ;u++ ) 42 { 43 memset( vis ,false, sizeof(vis) ); 44 if( dfs(u) ) 45 { 46 count++; 47 } 48 } 49 return count; 50 } 51 52 int main() 53 { 54 int t,m; 55 int x,y; 56 while(~scanf("%d",&t)) 57 { 58 while(t--) 59 { 60 scanf("%d %d",&n,&m); 61 inital(n); 62 while(m--) 63 { 64 scanf("%d %d",&x,&y); 65 mp[x].push_back(y);//这里 只需要将Y顶点压入X顶点即可 66 } 67 printf("%d\n",getSum() ); 68 } 69 } 70 }
至于 上文提过的 另外一种算法 等我掌握了 再来写。。
long long way to go