Tarjan算法小结1——SCC

来源:互联网 发布:东莞知美医院可靠吗 编辑:程序博客网 时间:2024/06/15 17:18

引入

  许多最短单源路径算法,如Dijkstra,SPFA, floyd, Bellman-Ford等,在运用时只能给出指定点到任意点的最短距离,抑或是给出图中是否有环的信息,并不能准确确定环的个数、包含的点等等。那么我们该如何解决这类问题呢?
  例题:
  (来源:https://www.luogu.org/problemnew/show/2863)
  约翰的N (2 <= N <= 10,000)只奶牛非常兴奋,因为这是舞会之夜!她们穿上礼服和新鞋子,别 上鲜花,她们要表演圆舞.
只有奶牛才能表演这种圆舞.圆舞需要一些绳索和一个圆形的水池.奶牛们围在池边站好, 顺时针顺序由1到N编号.每只奶牛都面对水池,这样她就能看到其他的每一只奶牛.
  为了跳这种圆舞,她们找了 M (2< M< 50000)条绳索.若干只奶牛的蹄上握着绳索的一端, 绳索沿顺时针方绕过水池,另一端则捆在另一些奶牛身上.这样,一些奶牛就可以牵引另一些奶 牛.有的奶牛可能握有很多绳索,也有的奶牛可能一条绳索都没有.
  对于一只奶牛,比如说贝茜,她的圆舞跳得是否成功,可以这样检验:沿着她牵引的绳索, 找到她牵引的奶牛,再沿着这只奶牛牵引的绳索,又找到一只被牵引的奶牛,如此下去,若最终 能回到贝茜,则她的圆舞跳得成功,因为这一个环上的奶牛可以逆时针牵引而跳起旋转的圆舞. 如果这样的检验无法完成,那她的圆舞是不成功的.
  如果两只成功跳圆舞的奶牛有绳索相连,那她们可以同属一个组合.
  给出每一条绳索的描述,请找出,成功跳了圆舞的奶牛有多少个组合?
  输入格式:

Line 1: Two space-separated integers: N and M
Lines 2..M+1: Each line contains two space-separated integers A and B that describe a rope from cow A to cow B in the clockwise direction.

  输出格式:

Line 1: A single line with a single integer that is the number of groups successfully dancing the Round Dance.(成功跳了圆舞的奶牛的组数)

  分析:显然此题目标明确,考察求有向图中环的个数。
  知识预备:如果两个顶点可以相互通达,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
  Tarjan算法可以在O(N+M)的时间复杂度下找出该图强连通分量的个数及成员。它是一种基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
  因此我们需要如下变量:
  

const int N=10005;//最大奶牛数struct sd{    int node;//当前节点    vector <int> next;//下一个连接到的节点};sd data[N];int low[N];//栈中最早的与自己在同一环中的位置int dfn[N];//时间戳,表示dfs时的顺序 bool vis[N];int color[N];//染色结果(染色即将同一强连通分量标记为同一号码)bool gone[N]; //表示某节点是否在栈中int dye[N];//统计每种颜色的个数int dfnnum=0,col=0;//col表示染色的种类,dfnnum为遍历的顺序 stack <int> mystack;//博主喜欢STL,当然手写栈也是可以的

工作原理

  考虑dfs过程中两种可能遇到的环状的情况:
  一.平行联通路:
原创图片,转载注明出处
  此时dfs至点5(栈中元素1,2,3,4,5)——>回溯至点2(栈中元素1,2)——>搜索至点4(栈中元素1,2)——>点4不在当前遍历栈中——>2和4是两个独立的强连通分量。(单独一个点也要算一个强连通分量)
  二.环状联通路
  原创图片,转载注明出处
  
  此时dfs至点5(栈中元素1,2,3,4,5)——>回溯至点4(栈中元素1,2,3,4)——>搜索至点2(栈中元素1,2,3,4)——>点2在当前遍历栈中——2和4是一个强连通分量中的元素——>继续搜索点2,发现无其他可走路径——>退栈直至再次到达点2,将其间所有元素标记为一个颜色。
  
  理解了这两点,整个算法的核心也就不难理解了。
  

inline void tarjan(int p){    gone[p]=true;//入栈    dfnnum++;    dfn[p]=dfnnum;    low[p]=dfn[p];    vis[p]=true;    mystack.push(p);    for(register int i=data[p].next.size()-1;i>=0;i--)    {        int tar=data[p].next[i];        if(dfn[tar]==0)//未访问过的节点         {            tarjan(tar);//继续向下搜索            low[p]=min(low[tar],low[p]);            //此步骤是在找到环后将low值改为根节点low值方便统计数量        }        else if(vis[tar])//栈中有此点,找到环         {            low[p]=min(low[p],dfn[tar])        }    }    if(dfn[p]==low[p])//回溯涂色,将整个强连通分量涂成一种颜色     //dfn[p]==low[p]说明当前节点为子树根节点,递归过程后所有在该强连通    //分量内的点都已入栈    {        vis[p]=false;        col++;        color[p]=col;        while(mystack.top()!=p)//退栈时标记颜色        {            color[mystack.top()]=col;            vis[mystack.top()]=false;//别忘了改回来            mystack.pop();        }         mystack.pop();//根节点也应该弹出,应注意    }}

下面贴上main函数:

int main(){    int cow, rope,a,b;    memset(gone,false,sizeof(gone));    memset(dye,false,sizeof(dye));    memset(vis,false,sizeof(vis));    memset(dfn,false,sizeof(dfn));    scanf("%d%d",&cow,&rope);    for(int i=1;i<=rope;i++)    {        scanf("%d%d",&a,&b);        data[a].next.push_back(b);    }    for(int i=1;i<=cow;i++)//为了确保没有“离群的牛”每头牛都尝试搜一遍     {        if(!gone[i])        {            tarjan(i);        }    }    int ans=0;    for(int i=1;i<=cow;i++)//确定颜色数量     {        if(dye[color[i]]==0)        {            dye[color[i]]++;            continue;        }        if(dye[color[i]]==1)        {            dye[color[i]]++;            ans++;        }        else dye[color[i]]++;    }    printf("%d",ans);    return 0;} 

  ※博主个人认为Tarjan算法较为抽象,推荐大家在纸上模拟推演几遍出栈入栈搜索的操作,才可能熟练准确运用此算法。
  小结1到此结束,下期的小结2我们将会探讨Tarjan算法的缩环为点的操作。

原创粉丝点击