您的位置:首页 > 其它

详解二分图的最大匹配

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;
}


今天先写到这里,明天继续。虽然这个算法很简单。但是登高自卑嘛。明天我会写点关于这个模型的使用。因为每个算法都是针对某一特定模型的解。那么,我们要做的就是如何从实际问题中提取模型。然后采用相关算法或者算法的变种来解决它而已。好,今天算是打卡第一天。

希望对大家有帮助。希望自己能写出来高质量的博文。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  算法