太久不做题,发现很多学过的算法又通通不记得了,或许因为当时既没有深刻理解,也没有及时总结。最近开始复习图论,说是复习,不如说重新学习更加恰当。算法是进入大学紧跟着c语言接触到的第二项“技术”,虽然在竞赛中就好像最终创业失败一样不得善终,总还是希望不辜负那段年轻的时光,也不辜负难得一份坚持了这么久的兴趣。

    我的图论学习也不是从Tarjan算法开始的,而是最近在看双连通分量相关问题的时候又想起了当年学习Tarjan时的一些好笑经历。当时学习完求强连通分量的Tarjan解法,又发现双连通分量也是用这个算法,以当时对其浅薄的认识还以为是同一个算法却不知为何可以解决不同的问题。后来才知道包括解决LCA问题在内的这些算法,都是由图灵奖获得者Robert Tarjan这位计算机泰斗所创,可惜他没有创造出更多更艺术的算法名字(==)。为了能够保留我再次学习算法的这些浅薄认识,也为了表达我对Tarjan的膜拜,我决定从Tarjan算法开始我的学习总结。
  1.  双连通分量
         点连通度:直观来说,就是使一个连通图成为非连通图所需要去掉的最小点数。
         边连通度:同理就是使一个连通图成为非连通图所需要去掉的最小边数。
         割点:去掉这个点及所连的边,原图不连通
         割边(桥):去掉这条边原图不连通
         双连通图:一个无向连通图的点连通度大于1(不存在割点),那么该图就是点双连通图,如果边双连通度大于1,该图就是边双连通图(不存在桥,或者说图的任意两点间都有两条不相交的路径,如果有相交的边,显然这条边就是桥)。
         双连通分量:是一个图的极大双连通子图。
      双连通还是有很大的实际意义的,这一点我觉得也是图论有意思且牛叉的地方所在。比如一个通信网络,某一个通信点故障(割点),或一条通信线路故障(桥)会不会影响整个网络的通信。
    在一个图中,显然是由割点将各个双连通分量连接起来的(要么它们根本不连通),要求双连通分量,关键就是要把割点找到。
    什么样的点是割点呢?有下面两个图(对于一个无向图进行一次dfs后的搜索树):
  
                                     
          在这两个图中U都是割点,因为对于第一个图,u不为树根,且u的儿子以及它们的后代没有指向u的祖先的后向边(有一个后代成立便成立,因为如上图即使v2有一条后向边,u仍为割点,因为它连接了v1及其后代)。
          对于第二个图,u为树根,并且u有两个及以上的儿子,显然u将以其儿子们为根的子树分隔开来。
     为了判断一个点是不是割点,显然需要了解它的儿子与它的祖先的关系,为此,引入一个Low[]数组,来标记一个点能追溯到的最早的祖先标号(标号dfn[]由dfs获得),由第一个图知道,当dfn[u]<=low[v1]时,就能判断u是一个割点。
     low[u]的计算过程如下:
     a. dfs第一次搜索到u节点时,low[u] = dfn[u];
     b. u的儿子s已经被访问过(检查是不是指向祖先的后向边),low[u] = Min{low[u],dfn[s]};
     c. 检查u的未被访问的儿子们s,low[u] = Min{low[u],low[s]}。
 
     同时,判断桥也类似,设一条无向边(u,s),它为桥的条件是dfn[u]<low[s],注意这里不能取等号,我觉得可以简单理解为在下图的情况下,u仍是割点,(u,s)却不是桥。
                          
          怎么求割点和桥已经比较清晰了,模板用题目测过再拿过来吧。
    【题目1】用POJ 2117测了一下求割点的算法,这个题目大意是要找去掉哪个点可以使原图分成最多的连通块,是一个比较简单的求割点的题目,核心代码如下(我又很无力地用了vector):
void DFS(int x){
    dfn[x] = low[x] = cnt++;
    int len = mp[x].size();
    for (int i = 0; i < len; i++)
    {
        if (dfn[mp[x][i]] == -1)
        {
            if (x == root)
                son ++;
            DFS(mp[x][i], x);
            low[x] = min(low[x],low[mp[x][i]]);
            if (x == root && son > 1 || x != root && dfn[x] <= low[mp[x][i]])
                cut[x] ++;
        }
        else
        {
            low[x] = min(low[x],dfn[mp[x][i]]);
        }
    }
}

   求解点双连通分量:会求割点和桥,求双连通分量只需要再增加一个stack即可,dfs时每访问一条边就将这条边加入stack,当出现割点u(dfn[u]<=low[v])的时候开始退栈,退到边(u,v)为止,退出栈的这些边和点就构成了一个双连通分支。(stack里也可以存点,但是存点要考虑割点会存在多个连通分量这件事,退栈的时候要小心,存边的时候只要把边的两头节点都放在一个连通分量里就ok了)

     求边双连通分量:目前没有实际做过,通用的解法是先求出桥,原图删除桥之后就变成了一个一个的连通分量了。

     ps.重边的问题:若两点之间有重边,显然这也组成了一个边双连通,在DFS时对重边要特殊处理一下(例如做一个与父节点之间的标记),当然有的问题要求重边也算桥(比如poj 3177)总之要注意是否有重边,重边该怎样处理。

     【题目2】poj 2942,Knights of ther Round Table,是一道非常综合的图论题目,核心内容也是双连通分量。这道题目的大意是,n个人,有些人互相憎恨,要剔除一些人使剩下的人围成一个圈,满足:圈中有奇数个人;相邻的人不互相憎恨。注意同一个人可以参加多个会议。

   1. 首先,求出原图的补图(由憎恨图变成和谐图,边两端的点表示可以相邻)

   2. 对补图求点双连通分量,一个双连通分量里若存在奇圈,那么这个连通分量里的点就都可以参加会议了。因为这个分量里的点会几个一组分布在一个奇圈上,不知道怎么证明,只画了一些图。(比如原来的点分布在一个偶圈上,内部有一条边跟偶数条圈边形成一个奇圈,显然这条边跟剩下的偶数条边也形成了奇圈)相反,双连通分量里若不存在奇圈,那么这个连通分量里的点就都不能参加会议了。(注意点双连通分量里一个点可能存在多个双连通分量中,所以在一个双连通分量的点被删去在另一个双连通分量可能不需要被删去)

  3. 判断奇圈用到交叉染色的方法,对一条边的两个点染上不同的颜色,如果能够染成功,就说明没有奇圈。(判断二分图的方法,一个图是二分图当且仅当没有奇圈)

View Code
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 1005;

vector<int>mp[N];
vector<int>cut[N];
int G[N][N],low[N],dfn[N],stack[N],mark[N],color[N];
int m,n,cnt,stack_ptr,cut_num,flag;
void DFS(int x, int father)
{
    low[x] = dfn[x] = ++cnt;
    stack[stack_ptr++] = x;
    int len = mp[x].size();
    for (int i = 0; i < len; i++){
        int t = mp[x][i];
        if (!dfn[t]){
            DFS(t,x);
            low[x] = min(low[x], low[t]);
            if (dfn[x] <= low[t]) {
                cut[cut_num].push_back(x);        
                while (stack[stack_ptr-1] != t)
                    cut[cut_num].push_back(stack[--stack_ptr]);
                cut[cut_num++].push_back(stack[--stack_ptr]);
            }
        }
        else if (t != father)
            low[x] = min(low[x], dfn[t]);
    }
}
void check(int x, int d, int col,int fa)
{
    color[x] = col;
    int len = mp[x].size();
    for (int i = 0; i < len; i++){
        int t = mp[x][i];
        if (t == fa) continue;
        vector<int>::iterator it = find(cut[d].begin(),cut[d].end(),t);
        if (it != cut[d].end())
            if (color[t] == 0)
                check(t,d,-col,x);
            else if (color[t] == col)
                flag = 1;
    }
}
int Tarjan()
{
    memset(low,0,sizeof(low));
    memset(dfn,0,sizeof(dfn));
    memset(cut,0,sizeof(cut));
    cnt = stack_ptr = cut_num = 0;
    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            DFS(i,-1);
    memset(mark,0,sizeof(mark));
    for (int i = 0; i < cut_num; i++){
        memset(color,0,sizeof(color));
        int len = cut[i].size();
          flag = 0;
        if(len >= 3){
            check(cut[i][0],i,1,-1);
            if (flag)
                for (int j = 0; j < len; j++)
                    mark[cut[i][j]] = 1;
        }
    }
    int ans = 0;
    for (int i = 1; i <= n; i++)
        if (!mark[i])
            ans++;
    return ans;
}
int main()
{
    while (scanf("%d%d",&n,&m) && n+m)
    {
        int a,b;
        for (int i = 0; i <= n; i++){
            mp[i].clear();
            cut[i].clear();
        }
        memset(G,0,sizeof(G));
        while (m--){
            scanf("%d%d",&a,&b);
            G[a][b] = G[b][a] = 1;
        }
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                if (i!=j && G[i][j]==0)
                    mp[i].push_back(j);
        printf("%d\n",Tarjan());
    }

    return 0;
}

 

posted on 2013-04-09 23:57  swcandy  阅读(391)  评论(0)    收藏  举报