一般图匹配


前置知识:

二分图匹配

并查集

BFS 匈牙利算法

一般图最大匹配

传送门

题目的要求从二分图匹配比那成了一般图匹配,其实和 BFS 版的匈牙利算法想法有些相似:找增广路。但是题目的图变成了一般图,所以就会有环的出现。那么问题的聚焦点就是对环的处理。

环分为奇环和偶环,其中偶环可以“自产自消”但是奇环不行,所以我们重点处理的也是奇环。

偶环:

偶环

(其中,一条线代表路径,两个点可以互通,两条线代表两个点匹配,一个蓝点匹配一个粉点。)

带花树定义:可以处理图中有奇环的情况,将奇环缩成一个点(算法中叫做一朵花),然后再类似于匈牙利算法通过找增广路来找这个图的最大匹配。

我们不直接讲怎么处理奇环,先讲代码思路,碰见问题再说。

  • 数组初始化,读入基本信息(加边),如果都没有匹配,就先匹配上。

  • 尝试没有匹配的点,能不能让其它点改配,从而让这个点匹配成功。

  • 初始化信息(每个第一次没有匹配的点都会尝试,所以一定要初始化)。

这里我们用的信息有这些:标记该节点是否尝试过匹配,或是否已经加入尝试改配的序列了。

注意:这个数组要记录三中状态:$1$ 尝试换配,$2$ 尝试过的点,$0$ 没有尝试过的点。

想想一下,现在有 $4$ 个点,$a$ 和 $b$ 可以匹配,$b$ 和 $c,d$ 可以匹配,$d$ 和 $c$ 也可以匹配。但是现在,$b$ 和 $c$ 匹配上了(这显然不优)。

我们尝试让 $a$ 匹配,$a$ 可以匹配 $b$ 但是 $b$ 已经匹配了,这是将 $b$ 打上标记 $2$ 代表尝试过的点,而 $c$ 打上标记 $1$ 代表尝试换配的点。因为,如果 $c$ 可以匹配其他的,那么就将 $b$“让”出来了,那么 $a$ 也就匹配成功了,所以 $c$ 入队尝试匹配。

记得开新数组,因为这时的 $b$ 还要存储 $a$,是 $a$ 尝试匹配,这样如果匹配成功了,方便回溯。

发现 $c$ 和 $d$ 还可以匹配,那么就让 $c$ 和 $d$ 匹配,$b$ 被让出来了,利用刚刚开的新数组回溯让 $a$ 和 $b$ 匹配。

  • 尝试节点入队,尝试匹配,不断进行标记,如果出现了已经打过标记 $1$ 的节点,那么说明成环了,而且是奇环。

匹配遇见了标记 $1$ 的节点,我们现在遍历的点(尝试换配的点)全是标记为 $1$ 的点,而两个标号为 $1$ 的点相遇了,说明是奇环。如下图所示:

奇环:

奇环

(和上面的偶环的规则一样。这里蓝色节点代表了标记是 $1$。)

上图只是一个奇环,所以可能看不出什么名堂,请看下面这个图:

奇环换配

在第一个小图中可以看到,匹配的并不是最优的,那么我们就要尝试让他更优,我们从红点开始处理,尝试匹配。

(上面的紫虚边代表如果试配成功,那么换配后,紫虚边就换变成黑实边,也就是匹配边,而黑虚边就会消失;而如果匹配失败,黑虚边就会变回黑实边,紫虚边消失。)

最后匹配成功也就让整个环“合理”了(代码中标记的是“逆环”操作(名字乱起的,明白最重要))。

我们上边只是一个奇环,简单的一张图,如果是环套多个环(环中有环),那么怎么处理呢?

首先,要将整个环选出一个祖先(不管大环还是小环,缩完点后,就可以看成点,环套环就可以把里面的环先缩成点,再将外面的环缩成点。),大体流程如下(为了减少篇幅画一块了,先看左边的一列):

找祖先

注意:找祖先的时候,要将两个“碰头”的点同时往回找。如果只让一个点往回找的话,就直接回到绿点(开始的地方)了。

  • 如果匹配成功要记得换配(更新匹配关系)。

  • 统计结果,输出即可。

细节详见代码。

代码思路整理

  • 数组初始化,读入基本信息。

  • 尝试没有匹配的点。

  • 每个点匹配都要初始化信息。

  • 尝试更多匹配,匹配成功换配。

  • 统计结果,输出。

代码

#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<queue>
#include<vector>
#include<stack>
using namespace std;
long long r_r(){//快读 
  long long k=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    k=(k<<1)+(k<<3)+(c^48);
    c=getchar(); 
  }
  return k*f;
}
const int o_o=2e5+10;
queue<int>q_q;
int n,m;
int a_s;//统计结果 
int l_k[o_o];//记录和哪个点匹配 
int b_b[o_o];//标记 1 尝试换配,2 尝试过的点,0 没有尝试过的点 
int f_a[o_o];//记录父节点 
int p_r[o_o];//记录从那个点来的 
int x_x;//环缩点后的标号 
int g_h[o_o];//共环的编号 

//链式前向星 
struct po{
  int v;
  int n_t;
}p_p[o_o];
int h_d[o_o],x_p;
void a_d(int u,int v){//加边 
  p_p[++x_p].v=v;
  p_p[x_p].n_t=h_d[u];
  h_d[u]=x_p;
}
void n_w(int x){//换配 
  if(!x)return ;
  n_w(l_k[p_r[x]]);//一直走到头 
  
  //换配 
  l_k[x]=p_r[x];
  l_k[p_r[x]]=x;
}
int f(int k){//找父节点 
  if(k==f_a[k])return k;
  return f_a[k]=f(f_a[k]);//路径压缩 
}
int l_c(int x,int y){
  x_x++;//开新点(环缩点) 
  while(1){
    x=f(x);//上爬,爬到父节点(如果不是自己的话,相当于直接出环) 
    if(g_h[x]==x_x)return x;
    //跑到已经标记过的点了,定义当前点为这个环的祖先 
    else if(x)g_h[x]=x_x;//更新节点标记 
    x=p_r[l_k[x]];//更新为当前节点匹配的节点的来的点
    //下一个可能换配的点,即标记 b_b 为 1 的点 
    swap(x,y);//交换标号,两头一起跑,可以成环相遇
    //如果只有一个点跑就跑回起点了 
  }
}
void s_k(int x,int y,int f_f){
  while(f(x)!=f_f){//不是子节点 
    p_r[x]=y;//更新记录来的点 
    y=l_k[x];//记录匹配的点 
    f_a[x]=f_a[y]=f_f;//匹配的一对点更新父节点 
    if(b_b[y]==2){//遍历过 
      b_b[y]=1;//标记 
      q_q.push(y);//入队尝试 
    }
    x=p_r[y];//更新到 y 的点 
  }
}
int b_s(int x){
  for(int i=1;i<=n;i++){//初始化信息 
    b_b[i]=0;
    p_r[i]=0;
    f_a[i]=i;//初始化父节点 
  }
  b_b[x]=1;//标记 
  q_q=queue<int>();//清空队列 
  q_q.push(x);//将当前节点入队 
  int f_f;
  while(!q_q.empty()){
    int k=q_q.front();//提取队头 
    q_q.pop();
    for(int i=h_d[k];i;i=p_p[i].n_t){
      int v=p_p[i].v; 
      if(b_b[v]==1){//标记过,尝试匹配路径成环了 
        f_f=l_c(k,v);//记录环的公共祖先 
        
        //逆环尝试 
        s_k(k,v,f_f);
        s_k(v,k,f_f);
      }else if(!b_b[v]){//没有标记过 
        p_r[v]=k;//记录来的点 
        b_b[v]=2;//标记到过节点,但是不是尝试换配的节点 
        if(!l_k[v]){//当前节点没有匹配过 
          n_w(v);//换配 
          return 1;//匹配成功 
        }else {//当前节点已经匹配 
          b_b[l_k[v]]=1;//和目标匹配的节点标记,尝试换配 
          q_q.push(l_k[v]);//入队,尝试匹配其他节点
          //如果它能匹配成功别的点,就能把这个点“让”出来 
        }
      }
    }
  }
  return 0;
}
int main(){
  n=r_r(),m=r_r();
  for(int i=1;i<=m;i++){
    int a=r_r(),b=r_r();
    //加边 
    a_d(a,b);
    a_d(b,a);
    if(!l_k[a]&&!l_k[b]){//两个点都没有匹配,先匹配上 
      l_k[a]=b;//记录匹配的点 
      l_k[b]=a;
      a_s++;//统计匹配对数 
    }
  }
  for(int i=1;i<=n;i++) 
    if(!l_k[i]&&b_s(i))a_s++;
  //节点未匹配,尝试匹配,如果匹配成功,统计结果 
  printf("%d\n",a_s);//输出匹配成功的对数 
  for(int i=1;i<=n;i++)printf("%d ",l_k[i]);//输出匹配编号 
  return 0;
}

文章作者: 王大神——A001
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 王大神——A001 !
  目录