详解二分图的最大匹配
2016-04-14 21:05
309 查看
一天一算法
二分图的匹配算法——最大匹配基本概念:
二分图本身的概念就不解释了。这里主要介绍一下增广路。也有人称它为交错路。之所以称它为交错路是因为在这条路径上,未匹配边和匹配边是交替出现的。之所以称它为增广路,是因为如果能找到这条路,就能够使得匹配数加一。事实上,他的名字就是增广路的性质的体现。我们把二分图中的点分为左边和右边。增广路的理解,就是,一个未匹配点a(左边)到另一个未匹配点b(右边)的一条路径。这条路径满足上述的交错的性质。KM算法
:在求二分图的匹配中,一个基本的算法就是KM算法。KM的算法核心思想,就是寻找增广路。因为,每找到一条增广路,那么匹配数就会多1.所以,那么就一直找增广路,直到找不到为止。找不到的时候,得到的就是最大匹配数了和相应的匹配了。KM算法运行步骤的文字描述:
我觉得初学者看到这里还是不明白KM算法到底是怎么操作的。那么,我用文字来大致的描述一下。假设我们现在要给n1(左边)找一个匹配点。我们首先遍历a的相邻点。加入我们找到了一个点。称它为n2(右边).如果n2(右边)没有匹配,则我们就已经找到了一条增广路(看上文的定义)。如果n2(右边)已经匹配,并且我们设他匹配的点是n3(左边),那么我们就尝试着找一条增广路。找增广路的本质,就是我们要找一种调整方案。把我们原先的匹配的方案在调整一下,尽可能使得当前的点也可有匹配的点。
那么怎么调整呢。接下来我用拟人来描写。:)我们假设n1(左)和n2(右)匹配。但是n2(右)和n3(左)在之前就匹配了,所以,为了不让n3(左)生气,我们还得再给n3(左)找一个匹配点。如果n3没有找到匹配点,那么我们就还按照原来的匹配方案匹配。因为这样谁也不得罪。但是,如果n3(左)找到了一个匹配点,我们叫他n4(右),那么我们就调整,让n3(左)去和n4(右)匹配,n1(左)和n2(右)匹配。这样一来,我们不就多了一个匹配吗?那么问题来了,怎么给n3(左)找匹配点呢?很显然,在计算机看来,n3(左)和n1(左)没有任何区别不是吗?你怎么给n1找,你就怎么给n3找不就得了。仍然是,遍历相邻的点,我们找到一个点n4,如果n4没有匹配,那么就让n3和n4匹配。如果,n4已经匹配…………代码经验多的同志们可能就看出来了,这实际上就是递归的过程。当然,KM算法,也有非递归的形式。但这样理解无妨。
复杂度分析
KM算法的理论复杂度是O(VE).当然,这取决与你的实现方式。KM有BFS实现方式和DFS实现方式。总体而言,BFS是较好的。尤其是在稀疏图中。BFS有明显的优势。同时较好的存图方式应该是采用邻接表。要实现邻接表。可以借助容器vector ,当然,这样会浪费一些空间,因为vector不能动态的申请大小,也可以自己实现。在接下来的代码展示中,我会采用容器实现,以求方便。并给出不用容器实现邻接表的c++代码。
Talk’s cheap ,show me the code
//首先给出邻接表的完成 #include<iostream> struct Link{ int node; struct Link *next; Link(){ node=-1; next=NULL; } }; struct _Node{ int nodeNum; struct Link *Node; _Node(){ nodeNum=0; Node=new struct Link; } }; void buildAdj(const int from,const int to ,const int weight,struct _Node*Adj){ Adj[from].nodeNum++; struct Link *tempLink=new struct Link; tempLink->node=to; tempLink->next=Adj[from].Node; Adj[from].Node=tempLink; } void Destroy_Adj(int nodeNum,struct _Node *Adj){ using std::endl; using std::cout; for(int i=0;i<nodeNum;i++){ Link *tempLink=Adj[i].Node; while(tempLink->node!=-1){ struct Link* LocalLink=tempLink->next; delete tempLink; tempLink=LocalLink; } } delete []Adj; cout<<"Destroy_Adj runs well "<<endl; } int main(){ using std::cin; using std::cout; using std::endl; int nodeNum; int edgeNum; struct _Node *Adj; cin>>nodeNum; cin>>edgeNum; Adj=new struct _Node [nodeNum]; for(int i=0;i<edgeNum;i++){ int source,end; int weight; cin>>source>>end>>weight; buildAdj(source,end,weight,Adj); } cout<<"buildAdj runs well"<<endl; Destroy_Adj(nodeNum,Adj); }
/*接下来给出DFS版本 当然,宏定义显得有点蠢,但是,由于是借助vector实现的。所以,只能这么做了。如果采用上面的自己实现邻接表,那么就不用定义maxNode了。 我之所以把他封装,就尽量避免全局变量,尽量,模块化。我希望自己在blog上放的代码都是直接可以拿去用的,代码风格较好的模块。:)。希望自己最后能做到这样 */ #include <iostream> #include <cstring> #include <vector> #define maxNode 100 using std::vector; struct _Edge{ int from; int to; int weight; }; class maxMatch{ public: vector<int>G[maxNode]; vector<_Edge> Edge; /* 事实上,这里是最简单的情况,就是说,左边的点依次为0,1,2....,右边的点也依次为0 1,2,3.... 但实际中很可能不是这么直接的模型,所以就设计到对原图或者原问题点的重新编号。基于科普,所以,这里只给最直接的模型的求解。 实际运用,根据问题进行转换。 */ int numLeft;//左边的点的个数 int numRight; int numEdge;//左右之间的边的个数 int numNode;//其实可以不要 bool *Checked;//判断某个点是否被访问过 int *matchLeft,*matchRight;//left记录左边点的匹配结果,right记录右边点的匹配结果 public: maxMatch(int nu a556 mLeft,int numRight,int numEdge,int numNode):numLeft(numLeft),numRight(numRight),numEdge(numEdge),numNode(numNode){ Checked=new bool[numRight]; matchLeft=new int [numLeft]; matchRight=new int [numRight]; } ~maxMatch(){ delete []Checked; delete []matchLeft; delete []matchRight; } bool DFS(int u); int Hungarian(); void setEdge();//初始化边的函数 }; void maxMatch::setEdge(){ using std::cin; for(int i=0;i<numEdge;i++){ int from,to,weight; cin>>from>>to>>weight; struct _Edge tempEdge; tempEdge.from=from; tempEdge.to=to; tempEdge.weight=weight; Edge.push_back(tempEdge); G[from].push_back(i); } } bool maxMatch::DFS(int u){ for(int i=0;i<G[u].size();i++){ int v=Edge[G[u][i]].to; if(!Checked[v]){ Checked[v]=true; /* 这里的递归,请参考上面的文字描述进行理解 如果v没有匹配。则找到增广路 如果v匹配了,他的匹配点就是matchRight[v] 如果我能给v在找一个匹配点的话,我就v和u匹配。如果找不 到,那么为了不得罪matchRight[v],就还是按照原先的匹 配方案。 */ if(matchRight[v]<0||DFS(matchRight[v])){ matchRight[v]=u; matchLeft[u]=v; return true; } } } return false ; } int maxMatch::Hungarian(){ int ans=0; memset(matchLeft,-1,sizeof(int)*numLeft); memset(matchRight,-1,sizeof(int)*numRight); for(int i=0;i<numLeft;i++){ if(matchLeft[i]==-1){ memset(Checked,0,sizeof(bool)*numRight); if(DFS(i)) ans++; } } return ans; } int main(){ using std::cin; using std::cout; //用来存储每个点出发的边的编号 int numLeft,numRight,numEdge,numNode; int ans; cin>>numLeft>>numRight>>numEdge>>numNode; maxMatch bestMatch(numLeft,numRight,numEdge,numNode); bestMatch.setEdge(); ans=bestMatch.Hungarian(); cout<<ans; }
今天先写到这里,明天继续。虽然这个算法很简单。但是登高自卑嘛。明天我会写点关于这个模型的使用。因为每个算法都是针对某一特定模型的解。那么,我们要做的就是如何从实际问题中提取模型。然后采用相关算法或者算法的变种来解决它而已。好,今天算是打卡第一天。
希望对大家有帮助。希望自己能写出来高质量的博文。
相关文章推荐
- 书评:《算法之美( Algorithms to Live By )》
- 动易2006序列号破解算法公布
- Ruby实现的矩阵连乘算法
- C#插入法排序算法实例分析
- 超大数据量存储常用数据库分表分库算法总结
- C#数据结构与算法揭秘二
- C#冒泡法排序算法实例分析
- 算法练习之从String.indexOf的模拟实现开始
- C#算法之关于大牛生小牛的问题
- C#实现的算24点游戏算法实例分析
- c语言实现的带通配符匹配算法
- 浅析STL中的常用算法
- 算法之排列算法与组合算法详解
- C++实现一维向量旋转算法
- Ruby实现的合并排序算法
- C#折半插入排序算法实现方法
- 基于C++实现的各种内部排序算法汇总
- C++线性时间的排序算法分析
- C++实现汉诺塔算法经典实例
- PHP实现克鲁斯卡尔算法实例解析