【基本概念】:
二分图:
二分图又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。
无向图G为二分图的充分必要条件是,G至少有两个顶点,且其所有回路的长度均为偶数。
最大匹配:
给定一个二分图G,在G的一个子图M中,M的边集中的任意两条边都不依附于同一个顶点,则称M是一个匹配. 选择这样的边数最大的子集称为图的最大匹配问题,如果一个匹配中,图中的每个顶点都和图中某条边相关联,则称此匹配为完全匹配,也称作完备匹配.
最小覆盖:
最小覆盖要求用最少的点(X集合或Y集合的都行)让每条边都至少和其中一个点关联。可以证明:最少的点(即覆盖数)=最大匹配数
最小路径覆盖:
用尽量少的不相交简单路径覆盖有向无环图G的所有结点。解决此类问题可以建立一个二分图模型。把所有顶点i拆成两个:X结点集中的i和Y结点集中的i',如果有边i->j,则在二分图中引入边i->j',设二分图最大匹配为m,则结果就是n-m。
增广路(增广轨):
若P是图G中一条连通两个未匹配顶点的路径,并且属于M的边和不属于M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径(举例来说,有A、B集合,增广路由A中一个点通向B中一个点,再由B中这个点通向A中一个点……交替进行)。
增广路径的性质:
1 有奇数条边。
2 起点在二分图的左半边,终点在右半边。
3 路径上的点一定是一个在左半边,一个在右半边,交替出现。(其实二分图的性质就决定了这一点,因为二分图同一边的点之间没有边相连,不要忘记哦。)
4 整条路径上没有重复的点。
5 起点和终点都是目前还没有配对的点,而其它所有点都是已经配好对的。
6 路径上的所有第奇数条边都不在原匹配中,所有第偶数条边都出现在原匹配中。
7 最后,也是最重要的一条,把增广路径上的所有第奇数条边加入到原匹配中去,并把增广路径中的所有第偶数条边从原匹配中删除(这个操作称为增广路径的取反),则新的匹配数就比原匹配数增加了1个。
了解了增广路的定义以及性质之后,我们仔细理解第7条性质。因为增广路径的长度为奇数,我们不妨设为2*K+1,又因为第一条是非匹配边,且匹配边与非匹配边交替出现,所以非匹配边有K+1条,匹配边有K条。此时,我们做取反操作,则匹配边的个数会在原来的基础上+1。求最大匹配的“匈牙利算法”即是此思想:无论从哪个匹配开始,每一次操作都让匹配数+1,不断扩充,直到找不到增广路径,此时便得到最大匹配。
匈牙利算法:
算法的核心就是根据一个初始匹配不停的找增广路,直到没有增广路为止。
匈牙利算法的本质实际上和基于增广路特性的最大流算法还是相似的,只需要注意两点:
(一)每个X节点都最多做一次增广路的起点;
(二)如果一个Y节点已经匹配了,那么增广路到这儿的时候唯一的路径是走到Y节点的匹配点(可以回忆最大流算法中的后向边,这个时候后向边是可以增流的)。
找增广路的时候既可以采用dfs也可以采用bfs,两者都可以保证O(nm)的复杂度,因为每找一条增广路的复杂度是O(m),而最多增广n次,dfs在实际实现中更加简短。
匈牙利算法的基本模式:
1、 初始时最大匹配为空
2、 while (找得到增广路径)
3、 do 把增广路径加入到最大匹配中。
如果二分图的左半边一共有n个点,最多找n条增广路径,如果图中有m条边,每一条增广路径把所有边遍历一遍,所以时间复杂度为O(n*m);
算法思想:
算法的思路是不停的找增广轨, 并增加匹配的个数,增广轨顾名思义是指一条可以使匹配数变多的路径,在匹配问题中,增广轨的表现形式是一条"交错轨",也就 是说这条由图的边组成的路径, 它的第一条边是目前还没有参与匹配的,第二条边参与了匹配,第三条边没有..最后一条边没有参与匹配,并且始点和终点还没 有被选择过.这样交错进行,显然 他有奇数条边.那么对于这样一条路径,我们可以将第一条边改为已匹配,第二条边改为未匹配...以此类推.也就是将所有 的边进行"反色",容易发现这样修 改以后,匹配仍然是合法的,但是匹配数增加了一对.另外,单独的一条连接两个未匹配点的边显然也是交错轨.可以证明, 当不能再找到增广轨时,就得到了一个 最大匹配.这也就是匈牙利算法的思路.、
二分图匹配中较为重要的三个公式:
二分图最小顶点覆盖 = 二分图最大匹配;
DAG图的最小路径覆盖 = 节点数(n)- 最大匹配数;
二分图最大独立集 = 节点数(n)- 最大匹配数;
1 #include <iostream> 2 3 #include <cstdio> 4 5 #include <cstring> 6 7 using namespace std; 8 9 const int N=1001; 10 11 int n1,n2,k; 12 13 //n1,n2为二分图的顶点集,其中x∈n1,y∈n2 14 15 int map[N][N],vis[N],link[N]; 16 17 //link记录n2中的点y在n1中所匹配的x点的编号 18 19 int find(int x) 20 21 { 22 23 int i; 24 25 for(i=1;i<=n2;i++) 26 27 { 28 29 if(map[x][i]&&!vis[i])//x->i有边,且节点i未被搜索 30 31 { 32 33 vis[i]=1;//标记节点已被搜索 34 35 //如果i不属于前一个匹配M或被i匹配到的节点可以寻找到增广路 36 37 if(link[i]==0||find(link[i])) 38 39 { 40 41 link[i]=x;//更新 42 43 return 1;//匹配成功 44 45 } 46 47 } 48 49 } 50 51 return 0; 52 53 } 54 55 int main() 56 57 { 58 59 int i,x,y,s=0; 60 61 scanf("%d%d%d",&n1,&n2,&k); 62 63 for(i=0;i<k;i++) 64 65 { 66 67 scanf("%d%d",&x,&y); 68 69 map[x][y]=1; 70 71 } 72 73 for(i=1;i<=n1;i++) 74 75 { 76 77 memset(vis,0,sizeof(vis)); 78 79 if(find(i)) 80 81 s++; 82 83 } 84 85 printf("%d\n",s); 86 87 return 0; 88 89 }
POJ 3041 Asteroids 链接:http://poj.org/problem?id=3041
题意:在某个n*n的空间内,分布有一些小行星,某人在里面打炮,放一枪后某一行或某一列的行星就都没了,让求最少的打炮数。
1 #include <iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 int link[550],map[550][550],vis[550]; 6 int find(int x,int n) 7 { 8 int i,j; 9 for(i= 1 ; i <= n ; i++) 10 { 11 if(map[x][i]&&!vis[i]) 12 { 13 vis[i] = 1; 14 if(link[i]==0||find(link[i],n)) 15 { 16 link[i] = x; 17 return 1; 18 } 19 } 20 } 21 return 0; 22 } 23 int main() 24 { 25 int i,j,n,m,k,a,b; 26 cin>>n>>m; 27 for(i = 1; i <= m ; i++) 28 { 29 cin>>a>>b; 30 map[a][b] = 1; 31 } 32 int s = 0; 33 for(i = 1 ; i <= n ; i++) 34 { 35 memset(vis,0,sizeof(vis)); 36 if(find(i,n)) 37 s++; 38 } 39 cout<<s<<endl; 40 return 0; 41 }