二分图最大匹配Hungary算法详解

二分图最大匹配——匈牙利算法

1. 二分图

    首先要介绍下二分图。

   二分图又称为二部图,是图论中的一个特殊模型。

    设有一无向图G,G的所有顶点可以分割为两个互不相交的子集A和B,且图中每一条边所关联的两个点分属于两个集合A,B,那么图G便是一个二分图

2. 最大匹配

  对于一个二分图G,存在一个子集M,使得M的边集中的任意两条边都不关联同一个顶点,则M是图G的一个匹配。

    显然,边数最大的子集M即为图的最大匹配。

   举个例子:有一批男女,异性之间互有好感,不存在同性恋(嘻嘻),一个人可以同时对多名异性有好感。这样我们就可以根据暗恋关系建图,互相有好感则连一条边。这样得到的就是一个二分图。因为我们可以将其分为男性和女性两个互不相交的子集,且两个子集之间有连线,同一个子集之中没有连线。       然后让我们当一回月老,进行牵线。规定不准脚踏多只船,一个人只能和一名异性牵手成功。而且要根据好感决定,互相没有好感的不能牵手。那么怎样选择能使牵手的情侣数最多呢?这就是一个求最大匹配的问题了。

3. 求法?

       那我们应该怎么求最大匹配呢?

    有种方法就是枚举,枚举点,枚举边,然而数量一大就原地爆炸。奈何?要找优秀算法。

3.1 增广路径

   我们引入增广路径的概念。

    设二分图G中存在一条路径,它的起点和终点都是未被匹配的点(不属于子集M的点),不重复经过点和边,且路径中已匹配的边(属于M的边)和未匹配的边(不属于M的边)交替出现,则该路径是一条增广路径。

    还是上面的男女关系问题。假设,有男性3人:A,B,C;女性3人:X,Y,Z。下面用<-->表示互相有好感:

       A<-->X   A<-->Z   B<-->X  C<-->Y  C<-->Z 

    于是建出二分图。现在初步决定A-X牵手,C-Z牵手,那么就存在这样一条增广路径:

       B<-->X<==>A<-->Z<==>C<-->Y   (<-->表示未被选择的关系    <==>表示已被选择的关系)

    这条路径中相邻点在图中都有连边。于是我们发现:

 结论1:增广路径中边的个数必为奇数,相邻点必然是一个已匹配,一个未匹配。且始边和终边都不属于子集M(即都未被匹配)

   这一结论显而易见。因为是交替的,且开始和结束要相同,边数自然是奇数,相邻点必然是一个已匹配,一个未匹配。又因为起点和终点都不属于子集M,与他们相关联的始边和终边自然不会属于子集M。(但若一个点属于子集M,与该点相关联的某一条边不一定属于子集M)

 结论2:对增广路径取反,可以得到更大的匹配方案

    这一点让我们用上面的例子来帮助理解。对于增广路径 B<-->X<==>A<-->Z<==>C<-->Y ,如果我们把已经匹配的边取出,反而放入没有匹配的边,即身份交换,我们可以得到 B<==>X<-->A<==>Z<-->C<==>Y,这依然是一条交替路,但是匹配数多了1。这样操作之后依旧符合要求(没有脚踏多只船,没有同性相吸),却得到了更优解。是不是很妙呀?所以由这一条结论我们可以得出下一条结论:

 结论3:若找得到增广路径,则当前方案不是最大匹配;反之亦然。

    这根据结论二很容易得出。

    综上所述,我们知道,求最大匹配,一个关键就是求增广路径。对于一个新图,不停地寻找增广路径,不停地优化匹配方案,直到找不到增广路径为止这便是匈牙利算法

    那么程序如何实现?

3.2 Hungary程序实现

    为了方便实现,我们采用枚举顶点扩展增广路的方式。定义一个函数如下(伪代码)


func found(x):boolean                      //布尔类型函数 x为枚举到的A子集顶点下标。采用dfs找增广路
    for i 1 to m                           //枚举B子集顶点
    {
        if (not used)and(u{x,i})           //当点x与点i间有边连接,且该点未被访问过时
        {
            used => TRUE                   //更新该点为已访问
            if (match=0)or(found(match))   //当这个点未被匹配,或者已经匹配了但它的匹配点可以扩出一条增广路
            {
                match => x                 //该点的匹配点改为x
                exit(TRUE)                 //返回真,表示已找到
            }
        }
    }
    exit(FALSE)                            //运行至此搜遍了点但仍未找到,则返回假,说明该点无增广路可以扩展

main{                                      //主程序中调用

for i 1 to n                               //枚举A子集中的顶点
{
    used => FALSE                          //初始化访问列表(均未访问过)
    if found(i)                            //如果以点i拓展出了增广路
    {
        ans => ans+1                       //那么匹配数加1
    }
}

}
    上面便是代码的实现过程。简洁易懂。其中used match可以用一维数组储存,边集u可用邻接矩阵或邻接表存储。

    要注意两次的循环中是按A、B两子集的顶点数分别循环的,n表示A子集顶点数,m表示B子集顶点数。

    下面是完整Pascal模板


var
 match:array[0..1001]of longint; //记录B子集中元素所相匹配的A子集元素下标
 used:array[0..1001]of boolean; //记录B子集中元素是否被访问过
 a:array[0..1001,0..1001]of boolean; //邻接矩阵存储边
 i,j,k,n,m,l,ans,t:longint;

function found(x:longint):boolean; //寻找增广路径
var
 i,j:longint;
begin
 for i:=1 to m do //枚举B子集中元素
  if (a[x,i])and(not used[i]) then //如果两点之间有边且没有访问过
   begin
    used[i]:=TRUE; //记录为已访问
    if (match[i]=0)or(found(match[i])) then //如果该点未被匹配,或相邻点能引出另一条增广路径
     begin
      match[i]:=x; //更新
      exit(TRUE); //返回已找到
     end;
   end;
 exit(FALSE); //无法找到
end;

begin
{文件操作}

//输入部分可做改动
 readln(n,m,t); //该模板中N为A子集点数,M为B子集点数,T为边数
 for i:=1 to n do
  for j:=1 to m do
   a[i,j]:=FALSE;
 for i:=1 to t do
  begin
   readln(j,k);
   a[j,k]:=TRUE;
  end;
//输入部分可做改动

//Hungary算法,计算最大匹配数
 ans:=0; //匹配数清零
 fillchar(match,sizeof(match),0); //匹配子集清空
 for i:=1 to n do //枚举A子集中元素
  begin
   fillchar(used,sizeof(used),FALSE); //每次都要清空访问列表
   if found(i) then ans:=ans+1; //如果从点i出发能找到增广路径就将匹配数加1
  end;

//输出部分可做改动
 writeln(ans);
//输出部分可做改动

{文件操作}
end.


4. Hungary应用实战

 4.1 luogu P1129 矩阵游戏

  二分图匹配,难点在建图。我们可以发现,不论如何操作,原本在同一列或行黑色方格不可能分离到不同列或行,原本不在同一列或行的黑色方格也不可能合并到同一列或行。抓住这一点,就可以想到一些微妙的关系。将行与列作为二分子集,某行与某列交点有黑色方格,则该行与该列之间连边,最后判断匹配数是否达到行列数。数据量不大,由此用匈牙利算法很快水过。


var
 used:array[0..201]of boolean;
 match:array[0..201]of longint;
 a:array[0..201,0..201]of boolean;
 i,j,k,n,m,l,t,cnt:longint;

function found(x:longint):boolean;
var
 i,j:longint;
begin
 for i:=1 to n do
  if (a[x,i])and(not used[i]) then
   begin
    used[i]:=TRUE;
    if (match[i]=0)or(found(match[i])) then
     begin
      match[i]:=x;
      exit(TRUE);
     end;
   end;
 exit(FALSE);
end;

begin
 readln(t);
 repeat
  t:=t-1;
  readln(n);
  l:=0;
  for i:=1 to n do
   for j:=1 to n do
    begin
     read(k);
     if k=1 then begin a[i,j]:=TRUE;l:=l+1;end else a[i,j]:=FALSE;
    end;
  if l<n then writeln('No') else
   begin
    cnt:=0;
    fillchar(match,sizeof(match),0);
    for i:=1 to n do
     begin
      fillchar(used,sizeof(used),FALSE);
      if found(i) then cnt:=cnt+1;
     end;
    if cnt>=n then writeln('Yes') else writeln('No');
   end;
 until t<=0;
end.

 4.2 luogu P2055 假期的宿舍

    这道题稍稍复杂些,感觉有点绕。但是最大匹配的算法显而易见,关键是如何处理。

    二分图的两个子集分别是需要留宿的学生和床。学生和能睡的床间连线,然后进行匹配,最后判断匹配数是否达到要求即可。

    有几点要注意:一是n个学生并非全部要留宿,最后比较时不应与n比较,而要与留宿的学生比较; 二是输入中每个学生自己与自己的关系没有标为1,但是自己当然是可以睡自己的床的,所以不要忘记自己与自己的床也要连线!三是n个学生也不是所有学生都有床!所以dfs的时候要记得判断该学生是否是在校生

 
var
 match:array[0..51]of longint;
 used,could,bed:array[0..51]of boolean;
 a:array[0..51,0..51]of boolean;
 i,j,k,n,m,l,cnt,t:longint;

function found(x:longint):boolean;
var
 i,j:longint;
begin
 for i:=1 to n do
  if (a[x,i])and(not used[i])and(could[i]) then
   begin
    used[i]:=TRUE;
    if (match[i]=0)or(found(match[i])) then
     begin
      match[i]:=x;
      exit(TRUE);
     end;
   end;
 exit(FALSE);
end;

begin
 readln(t);
 repeat
  t:=t-1;
  readln(n);
  m:=0;
  for i:=1 to n do
   begin
    read(k);
    if k=1 then could[i]:=TRUE else could[i]:=FALSE;
   end;
  for i:=1 to n do
   begin
    read(k);
    if (could[i]) then if k=0 then bed[i]:=TRUE else bed[i]:=FALSE;
    if not(could[i]) then bed[i]:=TRUE;
    if bed[i] then m:=m+1;
   end;
  for i:=1 to n do
   for j:=1 to n do
    begin
     read(k);
     if (k=1)or(i=j) then a[i,j]:=TRUE else a[i,j]:=FALSE;
    end;
  cnt:=0;
  fillchar(match,sizeof(match),0);
  for i:=1 to n do
   begin
    if not(bed[i]) then continue;
    fillchar(used,sizeof(used),FALSE);
    if found(i) then cnt:=cnt+1;
   end;
  if (cnt>=m)and(m<=n) then writeln('^_^') else writeln('T_T');
 until t<=0;
end.

4.3-luogu P2825 游戏

   典型的最大匹配问题,难点也在于选取建图要素。

   也许我把每个相互有冲突的点都连线,转而求、、、求什么?最大独立集?照样不够优秀。

   要是这样想:炸弹是沿横向和纵向两个方向爆炸,那我们就分割横向和纵向。把每一行、列中的联通条分别标号,成为两个子集。当横向联通条与纵向联通条相交在某一块空地上时,将他们连边,说明会相互影响。然后求最大匹配即可。预处理过程可能有些繁琐。其实只要先记下某一个方向下的联通条标号,之后在对另一个方向的联通条标号的同时进行判断,当某一格是空地时就对该格上两个方向下的联通条连边就行了。


var
 match:array[0..2001]of longint;
 used:array[0..2001]of boolean;
 a:array[0..2001,0..2001]of boolean;
 f,x,y:array[0..51,0..51]of longint;
 i,j,k,n,m,l,ans,cnt,tmp:longint;
 c:char;
 canset:boolean;

function found(x:longint):boolean;
var
 i:longint;
begin
 for i:=1 to tmp do
  if (a[x,i])and(not used[i]) then
   begin
    used[i]:=TRUE;
    if (match[i]=0)or(found(match[i])) then
     begin
      match[i]:=x;
      exit(TRUE);
     end;
   end;
 exit(FALSE);
end;

begin
 readln(n,m);
 for i:=1 to n do
  begin
   for j:=1 to m-1 do
    begin
     read(c);
     case c of
      '#':f[i,j]:=9;
      'x':f[i,j]:=5;
      '*':f[i,j]:=1;
     end;
    end;
   readln(c);
   case c of
    '#':f[i,m]:=9;
    'x':f[i,m]:=5;
    '*':f[i,m]:=1;
   end;
  end;

 cnt:=0;
 for i:=1 to n do
  begin
   cnt:=cnt+1;
   for j:=1 to m do
     if f[i,j]<>9 then x[i,j]:=cnt
      else begin cnt:=cnt+1;x[i,j]:=0;end;
  end;

 fillchar(a,sizeof(a),FALSE);
 tmp:=0;
 for j:=1 to m do
  begin
   tmp:=tmp+1;
   for i:=1 to n do
    begin
     if f[i,j]<>9 then y[i,j]:=tmp
      else begin tmp:=tmp+1;y[i,j]:=0;end;
     if (x[i,j]<>0)and(y[i,j]<>0)and(not a[x[i,j],y[i,j]])and(f[i,j]=1)
      then a[x[i,j],y[i,j]]:=TRUE;
    end;
  end;
 fillchar(match,sizeof(match),0);
 ans:=0;
 for i:=1 to cnt do
  begin
   fillchar(used,sizeof(used),FALSE);
   if found(i) then ans:=ans+1;
  end;
 writeln(ans);
end.


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值