[SinGuLaRiTy] 二分图&匈牙利算法
【SinGuLaRiTY-1019】 Copyright (c) SinGuLaRiTy 2017. All Rights Reserved.
二分图
二分图是图论中一种特殊的图形。顾名思义,二分图G可以被分为X、Y两个子图,在二分图中,没有任意一条连线的两个端点都属于X图或Y图,即每一条连线都贯穿连接着两个子图。二分图的一个重要等价定义是:不含有「含奇数条边的环」的图。
如图-1所描绘的图像就是一个二分图,而图-2就不是一个二分图。
图-1 图-2
匹配
在图论中,一个匹配是一个边的集合,其中任意两条边都没有公共顶点。
如图-3中的红色边就是图-1的一种匹配(显然,匹配可能不止一种)
图-3
相关的定义还有匹配点、匹配边、未匹配点、非匹配边,它们的含义也就显而易见了。
最大匹配
一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。图 -4 是一个最大匹配。
完美匹配
如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。图 -4 是一个完美匹配。显然,完美匹配一定是最大匹配(完美匹配的任何一个点都已经匹配,添加一条新的匹配边一定会与已有的匹配边冲突)。但并非每个图都存在完美匹配。
图-4
下面我们借用一个例子来看看这两种匹配的区别:(Tips: 这个例子摘自其它博客,但确实很形象)
如下图所示,如果在某一对男孩和女孩之间存在相连的边,就意味着他们彼此喜欢。是否可能让所有男孩和女孩两两配对,使得每对儿都互相喜欢呢?图论中,这就是完美匹配问题。如果换一个说法:最多有多少互相喜欢的男孩/女孩可以配对儿?这就是最大匹配问题。
匈牙利算法的相关概念
交替路
从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边...形成的路径叫交替路 (比较形象)。
增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路。例如,图- 5 中的一条增广路如图 -6 所示(图中的匹配点均用红色标出)
图-5 图-6
增广路有一个重要特点:非匹配边比匹配边多一条。因此,研究增广路的意义是改进匹配,只要把增广路中的匹配边和非匹配边的身份交换即可。由于中间的匹配节点不存在其他相连的匹配边,所以这样做不会破坏匹配的性质。交换后,图中的匹配边数目比原来多了 1 条。
我们可以通过不停地找增广路来增加匹配中的匹配边和匹配点。找不到增广路时,达到最大匹配(这是增广路定理)。匈牙利算法正是这么做的。
匈牙利树
(Tips: 这一段的图画起来实在是太复杂了,故复制了pi9nc的图片)
匈牙利树一般由 BFS 构造(类似于 BFS 树)。从一个未匹配点出发运行 BFS(唯一的限制是,必须走交替路),直到不能再扩展为止。例如,由图Fig.7,可以得到如Fig.8 的一棵 BFS 树:
这棵树存在一个叶子节点为非匹配点(7 号),但是匈牙利树要求所有叶子节点均为匹配点,因此这不是一棵匈牙利树。如果原图中根本不含 7 号节点,那么从 2 号节点出发就会得到一棵匈牙利树。这种情况如Fig.9 所示(顺便说一句,Fig.8 中根节点 2 到非匹配叶子节点 7 显然是一条增广路,沿这条增广路扩充后将得到一个完美匹配)。
匈牙利算法的思路
用增广路求最大匹配(称作匈牙利算法,匈牙利数学家Edmonds于1965年提出) 算法轮廓:
(1)置M为空
(2)找出一条增广路径P,通过取反操作获得更大的匹配M’代替M
(3)重复(2)操作直到找不出增广路径为止
<伪代码>
bool dfs(int k) //寻找从k出发的对应项的可增广路 { while (从邻接表中列举k能关联到顶点j) { if (j不在增广路上) { 把j加入增广路; if (j是未盖点 或者 从j的对应项出发有可增广路) { 修改j的对应项为k; 从k的对应项有可增广路,返回true; } } } 从k的对应项出没有可增广路,返回false; } int hungary() //匈牙利算法主函数 { for i = 1 to n do//左部顶点数 { 清标志数组//右部 if (从I有可增广路) 匹配数++; } return 匹配数; }
/*
[说明]
算法的核心是 dfs(int k)函数,它的作用是:
寻找顶点k可能匹配的顶点。对于每个可以与k匹配的顶点j,假如它未被匹配,可以直接使用j与k匹配;
假如j已与某顶点x匹配,那么只需调用dfs(x)来求证x是否可以与其它顶点匹配,如果返回true的话,仍可以使j与k匹配,这就是一次dfs;
每次dfs时,要标记访问到的顶点(visit[j]=true),以防死循环和重复计算;
每次dfs开始前所有的顶点都是未标记的。 主过程只需对每个左侧的顶点调用dfs,如果返回一次true,就对最大匹配数加一;一个简单的循环就求出了最大匹配的数目。
*/
实现代码
#include<cstdio> #include<cstring> #include<cmath> #include<cstdlib> #include<algorithm> #include<iostream> #define MAXN 110 //最大顶点数 using namespace std; bool Map[MAXN][MAXN]; //二分图结构-邻接矩阵存储,有向图 bool visit[MAXN]; //标志数组,标记Y集合中的 i 元素是否被检测到过 int match[MAXN]; //存储匹配的方案,Y集合中的 i 元素匹配 X 中的某个元素 int n,m; //n, m 分别为X集合与Y集合的顶点数 bool dfs(int k) { int i,t; for(i=1;i<=m;i++) if(Map[k][i]&&!visit[i]) { visit[i]=true; if( match[i]==0||dfs(match[i])) { match[i]=k; return true; } } return false; } int hungary() { int i,ans; ans=0; for(i=1;i<=m;i++) match[i]=0; for(i=1;i<=n;i++) { memset(visit,0,sizeof(visit)); if(dfs(i)) ans++; } return ans; } int main() { int i,j,k; scanf("%d%d",&n,&m); for(i=1;i<=n;i++) for(j=1;j<=m;j++) { scanf("%d",&k); if(k==1) Map[i][j]=true; else Map[i][j]=false; } printf("%d",hungary()); return 0; }
Time: 2017-07-05