poj 2186 Popular Cows Tarjan 强连通分量
Time Limit: 2000MS | Memory Limit: 65536K | |
Total Submissions: 20472 | Accepted: 8316 |
Description
popular, even if this is not explicitly specified by an ordered pair in the input. Your task is to compute the number of cows that are considered popular by every other cow.
Input
* Lines 2..1+M: Two space-separated numbers A and B, meaning that A thinks B is popular.
Output
Sample Input
3 3 1 2 2 1 2 3
Sample Output
1
Hint
Source
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #include<vector> 6 #include<set> 7 #include<queue> 8 #include<string> 9 #include<cmath> 10 #include<fstream> 11 #include<iomanip> 12 #include<climits> 13 #include<cfloat> 14 15 using namespace std; 16 17 #define MAX_INT 0x7fffffff 18 #define MAX_LL 0x7fffffffffffffff 19 #define ULL unsigned long long 20 #define LL long long 21 #define MAX(x,y) ((x) > (y) ? (x) : (y)) 22 #define MIN(x,y) ((x) < (y) ? (x) : (y)) 23 24 #define MAXN 11111 25 #define MAXM 55555 26 int n,m; 27 28 int h[MAXN],next[MAXM],u[MAXM],v[MAXM]; 29 30 int tm,dfn[MAXN],s[MAXN]; 31 int id[MAXN],cnt,top; 32 int low[MAXN]; 33 int num[MAXN]; 34 35 void dfs(int x){ 36 int y,e; 37 dfn[x]=low[x]=++tm; 38 s[top++]=x; 39 for(e=h[x]; e!=-1; e=next[e]){ 40 y=v[e]; 41 if(!dfn[y]) dfs(y),low[x]=MIN(low[x],low[y]); 42 else if(id[y]==-1) low[x]=MIN(low[x],dfn[y]); 43 } 44 if(low[x]==dfn[x]){ 45 ++cnt; //判断条件可改为id[x]==-1,这表明x还在栈中 46 num[cnt]=0; 47 do{ 48 y=s[--top]; 49 id[y]=cnt; 50 num[id[y]]++; 51 }while(x!=y); 52 /* num[id[x]=++cnt]=1; 53 while((y=s[--top])!=x) printf("%d\n",cnt),num[id[y]=cnt]++; */ 54 } 55 } 56 57 void tarjan(){ 58 memset(dfn, 0, sizeof(dfn)); 59 memset(id, -1, sizeof(id)); 60 tm=cnt=top=0; 61 for(int i=1; i<=n; i++) 62 if(!dfn[i]) dfs(i); 63 } 64 65 int main(){ 66 //freopen("C:\\Users\\Administrator\\Desktop\\in.txt","r",stdin); 67 while(scanf(" %d %d",&n,&m)==2){ 68 int i,j; 69 memset(h, -1, sizeof(h)); 70 for(i=0; i<m; i++){ 71 scanf(" %d %d",&u[i], &v[i]); //邻接表建图 72 next[i]=h[u[i]]; h[u[i]]=i; 73 } 74 tarjan(); //求强连通分量 75 memset(s, 0, sizeof(s)); 76 for(i=0; i<m; i++){ //排除非出度为0的强连通分量 77 int x=u[i],y=v[i]; 78 if(id[x]!=id[y]) s[id[x]]++; 79 } 80 for(tm=0,i=1; i<=cnt; i++) //统计出度为0的强连通分量 81 if(!s[i]) tm++,j=i; 82 if(tm>1) printf("0\n"); 83 else printf("%d\n",num[j]); 84 } 85 return 0; 86 }
首次学用tarjan求强连通分量算法,抄一抄算法漂亮的解析:
1.byvoid:https://www.byvoid.com/blog/scc-tarjan/
有向图强连通分量的Tarjan算法
[有向图强连通分量]
在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。
下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
直接根据定义,用双向遍历取交集的方法求强连通分量,时间复杂度为O(N^2+M)。更好的方法是Kosaraju算法或Tarjan算法,两者的时间复杂度都是O(N+M)。本文介绍的是Tarjan算法。
[Tarjan算法]
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。由定义可以得出,
Low(u)=Min
{
DFN(u),
Low(v),(u,v)为树枝边,u为v的父节点
DFN(v),(u,v)为指向栈中节点的后向边(非横叉边)
}
当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。
算法伪代码如下
tarjan(u)
{
DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值
Stack.push(u) // 将节点u压入栈中
for each (u, v) in E // 枚举每一条边
if (v is not visted) // 如果节点v未被访问过
tarjan(v) // 继续向下找
Low[u] = min(Low[u], Low[v])
else if (v in S) // 如果节点v还在栈内
Low[u] = min(Low[u], DFN[v])
if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
repeat
v = S.pop // 将v退栈,为该强连通分量中一个顶点
print v
until (u== v)
}
接下来是对算法流程的演示。
从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。
返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。
返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。
继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。
至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。
可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。
求有向图的强连通分量还有一个强有力的算法,为Kosaraju算法。Kosaraju是基于对有向图及其逆图两次DFS的方法,其时间复杂度也是 O(N+M)。与Trajan算法相比,Kosaraju算法可能会稍微更直观一些。但是Tarjan只用对原图进行一次DFS,不用建立逆图,更简洁。 在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。
求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量的Tarjan算法,以及求最近公共祖先的离线Tarjan算法,在此对Tarjan表示崇高的敬意。
附:tarjan算法的C++程序
void tarjan(int i)
{
int j;
DFN[i]=LOW[i]=++Dindex;
instack[i]=true;
Stap[++Stop]=i;
for (edge *e=V[i];e;e=e->next)
{
j=e->t;
if (!DFN[j])
{
tarjan(j);
if (LOW[j]<LOW[i])
LOW[i]=LOW[j];
}
else if (instack[j] && DFN[j]<LOW[i])
LOW[i]=DFN[j];
}
if (DFN[i]==LOW[i])
{
Bcnt++;
do
{
j=Stap[Stop--];
instack[j]=false;
Belong[j]=Bcnt;
}
while (j!=i);
}
}
void solve()
{
int i;
Stop=Bcnt=Dindex=0;
memset(DFN,0,sizeof(DFN));
for (i=1;i<=N;i++)
if (!DFN[i])
tarjan(i);
}
[参考资料]
BYVoid 原创作品,转载请注明。
2.wikipedia
Tarjan算法
Tarjan算法 (以发现者Robert Tarjan[1]命名)是一个在图中寻找强连通分量的算法。虽然发表时间更早,它仍可以被视为Kosaraju算法的一个改进。它的效率跟Gabow算法差不多。
概述
此算法以一个有向图作为输入,并按照所在的强连通分量给出其顶点集的一个划分。图中的每个结点只在一个强连通分量中出现,即使是在有些结点单独构成一个强连通分量的情况下(比如图中出现了树形结构或孤立结点)。
算法的基本思想如下:任选一结点开始进行深度优先搜索(若深度优先搜索结束后仍有未访问的结点,则再从中任选一点再次进行)。搜索过程中已访问的结点不再访问。搜索树的若干子树构成了图的强连通分量。
结点按照被访问的顺序存入栈中。从搜索树的子树返回至一个结点时,检查该结点是否是某一强连通分量的根结点(见下)并将其从栈中删除。如果某结点是强连通分量的根,则在它之前出栈且还不属于其他强连通分量的结点构成了该结点所在的强连通分量。
根结点的性质
算法的关键在于如何判定某结点是否是强连通分量的根。注意“强连通分量的根”这一说法仅针对此算法,事实上强连通分量是没有特定的“根”的。在这里根结点指深度优先搜索时强连通分量中首个被访问的结点。
为找到根结点,我们给每个结点v一个深度优先搜索标号v.index,表示它是第几个被访问的结点。此外,每个结点v还有一个值v.lowlink,表示从v出发经有向边可到达的所有结点中最小的index。显然v.lowlink总是不大于v.index,且当从v出发经有向边不能到达其他结点时,这两个值相等。v.lowlink在深度优先搜索的过程中求得,v是强连通分量的根当且仅当v.lowlink = v.index。
伪代码
algorithm tarjan is input: 图 G = (V, E) output: 以所在的强连通分量划分的顶点集 index := 0 S := empty // 置栈为空 for each v in V do if (v.index is undefined) strongconnect(v) end if function strongconnect(v) // 将未使用的最小index值作为结点v的index v.index := index v.lowlink := index index := index + 1 S.push(v) // 考虑v的后继结点 for each (v, w) in E do if (w.index is undefined) then // 后继结点w未访问,递归调用 strongconnect(w) v.lowlink := min(v.lowlink, w.lowlink) else if (w is in S) then // w已在栈S中,亦即在当前强连通分量中 v.lowlink := min(v.lowlink, w.index) end if // 若v是根则出栈,并求得一个强连通分量 if (v.lowlink = v.index) then start a new strongly connected component repeat w := S.pop() add w to current strongly connected component until (w = v) output the current strongly connected component end if end function
变量index是深度优先搜索的结点计数器。S是栈,初始为空,用于存储已经访问但未被判定属于任一强连通分量的结点。注意这并非一个一般深度优先搜索的栈,结点不是在以它为根的子树搜索完成后出栈,而是在整个强连通分量被找到时。
最外层循环用于查找未访问的结点,以保证所有结点最终都会被访问。strongconnect进行一次深度优先搜索,并找到结点v的后继结点构成的子图中所有的强连通分量。
当一个结点完成递归时,若它的lowlink仍等于index,那么它就是强连通分量的根。算法将在此结点之后入栈(包含此结点)且仍在栈中的结点出栈,并作为一个强连通分量输出。
备注
- 复杂度:对每个结点,过程strongconnect只被调用一次;整个程序中每条边最多被考虑两次。因此算法的运行时间关于图的边数是线性的,即。
- 判断结点v'是否在栈中应在常数时间内完成,例如可以对每个结点保存一个是否在栈中的标记。
- 同一个强连通分量内的结点是无序的,但此算法具有如下性质:每个强连通分量都是在它的所有后继强连通分量被求出之后求得的。因此,如果将同一强连通分量收缩为一个结点而构成一个有向无环图,这些强连通分量被求出的顺序是这一新图的拓扑序的逆序[2]。
参考
- ^ Tarjan, R. E., Depth-first search and linear graph algorithms, SIAM Journal on Computing. 1972, 1 (2): 146–160, doi:10.1137/0201010
- ^ Harrison, Paul. Robust topological sorting and Tarjan's algorithm in Python. [9 February 2011].