详解二分图的最大匹配

来源:互联网 发布:网络代销商 编辑:程序博客网 时间:2024/05/22 13:12

一天一算法

二分图的匹配算法——最大匹配

基本概念:

二分图本身的概念就不解释了。这里主要介绍一下增广路。也有人称它为交错路。之所以称它为交错路是因为在这条路径上,未匹配边和匹配边是交替出现的。之所以称它为增广路,是因为如果能找到这条路,就能够使得匹配数加一。事实上,他的名字就是增广路的性质的体现。我们把二分图中的点分为左边和右边。增广路的理解,就是,一个未匹配点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 100using 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 numLeft,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;}

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

1 0