Tarjan

来源:互联网 发布:广贵交易软件 编辑:程序博客网 时间:2024/06/04 18:19

Tarjan求有向图的强连通分量(Tarjan算法描述)

分类: 算法整理 1130人阅读 评论(0) 收藏 举报
算法struct

        强连通分量是有向图中的概念,我们先说强连通分量的定义吧:在一个图的子图中,任意两个点相互可达,也就是存在互通的路径,那么这个子图就是强连通分量(或者称为强连通分支)。如果一个有向图的任意两个点相互可达,那么这个图就称为强连通图。

        我们常用的求强连通分量的算法有两个,一个是Kosaraju算法,这个算法是基于两次dfs来实现的;还有一个就是Tarjan算法,这个算法完成一次dfs就可以找到图中的强连通分支。我的这篇文章主要介绍Tarjan算法。

       Tarjan算法是基于这样一个原理:如果u是某个强连通分量的根,那么:

(1)u不存在路径可以返回到它的祖先

(2)u的子树也不存在路径可以返回到u的祖先。

        因此我们在实现Tarjan算法的时候,使用dfsnum[i]记录节点i被访问的时间,也可以理解为在访问该点之前已经访问的点的个数。然后使用数组low[i]记录点i或者i的子树最小可以返回到的节点(在栈中)的次序号。

        这里还要说一下low[i]的更新过程,

if(v是i向下dfs的树边) low[i]=min(low[i],low[v]);//这里也就是说low[i]表示i或者i的子树所能追回到的最小的点序号。

if(v不是树边也不是横叉边) low[i]=min(low[i],dfsnum[v]);//其实这里你直接更新成low[v]代替dfsnum[v]也是可以的

        根据上面的原理,我们可以发现只有当dfsnum[i]==low[i]的时候就正好是强连通分量的根。这个时候我们把在栈中的点(在遇到根之前在栈中的点)出栈,并且标记好点所属的强连通分支的编号。

        整个Tarjan算法跑下来就可以完成强连通分支的求解了。

        下面我贴上我的在HDU 1269上判断一个图是否是强连通图的代码,这个代码其实就完成了Tarjan算法,最后只要简单判断下整个图是否是只有一个强连通分支就可以了。

[cpp] view plaincopy
  1. #include <string.h>  
  2. #include <stdio.h>  
  3. #include <stdlib.h>  
  4. #define MAX 100010  
  5. int dfsnum[MAX],dfsNum,low[MAX];  
  6. int sccnum[MAX],sccNum;  
  7. int instack[MAX],st[MAX],top;  
  8.   
  9. typedef struct EDGE  
  10. {  
  11.     int v,next;  
  12. }edge;  
  13. edge e[MAX];  
  14. int edgeNum;  
  15. int head[MAX];  
  16.   
  17. void insertEdge(int a,int b)  
  18. {  
  19.     e[edgeNum].v=b;  
  20.     e[edgeNum].next=head[a];  
  21.     head[a]=edgeNum++;  
  22. }  
  23.   
  24. void Tarjan(int i)  
  25. {  
  26.     dfsnum[i]=low[i]=++dfsNum;  
  27.     st[top++]=i;  
  28.     instack[i]=1;  
  29.     int j=head[i];  
  30.     for(j=head[i];j!=-1;j=e[j].next)  
  31.     {  
  32.         int v=e[j].v;  
  33.         if(dfsnum[v]==0)//为树边  
  34.         {  
  35.             Tarjan(v);  
  36.             if(low[i]>low[v])  
  37.                 low[i]=low[v];  
  38.         }  
  39.         else if(instack[v])  
  40.         {  
  41.             if(low[i]>dfsnum[v])  
  42.                 low[i]=dfsnum[v];  
  43.         }  
  44.     }  
  45.     if(dfsnum[i]==low[i])  
  46.     {  
  47.         do  
  48.         {  
  49.             top--;  
  50.             sccnum[st[top]]=sccNum;  
  51.             instack[st[top]]=0;  
  52.         }while(top>=0&&st[top]!=i);  
  53.         sccNum++;  
  54.     }  
  55. }  
  56. void solve(int n)  
  57. {  
  58.     int i;  
  59.     memset(dfsnum,0,sizeof(dfsnum));  
  60.     memset(instack,0,sizeof(instack));  
  61.     dfsNum=0;  
  62.     top=0;  
  63.     sccNum=0;  
  64.     for(i=1;i<=n;i++)  
  65.     {  
  66.         if(dfsnum[i]==0)  
  67.             Tarjan(i);  
  68.     }  
  69. }  
  70. int main()  
  71. {  
  72.     int n,m;  
  73.     int a,b,i;  
  74.     while(scanf("%d %d",&n,&m))  
  75.     {  
  76.         if(m==0&&n==0)  
  77.             break;  
  78.         memset(head,-1,sizeof(head));  
  79.         edgeNum=0;  
  80.         for(i=0;i<m;i++)  
  81.         {  
  82.             scanf("%d %d",&a,&b);  
  83.             insertEdge(a,b);  
  84.         }  
  85.         solve(n);  
  86.         if(sccNum==1)  
  87.             printf("Yes\n");  
  88.         else  
  89.             printf("No\n");  
  90.     }  
  91.     return 0;  
  92. }  

(算法)Tarjan离线算法解决LCA问题 (附POJ 1470 Closest Common Ancestors 代码)

分类: 算法整理 1261人阅读 评论(0) 收藏 举报
算法cini

       对于最近公共祖先问题,我们先来看这样一个性质,当两个节点(uv)的最近公共祖先是x时,那么我们可以确定的说,当进行后序遍历的时候,必然先访问完x的所有子树,然后才会返回到x所在的节点。这个性质就是我们使用Tarjan算法解决最近公共祖先问题的核心思想。

      同时我们会想这个怎么能够保证是最近的公共祖先呢?我们这样看,因为我们是逐渐向上回溯的,所以我们每次访问完某个节点x的一棵子树,我们就将该子树所有节点放进该节点x所在的集合,并且我们设置这个集合所有元素的祖先是该节点x。那么到我们完成对一个节点的所有子树的访问时,我们将这个节点标记为已经找到了祖先的点。

       这个时候就体现了Tarjan采用离线的方式解决最近公共祖先的问题特点所在了,所以这个时候就体现了这一点。假设我们刚刚已经完成访问的节点是a,那么我们看与其一同被询问的另外一个点b是否已经被访问过了,若已经被访问过了,那么这个时候最近公共祖先必然是b所在集合对应的祖先c,因为我们对a的访问就是从最近公共祖先c转过来的,并且在从c的子树b转向a的时候,我们已经将b的祖先置为了c,同时这个c也是a的祖先,那么c必然是ab的最近公共祖先。

       对于一棵子树所有节点,祖先都是该子树的根节点,所以我们在回溯的时候,时常要更新整个子树的祖先,为了方便处理,我们使用并查集维护一个集合的祖先。总的时间复杂度是O(n+q)的,因为dfs是O(n)的,然后对于询问的处理大概就是O(q)的。

       这就是离线的Tarjan算法,可能说起来比较难说清楚,但是写起来还是比较好写。下面贴上我在POJ 1470上过的题的代码,简单的LCA问题的求解。

[cpp] view plaincopy
  1. /* 
  2. author UESTC_Nowitzki  
  3. */  
  4.   
  5. #include <iostream>  
  6. #include <cstring>  
  7. #include <cstdio>  
  8. #include <cstdlib>  
  9. #include <vector>  
  10. using namespace std;  
  11. const int MAX=1000;  
  12. int indegree[MAX];  
  13. int ancestor[MAX];  
  14. int set[MAX];  
  15. int vis[MAX];  
  16. int time[MAX];  
  17. vector<int> adj[MAX];  
  18. vector<int> que[MAX];  
  19.   
  20. void init(int n)  
  21. {  
  22.     memset(time,0,sizeof(time));  
  23.     memset(vis,0,sizeof(vis));  
  24.     memset(indegree,0,sizeof(indegree));  
  25.     for(int i=1;i<=n;i++)  
  26.     {  
  27.         adj[i].clear();  
  28.         que[i].clear();  
  29.         set[i]=i;  
  30.         ancestor[i]=i;  
  31.     }  
  32. }  
  33.   
  34. int find(int k)  
  35. {  
  36.     int r=k;  
  37.     while(set[r]!=r)  
  38.         r=set[r];  
  39.     int i=k,j;  
  40.     while(set[i]!=r)  
  41.     {  
  42.         j=set[i];  
  43.         set[i]=r;  
  44.         i=j;  
  45.     }  
  46.     return r;  
  47. }  
  48.   
  49. void dfs(int i)  
  50. {  
  51.     int len=adj[i].size();  
  52.     for(int j=0;j<len;j++)  
  53.     {  
  54.         int son=adj[i][j];  
  55.         dfs(son);  
  56.         set[son]=i;  
  57.         ancestor[find(i)]=i;  
  58.     }  
  59.     vis[i]=1;  
  60.     len=que[i].size();  
  61.     for(int j=0;j<len;j++)  
  62.     {  
  63.         int son=que[i][j];  
  64.         if(vis[son])  
  65.         {  
  66.             int ans=ancestor[find(son)];  
  67.             time[ans]++;  
  68.         }  
  69.     }  
  70. }  
  71.   
  72. int main()  
  73. {  
  74.     int n,i,t,a,b;  
  75.     while(scanf("%d",&n)!=EOF)  
  76.     {  
  77.         init(n);  
  78.         for(i=0;i<n;i++)  
  79.         {  
  80.             scanf("%d:(%d)",&a,&t);  
  81.             while(t--)  
  82.             {  
  83.                 scanf("%d",&b);  
  84.                 indegree[b]++;  
  85.                 adj[a].push_back(b);  
  86.             }  
  87.         }  
  88.         scanf("%d",&t);  
  89.         while(t--)  
  90.         {  
  91.             while(getchar()!='(');  
  92.             scanf("%d%d",&a,&b);  
  93.             que[a].push_back(b);  
  94.             que[b].push_back(a);  
  95.         }  
  96.         while(getchar()!=')');  
  97.         for(i=1;i<=n;i++)  
  98.         {  
  99.             if(indegree[i]==0)  
  100.             {  
  101.   //              printf("root=%d\n",i);  
  102.                 dfs(i);  
  103.                 break;  
  104.             }  
  105.         }  
  106.         for(i=1;i<=n;i++)  
  107.         {  
  108.             if(time[i]>0)  
  109.                 printf("%d:%d\n",i,time[i]);  
  110.         }  
  111.     }  
  112.     return 0;  
 

Tarjan算法求解桥和边双连通分量(附POJ 3352 Road Construction解题报告)

分类: 算法整理 1489人阅读 评论(1) 收藏 举报
construction算法

       在说Tarjan算法解决桥和边双连通分量问题之前我们先来回顾一下Tarjan算法是如何求解强连通分量的。

       Tarjan算法在求解强连通分量的时候,通过引入dfs过程中对一个点访问的顺序dfsNum(也就是在访问该点之前已经访问的点的个数)和一个点可以到达的最小的dfsNum的low数组,当我们遇到一个顶点的dfsNum值等于low值,那么该点就是一个强连通分量的根。因为我们在dfs的过程中已经将点仍入到栈中,因此我们只需要将栈中的元素出栈直到遇到根,那么这些点就组成一个强连通分量。

        对于边双连通分量,我们需要先了解一些概念:

边连通度:使一个子图不连通所需要删除的最小的边数就是该图的边连通度。

桥(割边):当删除一条边就使得图不连通的那条边称为桥或者是割边。

边双连通分量:边连通度大于等于二的子图称为边双连通分量。

        理解了这些概念之后我们来看看Tarjan是如何求解边双连通分量的,不过在此之前我们先说说Tarjan是怎样求桥的。同样引入了dfsNum表示一个点在dfs过程中所被访问的时间,然后就是low数组表示该点最小的可以到达的dfsNum。我们分析一下桥的特点,删除一条边之后,那么如果dfs过程中的子树没有任何一个点可以到达父亲节点及父亲节点以上的节点,那么这个时候子树就被封死了,这条边就是桥。有了这个性质,也就是说当我们dfs过程中遇到一条树边a->b,并且此时low[b]>dfsNum[a],那么a-b就是一座桥。

        呵呵桥都求出来了,还怕边双连通分量吗?我们把所有的桥去掉之后那些独立的分量就是不同的边双连通分量,这个时候就可以按照需要灵活的求出边双连通分量了。

       下面附上POJ 3352的解题思路吧:

       这道题的意思是说,给你一个无向图,然后问你至少需要添加几条边,可以使整个图变成边双连通分量,也就是说任意两点至少有两条路可以互相连通。我们这样考虑这个问题,属于同一个边双连通分量的任意点是至少有两条通路是可以互相可达的,因此我们可以将一个边双连通分量缩成一个点。然后考虑不在边双连通分量中的点,通过缩点后形成的是一棵树,我们发现如果要将一棵树连成一个边双连通分量是需要(叶子节点数+1)/2的边数。这样问题就是变成缩点之后,求树的叶子节点的个数了。

       这个题目的条件给的很强,表示任意两个点之间不会有重边,因此我们可以直接经过Tarjan的low值进行边双连通分量的划分,最后求出叶子节点数就可以解决问题了。如果是有重边的话,那么不同的low值是可能是属于同一个边双连通分量的,这个时候就要通过将图中的桥去掉然后求解边双连通分量,这个请见我的博客的另外一篇解题报告。

        下面贴上POJ 3352的ac代码,供网友们参考:

[cpp] view plaincopy
  1. #include <iostream>  
  2. #include <cstring>  
  3. #include <cstdlib>  
  4. #include <cstdio>  
  5. #include <vector>  
  6. using namespace std;  
  7. const int Max=1010;  
  8. int top[Max],edge[Max][Max];//memset(top,0,sizeof(top));  
  9. int dfsNum[Max],dfsnum;//memset(dfsNum,0,sizeof(dfsNum)),dfsNum=1;  
  10. int low[Max];  
  11. int degree[Max];  
  12. int ans;  
  13.   
  14. void tarjan(int a,int fa)  
  15. {  
  16.     dfsNum[a]=low[a]=++dfsnum;  
  17.     for(int i=0;i<top[a];i++)  
  18.     {  
  19.         if(edge[a][i]!=fa)  
  20.         {  
  21.             if(dfsNum[edge[a][i]]==0)  
  22.             {  
  23.                 tarjan(edge[a][i],a);  
  24.                 if(low[a]>low[edge[a][i]])  
  25.                     low[a]=low[edge[a][i]];  
  26.             }  
  27.             else  
  28.             {  
  29.                 if(low[a]>dfsNum[edge[a][i]])  
  30.                     low[a]=dfsNum[edge[a][i]];  
  31.             }  
  32.   //          if(low[edge[a][i]]>dfsNum[a])  
  33.   //          {  
  34.   
  35.    //         }  
  36.         }  
  37.     }  
  38. }  
  39.   
  40. int solve(int n)  
  41. {  
  42.     int i,j;  
  43.     int a,b;  
  44.     for(i=1;i<=n;i++)  
  45.     {  
  46.         a=i;  
  47.         for(j=0;j<top[i];j++)  
  48.         {  
  49.             b=edge[a][j];  
  50.             if(low[a]!=low[b])  
  51.             {  
  52.                 degree[low[a]]++;  
  53.                 degree[low[b]]++;  
  54.             }  
  55.         }  
  56.     }  
  57.     int leaves=0;  
  58.     for(i=1;i<=n;i++)  
  59.     {  
  60.         if(degree[i]==2)  
  61.         {  
  62.             leaves++;  
  63.         }  
  64.     }  
  65.     return (leaves+1)/2;  
  66. }  
  67.   
  68. int main()  
  69. {  
  70.     int n,m;  
  71.     int i,a,b;  
  72.     while(scanf("%d %d",&n,&m)!=EOF)  
  73.     {  
  74.         memset(top,0,sizeof(top));  
  75.         memset(degree,0,sizeof(degree));  
  76.         for(i=0;i<m;i++)  
  77.         {  
  78.             scanf("%d %d",&a,&b);  
  79.             edge[a][top[a]++]=b;  
  80.             edge[b][top[b]++]=a;  
  81.         }  
  82.   
  83.         memset(dfsNum,0,sizeof(dfsNum));  
  84.         dfsnum=0;  
  85.   
  86.         tarjan(1,-1);  
  87.         ans=solve(n);  
  88.         printf("%d\n",ans);  
  89.     }  
  90.     return 0;  

原创粉丝点击