Tarjan是一个对图的分析的强有力的算法,主要应用有:有向图的强连通分量、无向图的割点桥与双连通分量、LCA(最近公共祖先)
基本概念
下面主要介绍tarjan算法在强连通分量中的应用。
首先我们需要知道强连通是有向图特有的概念,如果一个有向图中任意两点之间都是相互可达的那么称这个图为强连通图。一个图的极大连通子图称为改图的强连通分量。
Tarjan算法求解强连通分量
通过Tarjan算法可以得到每个点属于哪个连通分量。
我们可以先初步把Tarjan算法看成是一个对图进行深搜并结合栈对节点进行处理的算法。
该算法涉及到三个值:
dfn[i]: dfn[i]表示图中的节点i在搜索过程中的(访问)次序号,是第几个访问到的,也叫做时间戳,每个节点的时间戳都是不一样的
low[i]: low[i]表示第i个节点的子树能够追溯到的最早的栈中节点的次序号
vis[i]: vis[i]值为1表示i在栈中,为0表示不在栈中。
我们先大致了解了tarjan算法是做什么的以及里面的符号约定,不太懂也没关系,现在我们换一种方式来观察这个算法到底是做什么的。
我们始终应该明确的一点是我们要求的是每个点属于哪个连通分量,其实上面的low[i]的值就表示的是点i和哪个点(这个点也被称作根,但不一定是极小的根)是属于同一个连通分量的。
因为tarjan算法本质上是一个DFS的过程,这个算法可以看成是在一颗“搜索树上进行”(这里说树也不太准确,但不妨碍我们理解)
我们以下图为例:
转化成搜索树的形式就是
可以看出整个搜索的过程是1->2->3->5,每搜到一个就进栈(这个栈不是指用于DFS的栈,而是另外开的一个栈,用来存放被搜到的节点中还没确定连通分量的节点)
搜到5之后继续搜发现又搜到了2,这个时候我们发现成环了。这个环上时间戳最小的是节点2,那么当前这个连通分量(环)的根就是dfn[2]。
回溯之后2、3、5节点的low值都为dfn[2]了,
然后继续从2搜,发现又搜到1,而1的时间戳更小,所以以2为根连通分量将合并到以1为根的连通分量。
回溯后继续从1搜搜到4,发现4没有子节点也就是(low[4]==dfn[4]),4就是根节点,这个连通分量只有一个节点,所以以节点4为集合的一个极大连通分量。
再回溯,发现从1出发也没有点可搜了,此时low[1]==dfn[1]说明此时1作为根节点的连通分量是个极大连通分量。
整个搜索就完成了。
通过这个例子我们可以总结出算法在搜索时的规则:
从点u出发
1.如果从u出发没有点可以搜了并且low[u]==dfn[u]:那么说明我们已经遍历了这个点所属的连通分量并且这个点就是该连通分量的根(我们的算法也保证了在搜索过程中该连通分量上的每个点的low值都更新为该连通分量的根),由于该连通分量已经确定了,所以我们可以把以u点为根的连通分量从图中删掉。
2.如果搜到的下一个节点v未被访问过(可以由dfn来判断):那么就把它进栈并从它出发进行深搜。
3.如果搜到的下一个点v被访问过并且在栈中(也就是vis[i]=1,那些被访问过但已经出栈的节点所在的连通分量已经确定了,所以可以忽略这些点):那么说明从v到u这条链上的点都属于一个连通分量(因为把(u,v)练下去就会成环),这个时候就应该更新每个节点的low值为这个连通分量中时间戳最小的点的low值,在回溯的过程中也不断将这个最小的low值传递。
这些规则就是tarjan算法的基本框架了,具体的算法细节请见代码。
模板
const int MAXN=105;
int n;
int DFN[MAXN];
int LOW[MAXN];
int vis[MAXN];
int belong[MAXN];//belong[i]表示i属于缩点后的哪个节点
int cnt;
int tot;
struct Edge
{
int v;
int next;
}edge[MAXN*MAXN];
int edgecount;
int head[MAXN];
void Init()
{
edgecount=0;
memset(head,-1,sizeof(head));
}
void Add_edge(int u,int v)
{
edge[++edgecount].v=v;
edge[edgecount].next=head[u];
head[u]=edgecount;
}
stack<int > St;
void Tarjan(int u)//从节点x开始搜索
{
DFN[u]=LOW[u]=++tot;
vis[u]=1;//为1表示在队列里面
St.push(u);
for(int k=head[u];k!=-1;k=edge[k].next)
{
int v=edge[k].v;
if(!DFN[v])//还未访问过
{
Tarjan(v);
LOW[u]=min(LOW[u],LOW[v]);
}
else if(vis[v])//被访问过,还在队列里
{
LOW[u]=min(LOW[u],DFN[v]);
}
}
if(LOW[u]==DFN[u])
{
int x;
++cnt;
while(1)
{
x=St.top();
St.pop();
vis[x]=0;
belong[x]=cnt;
if(x==u)break;
}
}
}
void Solve()
{
tot=0;
cnt=0;//缩点后的点数
memset(DFN,0,sizeof(DFN));
memset(LOW,0,sizeof(LOW));
memset(vis,0,sizeof(vis));
while(!St.empty()) St.pop();
for(int i=1;i<=n;i++)
{
if(DFN[i]==0)Tarjan(i);
}
}