zoukankan      html  css  js  c++  java
  • 《啊哈算法》——割点、割边、二分图

      这篇文章我们简单的介绍求解图的割点、割边和二分图相关的概念。

      割点:

      对于含n个点、m条边的连通无向图G,如果去掉顶点vi(并同时去掉与之相连的边),使得G不再连通,那么称vi是一个割点。

      通过其定义,我们不难判断某个点是否是割点,但是现在我们面临的问题是,如何给出一个图G,编码让计算机求解割点呢?

      首先我们考虑这样一个问题,判定某个点的指标是什么。我们通过人脑来判断其是否是割点,其实是利用非常模糊的视觉效应,即“通过去掉该点观察图是否连通”即可,而如果想要通过计算机来判断,就需要非常量化的判断条件。

      我们考虑从深度优先搜索的角度来找到这样一个判断条件,利用dfs遍历图,得到的生成子图本质上会得到一个生成树,我们拿出两个相邻的点vi、vj,vi是vj的父节点。我们回到深搜遍历的过程中,假设当前遍历到vj,如果我们从vj能够找到一条回到已经访问过的v1、v2...等节点,那么这表明去掉vi,将不会影响剩余图的连通性。

      我们似乎发现了些什么,但是这种判定关系还是有些模糊。

      我们借用这样一个概念——时间戳,即深度优先搜索的过程中,我们记录访问节点的顺序,我们用num[i]来表示节点vi的时间戳,即在深搜遍历过程中第几个访问vi节点。借用这个工具,我们考虑能不能将上述我们描述的关系用量化的表达式表示出来呢?好像还是有点捉襟见肘啊,我们不妨再设置一个数组low[i],用以表达vi不经过dfs的生成树的父节点所能够到达的时间戳最小的节点(好好理解,非常拗口),基于这个工具,我们能够看到上述的判断条件,可以用这样一个表达式简洁的概括:

                                                                              low[j] < num[i]

      那么现在我们首要的问题似乎变成了求解n个节点的low[]、num[]了。

      首先,对于num[],也就是时间戳的记录,并不困难。而对于low[]数组的求解,就需要动一些脑筋了。我们模拟遍历过程,当前遍历到vi点,我们访问所有与vi连通的点vj,会出现如下两种情况。

      1.vj访问过,被我们打上过时间戳,  那么我们此时需要更新low[i]了,即low[i] = min{num[j] | vj与vi连通}。

      2.vj没有访问过,那么我们继续深搜遍历点的过程。

      在遍历完成之后,也完成了num[]、low[]的求解,我们再利用深搜的回溯过程,完成判断即可。

      这里需要注意的一点是,对于某个图的根节点,即dfs开始的那个点(记作v1)其实是不满足上文给出的判断式子的,需要我们特殊判断,记child是根节点的子树个数,则v1是个割点的必要条件是,child  = 2。

      简单的参考代码如下。

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    
    int n , m , e[9][9] , root;
    int num[9] , low[9] , flag[9],index;
    int min(int a , int b)
    {
         return a < b ? a : b;
    }
    void dfs(int cur , int father)
    {
        int child = 0 , i , j;
    
          index++;
          num[cur] = index;
          low[cur] = index;
          for(i = 1;i <= n;i++)
          {
                if(e[cur][i] == 1)
                {
                       if(num[i] == 0)  //第一种情况
                       {
                             child++;
                             dfs(i,cur);
                             low[cur] = min(low[cur] , low[i]);//回溯过程:判断割点
    
                             if(cur != root && low[i] >= num[cur])
                                  flag[cur] = 1;
                             else if(cur == root && child == 2)
                                  flag[cur] = 1;
                       }
                       
                        else if(i != father) //第二种情况
                        {
                            low[cur] = min(low[cur] , num[i]);
                        }
    
                }
          }
    }
    
    int main()
    {
         int i , j, x , y;
         scanf("%d %d",&n,&m);
         for(i = 1;i <= n;i++)
              for(j = 1;j <= n;j++)
                 e[i][j] = 0;
         for(i = 1;i <= m;i++)
         {
              scanf("%d %d",&x,&y);
              e[x][y] = 1;
              e[y][x] = 1;
         }
    
         root = 1;
         dfs(1,root);
    
         for(i = 1;i <= n;i++)
         {
               if(flag[i] == 1)
                  printf("%d ",i);
         }
    
         return 0;
    }

      割边:

      有个割点的概念,割点非常好理解,即对于图G,如果删除边ei,导致G的连通度发生变化,那么ei即是G的一个割边。

      那么我们来继续思考如何利用编程实现求G的割边。

      基于上文我们对割点问题的思考,这里问题会显得非常简单,在判断割点的时候,我们利用的核心判断条件是low[j] >= num[i],其中vi是vj的父节点。那么拿到割边上来,我们分两部分看,如果low[j]>num[i],则表明去掉eij后,vj便不再和vj连通,这是符合割边定义的。而如果low[j] = num[i],则表明去掉eij,vj依然能够在不经过eij的情况下到达vi,连通度没有发生改变。

      因此我们可以看到,对于互相连通的父子节点vi、vj,满足 low[j]>num[i],可判定eij是一条割边。

      基于dfs找割点的代码,我们进行稍微的改动,有如下代码。

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    
    int n , m , e[9][9] , root;
    int num[9] , low[9] , flag[9],index;
    int min(int a , int b)
    {
         return a < b ? a : b;
    }
    void dfs(int cur , int father)
    {
        int  i , j;
    
          index++;
          num[cur] = index;
          low[cur] = index;
          for(i = 1;i <= n;i++)
          {
                if(e[cur][i] == 1)
                {
                       if(num[i] == 0)  //第一种情况
                       {
    
                             dfs(i,cur);
                             low[cur] = min(low[cur] , low[i]);//回溯过程:判断割点
    
                             if(low[i] > num[cur])
                                  printf("%d-%d
    ",cur,i);
                       }
    
                        else if(i != father) //第二种情况
                        {
                            low[cur] = min(low[cur] , num[i]);
                        }
    
                }
          }
    }
    
    int main()
    {
         int i , j, x , y;
         scanf("%d %d",&n,&m);
         for(i = 1;i <= n;i++)
              for(j = 1;j <= n;j++)
                 e[i][j] = 0;
         for(i = 1;i <= m;i++)
         {
              scanf("%d %d",&x,&y);
              e[x][y] = 1;
              e[y][x] = 1;
         }
    
         root = 1;
         dfs(1,root);
    
    
         return 0;
    }

      二分图的最大匹配:

      我们先给出这样导入模型的实际问题:现有n个妹子和n个汉字相约去坐过山车,过山车的结构是两两一排,现在要求坐一排的必须是一男一女,并且要求两人必须相互认识,那么请问我们最多能安排多少对男女上过山车?

      我们将问题抽象化,将每个个体视为点,而男女之间的是否认识视为点与点之间的边,这里仅仅是强调了男生和女生的联系,其余的关系我们不考虑,因此我们将男生放入A集合(该集合中的元素之间不存在边),女生放入B集合,可以看到,这是典型的二分图。

      而我们将一对一对的男女送到两座一排的过山车的过程,抽象得来看,可以用图论中的术语——匹配,来表示。即将A中每个元素与B中的元素形成一一对应的关系,需要强调的是,只可以使一一对应的关系,而这种一一对应的匹配的对数,便称作匹配数量。即高度概括一下我们即将导入的模型——如何求解二分图的最大匹配数量。

      我们注意到“最多”这个字眼,容易联想到其与贪心算法有着密切的联系,因此我们考虑从这个角度给出一个计算二分图最大匹配数量的算法。

      考虑将全局问题给子问题化然后寻求局部最优解,这样方能引导出全局最优解。假设含2n个顶点的二分图的一个分图A含有n个顶点,我们依次遍历集合A中的点v1、v2...vn,我们从过程开始分析,假设当前遍历到vi点,显然,我们尽可能的将vi匹配到B集合中的某个和vi相连的点,是当前局部最优的策略,那么我们不妨再遍历B集合中与vi相连的点vi'、vj'......容易看到,对于这些点(以vi'为例),都会满足如下的两个性质之中的一个:

      1.vi'在之前A集合(i-1)个点遍历的过程中,并没有与前i-1个点匹配。

      2.vi'在之前A集合(i-1)个点遍历的过程中,和前i-1个点中的某个点进行了匹配。

      针对情况1,我们当然可以将vi与vi'匹配,则匹配数+1,这是当前的最优策略。

      针对情况2,就显得有些麻烦,既然vi'在A中已经有了匹配点,那么我们就此放弃vi了么?显然不是,我们需要经过深思熟虑才能决定是否放弃vi。我们注意到初始情况的二分图G是存在一对多的情况,即A集合中的某个点可能与B集合中的多个点均有边,这其实就像是在匹配的时候留下了“其他选项 ”,其实就十分像我们利用dfs实现的回溯寻找迷宫,而我们在面对这种情况的时候,则需要利用dfs来回溯回去来尝试所有这些“其他选项”,判断在A集合前i-1个点在最大匹配数的基础上,能否将vi返回情况1.如果存在,那则匹配数+1,这是当前的最优策略;如果不存在,那么即可放弃vi的匹配(想一想,为什么这里就可以直接放弃了,这种做法其实和前面的铺垫是自洽的)。

      理解了上文对求解二分图最大匹配数算法的过程的描述,其正确性是不言自明的。概括起来,它本质上是一种贪心策略和基于dfs的穷举策略。

      简单的参考代码如下。

    #include<cstdio>
    using namespace std;
    
    int e[101][101];
    int match[101];
    int book[101];
    int n , m;
    int dfs(int u)
    {
         int i;
         for(i = 1;i <= n;i++)
         {
              if(book[i] == 0 && e[u][i] == 1)
              {
                    book[i] = 1;
                       if(match[i] == 0 || dfs(match[i]))
                       {
                            match[i] = u;
                            match[u] = i;
                            return 1;
                       }
              }
         }
    
         return 0;
    }
    
    int main()
    {
          int i , j , t1 , t2 , sum = 0;
          scanf("%d %d",&n,&m);
    
          for(i = 1;i <= m;i++)
          {
                scanf("%d %d",&t1,&t2);
                e[t1][t2] = 1;
                e[t2][t1] = 1;
          }
    
            for(i = 1;i <= n;i++)   match[i] = 0;
    
            for(i = 1;i <= n;i++)
            {
                  for(j = 1;j <= n;j++)  book[j] = 0;
                      if(dfs(i))  sum++;
            }
    
            printf("%d",sum);
    
            return 0;
    }
  • 相关阅读:
    今天面试一些程序员(新,老)手的体会
    UVA 10635 Prince and Princess
    poj 2240 Arbitrage
    poj 2253 Frogger
    poj 2485 Highways
    UVA 11258 String Partition
    UVA 11151 Longest Palindrome
    poj 1125 Stockbroker Grapevine
    poj 1789 Truck History
    poj 3259 Wormholes
  • 原文地址:https://www.cnblogs.com/rhythmic/p/5515819.html
Copyright © 2011-2022 走看看