二分图匹配

二分图匹配

 

二分图

二分图(Bipartite graph)又称作二部图,是图论中的一种特殊模型。 在一个二分图中,所有的顶点都可以分为两个不相交的集合UV,两个集合大小不一定相等,但每条边都连接两个集合中各一个顶点。换句话说,一个二分图的顶点可以染成两种颜色,使得每条边两头顶点的颜色是不同的,这样的图也称为二色图。图1中的图是二分图。

 

 

二分图的例子

 

二分图的最大匹配

给定一个二分图G,在G的一个子图M中,M的边集中的任意两条边都不依附于同一个顶点,则称M是一个匹配。

如果一条边eM中,我们称其为匹配的,否则是自由的。

一个顶点v如果和一条匹配的边关联,则称该点为匹配的,否则是自由的。

匹配M的规模定义为M中所有匹配边的数量,一个图中的最大匹配是具有最大规模的匹配。

完全匹配,也称作完备匹配是这样一个匹配,图中的每个顶点都被匹配。

 

增广路径

在一个无向图G=VE)中给定一个匹配M,若P是图G中一条连通两个未匹配顶点的路径,并且属于M的边和不属于M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径。

         例如,在图2中,匹配的边用绿色的虚线表示,M={15),(28}是一个匹配,顶点3467是自由的,而1258是匹配的,边(15)和(28)是匹配的,(16)、(25)、(35)、(37)、(38)和(47)是自由的。路径3516是关于M的一条增广路径。

 

 

 图增广路径3516(绿色虚线表示匹配边)

 

了解了上述有关二分图的概念后,我们如何求二分图的最大匹配呢?不要告诉我先找出全部匹配,然后保留匹配数最多的,因为这个算法的复杂度是边数的指数级函数。必需要寻求一种更加高效的算法。

假设M是二分图的一个匹配,如何对它进行改进,以便找到一个包含更多边的匹配呢?显然,无论是UV,如果其中每个顶点都是匹配的,即作为M中一条边的端点,那么该匹配无法再改进,M已经是最大匹配了。因此,如果当前匹配可以改进,UV都必须包含未匹配顶点(自由顶点),即还有顶点没有和M中的任何边发生联系。例如,对于图3来说,当Ma={48),(59}时,顶点1236710是自由的,而顶点4589是匹配的。

另一个显而易见的事实是,我们只要增加一条连接两个自由顶点的边,立刻就扩大匹配。例如,图3 a中,把(16)加入匹配Ma={48),(59},就可以得到一个更大的匹配Mb={16),(48),(59}(图3b 。我们现在对顶点2做匹配,以求得一个比Mb更大的匹配。唯一的做法就是把边(26)加入新的匹配中。为了做到这一点只能把(,6)移走,但作为补偿,可以把(17)包含到新的匹配中。新的匹配MC={17),(26),(48),(59}显示在图3c中。

一般来说,我们通过构造一条增广路径来扩大当前的匹配,这条路径一头连接U中的自由顶点,另一头连接V中的自由顶点,路径上的边交替出现在E-MM中。也就是说,路径上的第一条边不属于M,第二条边属于M,依次类推,直到最后一条不属于M的边。由于增广路径的长度总是奇数,把奇数边加入M,偶数边从M中删除,就可以生成一个新的匹配,该匹配比M多一条边。

因此,对于增广路径我们可以得出下述三个结论: 

1P的路径长度必定为奇数,第一条边和最后一条边都不属于M 

2P经过取反操作可以得到一个更大的匹配M’。 

3MG的最大匹配当且仅当不存在相对于M的增广路径。 

由此,我们构造出一个构造二分图匹配的通用方法:从某初始匹配(例如空集合)开始,求出一个增广路径,并沿着该路径对当前匹配进行增广,如果无法再找到增广路径,算法终止并返回最后匹配,该匹配就是最大的。

这种用增广路径求二分图最大匹配的算法被称作匈牙利算法,是由匈牙利数学家Edmonds1965年提出的。 

 

算法轮廓: 

 1.M为空 

2.找出一条增广路径P,通过取反操作获得更大的匹配M’代替

3.重复(2)操作直到找不出增广路径为止

 

样例程序:

【输入】

13个整数,V1,V2的节点数目n1,n2,G的边数m

2-m+1行,每行两个整数t1,t2,代表V1中编号为t1的点和V2中编号为t2的点之间有边相连

【输出】

1个整数ans,代表最大匹配数

 

program match_matrix;

const

  maxn=100;

var

  edge:array[1..maxn,1..maxn] of boolean;    {邻接矩阵,记录的是两个点之间是否有边相连}

  result:array[1..maxn] of integer;                {记录当前连接方式,记录的是与当前点匹配的另一端节点}

  visited:array [1..maxn] of boolean;             {记录是否遍历过,防止死循环}

  m,n1,n2,i,t1,t2,ans:integer;

 

function dfs(p:integer):boolean;

var

  j:integer;

begin

  for j:=1 to n do

    if edge[p,j] and not(visited[j]) then           {有边存在且此j节点没有被搜索过}

      begin

        visited[j]:=true;                                  {j节点已被搜索过}

        if (result[j]=0)or dfs(result[j]) then       {j点在另一端无匹配点或有匹配点并从匹配点出发有增广路径}

          begin

            result[j]:=p;exit(true);                     {标记j点的匹配点presult}

          end;

      end;

  exit(false);

end;

 

begin

  readln(n,m);                                        {n个元素,m条边。例:某大学n个学生可以选修m门课}

  fillchar(edge,sizeof(edge),false);

  for i:=1 to m do

    begin

      readln(t1,t2);edge[t1,t2]:=true;         {读入边,并在邻接矩阵中标记}

    end;

  fillchar(result,sizeof(result),0);

  ans:=0;

  for i:=1 to n do                        {循环n1次,每次确定1个元素被匹配}

    begin

      fillchar(visited,sizeof(visited),false);           {每次调用dfs寻找增广路经前所有顶点都标记为未访问}

      if dfs(i) then inc(ans);

    end;

  writeln(ans);

end.

   

 【递归算法描述】

 1从点A出发的增广路径一定首先连向一个在原匹配中没有与点A配对的点B。如果点B在原匹配中没有与任何点配对,则它就是这条增广路径的终点;反之,如果点B已与点C配对,那么这条增广路径就是从AB,再从BC,再加上从点C出发的增广路径。并且,这条从C出发的增广路径中不能与前半部分的增广路径有重复的点。

2、算法的核心是dfs(p:integer)这个函数,它的作用是:寻找顶点p可能匹配的顶点。对于每个可以与p匹配的顶点j,假如它未被匹配,可以直接使用jp匹配;假如j已与某顶点x匹配,那么只需调用dfs(x)来求证x是否可以与其它顶点匹配,如果返回true的话,仍可以使jp匹配,这就是一次dfs。每次dfs时,要标记访问到的顶点(visited[j]=true),以防死循环和重复计算;每次dfs开始前所有的顶点都是未标记的。主过程只需对某一端的每个顶点调用dfs,如果返回一次true,就对最大匹配数加一;一个简单的循环就求出了最大匹配的数目。

 

posted @ 2011-10-18 15:18  木小漾  阅读(467)  评论(0编辑  收藏  举报