二分图的匹配算法小结

无权二分图的匹配与匈牙利算法

二分图:简单来说就是图中的点可以分成两部分,并且每一部分里面都没有边相连,任意一条边起点和终点都不在同一个点集。

匹配:边的集合,任意两条边都没有公共点。

匹配点,匹配边:在匹配中的点和边

非匹配点,非匹配边:不在匹配集合中的点和边。

最大匹配:图的所有匹配中,含边数最多的匹配就是最大匹配。

完美匹配:匹配集合中涵盖了所有的点,所有的点都是匹配点。显然,完备匹配一定是最大匹配,注意,并不是所有的二分图都存在完美匹配。

求二分图的最大匹配数一般都是使用经典的匈牙利算法。匈牙利算法中涉及到两个概念,分别是交替路增广路交替路是指匹配边和非匹配边交替出现。增广路是指从非匹配点出发沿着交替路前进终于非匹配点的路径。若存在增广路,那么只要把增广路上匹配边和非匹配边交换角色,就能在不减少当前匹配的情况下多增加一个匹配。于是匈牙利算法的概念就很明显了,就是不断的找增广路并交换角色。


匈牙利算法的流程如下:

1.从第一个未匹配点开始寻找增广路。若找到增广路,则修改匹配情况,并且增加匹配数。如果不存在增广路,停止对当前结点的搜索,从下一个未匹配的节点开始搜索增广路。

2.寻找增广路需要标记路径节点,如果找到一个未匹配点,则说明存在增广路,此时交换匹配边增广即可。若找到匹配点,则从该匹配点沿着匹配边寻找增广路。

匈牙利算法一般用DFS实现,因为思路清晰而且简单,但是其在稀疏图的情况下,效果不如BFS,在实践中为了加速DFS,会选择先用贪心的思路先寻找到一个匹配再来增广。具体模板代码实现如下:

 

// Todo

二分图的匹配问题很经典,但是一般不会直接问,可能绕个弯子或者是加个权重。简单来说绕弯子的话就是求二分图的最小顶点覆盖数,DAG的最小路径覆盖,最大独立集。

二分图的最小顶点覆盖:在二分图中求最少的点,使得每条边至少和一个点关联。最小顶点覆盖数就等于最大匹配数(Konig 定理)(HDOJ 1150)

DAG的最小路径覆盖:用尽量少的不相交的简单路径覆盖有向无环图的所有顶点。其最小覆盖数=节点数-最大匹配数(HDOJ 1151)

最大独立集:其等于节点数-最大匹配数(HDOJ1068)

 


Kuhn-Munkres算法(二分图最大权匹配)

二分图的带权匹配,就是求出一个匹配集合,使得集合中边的权值最大或者最小。这种情况下,直接用KM算法就可以求出最大权重即可。了解KM算法首先需要了解一个概念,就是完备匹配

定义:设G=<V1,V2,E>为二分图,|V1|≤|V2|,M为G中一个最大匹配,且|M|=|V1|,则称M为V1到V2的完备匹配

在上述定义中,若|V2|=|V1|,则完备匹配即为完美匹配,若|V1|<|V2|,则完备匹配为G中最大匹配

二分图的最佳(最大权或者最小权)匹配一定是完备匹配,于是就在完备匹配的基础上再来要求边权的最大或者最小,从而求得最佳匹配。而KM算法就是这么一个思路,但是KM算法的运行要求是必须存在一个完备匹配,如果要求一个最大权匹配(不一定完备)的话就需要把不存在的边的权值赋值为0。

KM算法是求最大权重的匹配,若是要求最小权重的话,就需要将所有边取负权值,然后结果取反。若是要求边权之积最大的话,则需要对权值取对数后运行KM算法,得出结果后映射回去。

KM算法是通过给每个顶点一个标号(也叫顶标)将求最大权匹配的问题转化为求完备匹配的问题的。设顶点Xi的顶标为A[i],顶点Yi的顶标为B[i],顶点Xi与Yj之间的边权为w[i,j]。在算法执行过程中的任一时刻,对于任一条边(i,j),A[i]+B[j]>=w[i,j]始终成立,初始A[i]为与xi相连的边的最大边权,B[j]=0。KM算法的正确性基于以下定理:

设:G(V,E) 为二部图,G'(V,E') 为二部图的子图。如果对于 G' 中的任何边<x,y>满足, L(x)+ L(y)== Wx,y,我们称G'(V,E') 为 G(V,E) 的等价子图或相等子图(是G的生成子图)。

若由二分图中所有满足A[i]+B[j]=w[i,j]的边(i,j)构成的等价子图有完备匹配,那么这个完备匹配就是二分图的最大权匹配。对于二分图的任意一个匹配,如果它包含于相等子图,那么它的边权和等于所有顶点的顶标和;如果它有的边不包含于相等子图,那么它的边权和小于所有顶点的顶标和(即不是最佳匹配)。所以相等子图的完备匹配一定是二分图的最大权匹配。

算法在初始时为了使A[ i ]+B[j]>=w[i,j]恒成立,令A[ i ]为所有与顶点Xi关联的边的最大权,B[j]=0。如果当前的相等子图没有完备匹配,就按下面的方法修改顶标以使扩大相等子图,直到相等子图具有完备匹配为止。

我们求当前相等子图的完备匹配失败了,是因为对于某个X顶点,我们找不到一条从它出发的交替路。这时我们获得了一棵匈牙利树,它的叶子结点全部是X顶点。现在我们把匈牙利树中X顶点的顶标全都减小某个值d,Y顶点的顶标全都增加同一个值d,那么我们会发现:

1)两端都在交错树中的边(i,j),A[ i ]+B[j]的值没有变化。也就是说,它原来属于相等子图,现在仍属于相等子图。

2)两端都不在交错树中的边(i,j),A[ i ]和B[j]都没有变化。也就是说,它原来属于(或不属于)相等子图,现在仍属于(或不属于)相等子图。

3)X端不在交错树中,Y端在交错树中的边(i,j),它的A[ i ]+B[j]的值有所增大。它原来不属于相等子图,现在仍不属于相等子图。

4)X端在交错树中,Y端不在交错树中的边(i,j),它的A[ i ]+B[j]的值有所减小。也就说,它原来不属于相等子图,现在可能进入了相等子图,因而使相等子图得到了扩大

现在的问题就是求d值了。为了使A[ i ]+B[j]>=w[i,j]始终成立,且至少有一条边进入相等子图,d应该等于min{A[i]+B[j]-w[i,j] | Xi在交错树中,Yi不在交错树中}。

以上就是KM算法的基本思路。但是朴素的实现方法,时间复杂度为O(n^4),需要找O(n)次增广路,每次增广最多需要修改O(n)次顶标,每次修改顶标时由于要枚举边来求d值,复杂度为O(n^2)。实际上KM算法的复杂度是可以做到O(n^3)的。我们给每个Y顶点一个“松弛量”函数slack,每次开始找增广路时初始化为无穷大。在寻找增广路的过程中,检查边(i,j)时,如果它不在等价子图中,则让slack[j]变成原值与A[i]+B[j]-w[i,j]的较小值。这样,在修改顶标时,取所有不在匈牙利树中的Y顶点的slack值中的最小值作为d值即可。但还要注意一点:修改顶标后,要把所有的不在匈牙利树中的Y顶点的slack值都减去d。

Kuhn-Munkras算法流程:

(1)初始化可行顶标的值

(2)用匈牙利算法寻找完备匹配

(3)若未找到完备匹配则修改可行顶标的值

(4)重复(2)(3)直到找到相等子图的完备匹配为止 

简单的来说,KM算法就是用一个标记函数lx(i)来去找完备匹配,在找的过程中不断降低总权重使边加入匹配中。完备匹配是用匈牙利算法求的,外加一个节点函数的update。总的复杂度是O(n^4),加上松弛维护可以达到O(n^3)。


书上的O(n^4)的模板

const int maxn = 105; // 最大节点数
int W[maxn][maxn], n; // 边的权重和点的个数
int Lx[maxn], Ly[maxn]; // 节点函数
int left[maxn]; //匹配
bool S[maxn], T[maxn]; //匹配的点集

// 匈牙利算法寻找完备匹配
bool match(int i)
{
  S[i] = true;
  for (int j = 1; j <= n; ++j) if (Lx[i] + Ly[j] == W[i][j] && !T[j]) // 边权的条件
  {
    T[j] = true;
    if (!left[j] || match(left[j]))
    {
      left[j] = i;
      return true;
    }
  }
  return false;
}

// 节点更新
void update()
{
  int a = 1<<30;
  for (int i = 1; i <= n; ++i) if (S[i])
    for (int j = 1; j <= n; ++j) if (!T[j])
      a = min(a, Lx[i] + Ly[j] - W[i][j]); // 寻找最小的d
  // 修改节点的权值
  for (int i = 1; i <= n; ++i)
  {
    if (S[i]) Lx[i] -= a;
    if (T[i]) Ly[i] += a;
  }
}

void KM()
{
  // 初始化
 for (int i = 1; i <= n; ++i)
  {
    left[i] = Lx[i] = Ly[i] = 0;
    for (int j = 1; j <= n; ++j)
      Lx[i] = max(Lx[i], W[i][j]);
  }
  for (int i = 1; i <= n; ++i)
  {
    // 对每一个节点都找到匹配,即寻找完备匹配
   for (;;)
    {
      for (int j = 1; j <= n; ++j) S[j] = T[j] = 0;
      if (match(i)) break;
      else update();
    }
  }
}
来自红书加上松弛的模板,O(n^3)
const int maxn = 555;
const int inf = 1000000000;
int w[maxn][maxn], x[maxn], y[maxn];
int prev_x[maxn], prev_y[maxn], son_y[maxn], slack[maxn], par[maxn];
int lx, ly, pop;

void adjust(int v)
{
  son_y[v] = prev_y[v];
  if (prev_x[son_y[v]] != -2)
    adjust(prev_x[son_y[v]]);
}

bool find(int v)
{
  int i;
  for (i = 0; i < pop; i++)
    if (prev_y[i] == -1)
    {
      if (slack[i] > x[v]+y[i]-w[v][i])
      {
        slack[i] = x[v]+y[i]-w[v][i];
        par[i] = v;
      }
      if (x[v]+y[i] == w[v][i])
      {
        prev_y[i] = v;
        if (son_y[i] == -1)
        {
          adjust(i);
          return true;
        }
        if (prev_x[son_y[i]] != -1)
          continue;
        prev_x[son_y[i]] = i;
        if (find(son_y[i]))
          return true;
      }
    }
  return true;
}

int km()
{
  int i, j, m;
  for (i = 0; i < pop; ++i)
  {
    son_y[i] = -1;
    y[i] = 0;
  }
  for (i = 0; i < pop; ++i)
  {
    x[i] = 0;
    for (j = 0; j < pop; ++j)
      x[i] = min(x[i], w[i][j]);
  }
  bool flag;
  for (i = 0; i < pop; ++i)
  {
    for (j = 0; j < pop; ++j)
    {
      prev_x[j] = prev_y[j] = -1;
      slack[j] = inf;
    }
    prev_x[i] = -2;
    if (find(i)) continue;
    flag = false;
    while (!flag)
    {
      m = inf;
      for (j = 0; j < pop; ++j)
        if (prev_y[j] == -1)
          m = min(m, slack[j]);
      for (j = 0; j < pop; ++j)
      {
        if (prev_x[j] != -1)
          x[j] -= m;
        if (prev_y[j] != -1)
          y[j] += m;
        else
          slack[j] -= m;
      }
      for (j = 0; j < pop; ++j)
        if (prev_y[j] == -1 && !slack[j])
        {
          prev_y[j] = par[j];
          if (son_y[j] == -1)
          {
            adjust(j);
            flag = true;
            break;
          }
          prev_x[son_y[j]] = j;
          if (find(son_y[j]))
          {
            flag = true;
            break;
          }
        }
    }
  }

  int ans = 0;
  for (int i = 0; i < pop; ++i)
    ans += w[son_y[i]][i];
  return ans;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值