二分图的最大匹配(匈牙利算法)

来源:互联网 发布:loveless world知乎 编辑:程序博客网 时间:2024/05/20 15:58

1、二分图、最大匹配

什么是二分图:二分图又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。 
4f7c595e-020c-3f6e-b20f-55630df1781a.jpg
什么是匹配:把上图想象成3男4女搞对象(无同性恋),连线代表彼此有好感,但最终只能1夫1妻,最终的配对结果连线就是一个匹配。匹配可以是空。 
什么是最大匹配:在有好感的基础上,能够最多发展几对。 

现在要用匈牙利算法找出最多能发展几对。 
[color=green][size=medium] 
匈牙利算法是解决寻找二分图最大匹配的。


二、最大匹配与最小点覆盖

最小点覆盖:假如选了一个点就相当于覆盖了以它为端点的所有边,你需要选择最少的点来覆盖所有的边

最小割定理是一个二分图中很重要的定理:一个二分图中的最大匹配数等于这个图中的最小点覆盖数。

算法:二分图的匹配 ———— 最小路径覆盖


      最小路径覆盖=最小路径覆盖=|G|-最大匹配数
      二分图的最大匹配总结

最小点集覆盖==最大匹配。在这里解释一下原因,首先,最小点集覆盖一定>=最大匹配,因为假设最大匹配为n,那么我们就得到了n条互不相邻的边,光覆盖这些边就要用到n个点。现在我们来思考为什么最小点击覆盖一定<=最大匹配。任何一种n个点的最小点击覆盖,一定可以转化成一个n的最大匹配。因为最小点集覆盖中的每个点都能找到至少一条只有一个端点在点集中的边(如果找不到则说明该点所有的边的另外一个端点都被覆盖,所以该点则没必要被覆盖,和它在最小点集覆盖中相矛盾),只要每个端点都选择一个这样的边,就必然能转化为一个匹配数与点集覆盖的点数相等的匹配方案。所以最大匹配至少为最小点集覆盖数,即最小点击覆盖一定<=最大匹配。综上,二者相等。


三、匈牙利算法

先给一个例子 
1、起始没有匹配 
d9b44964-f903-3b3d-85fb-22994a3009f9.jpg
2、选中第一个x点找第一跟连线 
3aa69867-6d31-3166-bc70-675ee19dfc1c.jpg
3、选中第二个点找第二跟连线 
ce8d2b7d-eff7-3232-8a49-5d4de5e75b0d.jpg
4、发现x3的第一条边x3y1已经被人占了,找出x3出发的的交错路径x3-y1-x1-y4,把交错路中已在匹配上的边x1y1从匹配中去掉,剩余的边x3y1 x1y4加到匹配中去 
3e61fee1-03fa-3daf-93ee-03cb9b4aca1d.jpg
5、同理加入x4,x5。 

匈牙利算法可以深度有限或者广度优先,刚才的示例是深度优先,即x3找y1,y1已经有匹配,则找交错路。若是广度优先,应为:x3找y1,y1有匹配,x3找y2。

深度优先匈牙利算法1

int nx,ny;//x集合和y集合中顶点的个数   int edge[maxn][maxn];//edge[i][j]为1表示ij可以匹配   int cx[maxn],cy[maxn];//用来记录x集合中匹配的y元素是哪个!   int visited[maxn];//用来记录该顶点是否被访问过!   int path(int u)   {       int v;       for(v=0;v<ny;v++)       {           if(edge[u][v]&&!visited[v])           {               visited[v]=1;              if(cy[v]==-1||path(cy[v]))//如果y集合中的v元素没有匹配或者是v已经匹配,但是从cy[v]中能够找到一条增广路               {                   cx[u]=v;                   cy[v]=u;                   return 1;               }           }       }       return 0;   }   int maxmatch()   {       int res=0;       memset(cx,0xff,sizeof(cx));//初始值为-1表示两个集合中都没有匹配的元素!       memset(cy,0xff,sizeof(cy));       for(int i=0;i<=nx;i++)       {           if(cx[i]==-1)           {               memset(visited,0,sizeof(visitited));               res+=path(i);           }       }       return res;   }</span>  
深度优先方法2:

/****************************************************二分图匹配(匈牙利算法的DFS实现)INIT:g[][]两边定点划分的情况CALL:res=hungary();输出最大匹配数优点:适于稠密图,DFS找增广路快,实现简洁易于理解时间复杂度:O(VE);****************************************************/const int MAXN=1000;int uN,vN;  //u,v数目int g[MAXN][MAXN];//编号是0~n-1的 int linker[MAXN];bool used[MAXN];bool dfs(int u){    int v;    for(v=0;v<vN;v++)        if(g[u][v]&&!used[v])        {            used[v]=true;            if(linker[v]==-1||dfs(linker[v]))            {                linker[v]=u;                return true;            }            }      return false;  }    int hungary(){    int res=0;    int u;    memset(linker,-1,sizeof(linker));    for(u=0;u<uN;u++)    {        memset(used,0,sizeof(used));        if(dfs(u))  res++;    }     return res;   }     
DFS就是利用增广路径的特性,为每个节点找到配对的点。核心是if (linker[v] == -1 || dfs(linker[v])),如果u可以和v配对,则直接返回;如果v已经有配对了,那么试试看能不能让v的对象另外再找个点配对,将v腾出来。然后一直递归下去。

广度优先遍历:

queue<int> Q;//Q队列,用来进行广度优先遍历int prev[__maxNodes];int Hungarian(){    int ans = 0;    memset(matching, -1, sizeof(matching));    memset(check, -1, sizeof(check));    for (int i=0; i<num_left; ++i)    {        if(matching[i] == -1)         {            while (!Q.empty())                Q.pop();            Q.push(i);            prev[i] = -1;  // 设 i 为路径起点            bool flag = false; // 尚未找到增广路            while (!Q.empty() && !flag)             {                int u = Q.front();                for (iterator_t ix = G[u].begin(); ix != G[u].end() && !flag; ++ix)                {                    int v = edges[*ix].to;                    if (check[v] != i)                     {                         check[v] = i;                        Q.push(matching[v]);                        //这里将matching[v]入队,如果matching[v]是-1的话,会直接进入下面的else,即 flag会变成true,到时候循环直接退出,-1也会被弹出。                        //当matching[v]有值时,表示这个节点可以出现在树的下一层,所有加入到层序遍历队列中                        if (matching[v] >= 0)                        //u可以和v配对,但是v已经有对象了,将u作为备胎记录下来                        { // 此点为匹配点                            prev[matching[v]] = u;                            //prev数组中存对应下标的点的备胎                        }                        else                         { // 找到未匹配点,交替路变为增广路                            flag = true;                            int d=u,e=v;                            while(d != -1)                            {                                //u原来是和t配对的,但是现在u甩了t和v配对                                //所有t去找备胎配对                                //依次循环下去                                int t = matching[d];                                matching[d] = e;                                matching[e] = d;                                d = prev[d];                                e = t;                            }                        }                    }                }                Q.pop();            }            if (matching[i] != -1)                 ++ans;        }    }    return ans;}

看每新加入一个节点能不能形成新的增广路径。方法就是,以新加入的节点为根节点,生成匈牙利树,看是否能找到未匹配的叶子结点。如果有就说明加入当前节点后可以形成一条增广路径,及配对数加一。