tarjan看了几次了,这次才感觉明白了。

Tarjan算法的操作原理如下:

Tarjan算法基于定理:在任何深度优先搜索中,同一强连通分量内的所有顶点均在同一棵深度优先搜索树中。也就是说,强连通分量一定是有向图的某个深搜树子树。

可以证明,当一个点既是强连通子图Ⅰ中的点,又是强连通子图Ⅱ中的点,则它是强连通子图Ⅰ∪Ⅱ中的点。

这样,我们用low值记录该点所在强连通子图对应的搜索子树的根节点的Dfn值。注意,该子树中的元素在栈中一定是相邻的,且根节点在栈中一定位于所有子树元素的最下方。

强连通分量是由若干个环组成的。所以,当有环形成时(也就是搜索的下一个点已在栈中),我们将这一条路径的low值统一,即这条路径上的点属于同一个强连通分量。

如果遍历完整个搜索树后某个点的dfn值等于low值,则它是该搜索子树的根。这时,它以上(包括它自己)一直到栈顶的所有元素组成一个强连通分量。

算法框架

Dfn(u)为节点u搜索的次序编号(时间戳)。

Low(u)我们用low值记录该点所在强连通子图对应的搜索子树的根节点的Dfn值,为u或者u的子树能够追溯到的最早的栈中的节点的次序号dfn。

1.数组的初始化:当首次搜索到点p时,Dfn与Low数组的值都为到该点的时间。

2.堆栈:每搜索到一个点,将它压入栈顶。

3.当点u有与点v相连时,如果此时(时间为dfn[u]时)v不在栈中,u的low值为两点的low值中较小的一个。(这是顺着树边向下找)

4.当点u有与点v相连时,如果此时(时间为dfn[u]时)v在栈中,u的low值为u的low值和v的dfn值中较小的一个。(出现环了,这是一条返祖边,一个联通分量可能有多个环)

5.每当搜索到一个点经过以上操作后(也就是子树已经全部遍历)的low值等于dfn值,则将它以及在它之上的元素弹出栈。这些出栈的元素组成一个强连通分量。

6.继续搜索(或许会更换搜索的起点,因为整个有向图可能分为两个不连通的部分),直到所有点被遍历。

伪代码

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) // 如果节点u还在栈内

        Low[u] = min(Low[u], DFN[v])

  if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根

  repeat v = S.pop  // 将v退栈,为该强连通分量中一个顶点

  print v

  until (u== v)

}

可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。N为点数,M为边数。

#include<cstdio>
#include<algorithm>
#include<string.h>
#include<iostream> 
using namespace std;
struct node {
	int v,next;
} edge[1001];
int DFN[1001],LOW[1001];
int stack[1001],heads[1001],visit[1001],cnt,tot,index;
void add(int x,int y) {
	edge[++cnt].next=heads[x];
	edge[cnt].v = y;
	heads[x]=cnt;
	return ;
}
void tarjan(int x) { //代表第几个点在处理。递归的是点。
	DFN[x]=LOW[x]=++tot;// 新进点的初始化。
	cout<<x<<":"<<DFN[x]<<" "<<LOW[x]<<endl;	
	stack[++index]=x;//进站
	visit[x]=1;//表示在栈里
	for(int i=heads[x]; i!=-1; i=edge[i].next) {
		if(!DFN[edge[i].v]) {//如果没访问过
		    cout<<"  in u-v:   "<<x<<"->"<<edge[i].v<<"  low"<<x<<"="<<LOW[x]<<" low"<<edge[i].v<<"="<<LOW[edge[i].v]<<endl;
			tarjan(edge[i].v);//往下进行延伸,开始递归
			cout<<"      out:  "<<x<<"->"<<edge[i].v<<"  low"<<x<<"="<<LOW[x]<<" ";
			LOW[x]=min(LOW[x],LOW[edge[i].v]);//递归出来,比较谁是谁的儿子/父亲,就是树的对应关系,涉及到强连通分量子树最小根的事情。
			cout<<"  low"<<edge[i].v<<"="<<LOW[edge[i].v]<<"=> low"<<x<<":"<<LOW[x]<<endl; 
		} 
		else if(visit[edge[i].v ]) { //出现了环:如果访问过,并且还在栈里。 
			cout<<"  else"<<edge[i].v<<":instack  "<<x<<"->"<<edge[i].v<<"  low"<<x<<"="<<LOW[x]<<" ";
			LOW[x]=min(LOW[x],DFN[edge[i].v]);//比较谁是谁的儿子/父亲。找出所有子树中最早出现的dfn 
			cout<<"  dfn"<<edge[i].v<<"="<<DFN[edge[i].v]<<"=>low"<<x<<"="<<LOW[x]<<endl;
		}
	}
	if(LOW[x]==DFN[x]) { //发现是整个强连通分量子树里的最小根。
	     cout<<"ssc:";
		do {
			printf("   %d dfn=%d, low=%d    \n",stack[index],DFN[stack[index]],LOW[stack[index]]);
			visit[stack[index]]=0;
			index--;
		} while(x!=stack[index+1]);//出栈,并且输出。
		printf("\n");
	}
	return ;
}
int main() {
	memset(heads,-1,sizeof(heads));
	int n,m;
	scanf("%d%d",&n,&m);
	int x,y;
	for(int i=1; i<=m; i++) {
		scanf("%d%d",&x,&y);
		add(x,y);
	}
	for(int i=1; i<=n; i++){
		cout<<i<<endl; 
			if(!DFN[i])  tarjan(i);//当这个点没有访问过,就从此点开始。防止图没走完
	}
	   
	
	return 0;
}

数据:及执行结果

6 7

1 2

2 3

3 5

3 4

4 1

5 6

5 4

1

1:1 1

  inu-v:   1->2  low1=1 low2=0

2:2 2

  inu-v:   2->3  low2=2 low3=0

3:3 3

  inu-v:   3->4  low3=3 low4=0

4:4 4

 else1:instack  4->1  low4=4  dfn1=1=>low4=1

     out:  3->4  low3=3  low4=1=> low3:1

  inu-v:   3->5  low3=1 low5=0

5:5 5

 else4:instack  5->4  low5=5  dfn4=4=>low5=4

  inu-v:   5->6  low5=4 low6=0

6:6 6

ssc:  6 dfn[]=6, low[]=6

 

     out:  5->6  low5=4  low6=6=> low5:4

     out:  3->5  low3=1  low5=4=> low3:1

      out: 2->3  low2=2   low3=1=> low2:1

     out:  1->2  low1=1  low2=1=> low1:1

ssc:  5 dfn[]=5, low[]=4

   4dfn[]=4, low[]=1

   3dfn[]=3, low[]=1

   2dfn[]=2, low[]=1

   1dfn[]=1, low[]=1

codevs1332 上白泽慧

Poj2186 Popular Cows


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值