[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   8    9

这棵树存在一个叶子节点为非匹配点(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

posted @ 2017-07-05 20:42  SinGuLaRiTy2001  阅读(296)  评论(0编辑  收藏  举报