思路
首先建成DFS树,对于每个节点,储存两个信息:dfn与low。
每当遍历到一个节点,就将其压入栈中,作用后面会说。
dfn储存一个节点的dfs序,low储存一个节点的子树能够到达的在栈中的最小dfs序。
首先有一个性质,每个节点子树中所有节点的dfn必然大于当前节点的。
那么,假如一个节点的low小于当前节点dfn,就说明它的子树中存在边通向当前节点的上级,当前分量就不是极大强连通分量,因为它可以与它的上级节点构成一个环,那么当前子树中的强连通分量就可以与上级节点合并。
同理,若一个节点的low等于当前节点dfn,那么就说明当前节点的子树中最长存在一个通向当前节点的环,就是一个极大强连通分量,不可能有其它节点加入了。
会不会存在一个节点的子树中存在有一块节点是单独的极大强连通分量呢?存在的,所以我们需要在dfs的同时进行操作,这样,当遍历到一个极大强连通分量子树时,就先把它标记为强连通分量,并且它的dfn就是它本身,绝对大于其父节点,不会对其父节点产生任何影响。
所以,在找到一段强连通分量,就要把它删除,防止加入其父节点的强连通分量之中。
感性想象一下,当前节点的子树中,不与当前节点连通的节点全都删除了,剩下的节点,都可以通过通向当前节点的返祖边回到当前节点,就产生了一个个环,就是一个极大强连通分量了。
在栈中,当前节点的子树全都压在当前节点上面,直接一个个弹出并标记就完事了。
细节
当前节点通向的节点有三种情况:
- 没有被遍历过。那么就把这个节点作为自己的子节点,dfs后用其low更新当前节点的low。因为当前节点存在一条通向这个节点的路径,所以这个节点能通向的节点当前节点也可以前往。\(寇可往我亦可往\)
- 被遍历过,并且还在栈中。那么,还是有两种情况:
- 返祖边,是当前节点的祖先,用它的dfn来更新当前节点的low。更具体的,这就产生了一个环,包括了从当前点到祖先点的整条路径,都属于同一个强连通分量。
- 横叉边,因为它还在栈中,就说明这个节点必然存在在一个环中,有路径通向两颗子树的公共祖先节点,那么也就形成了一个环。按理来说,我们应该用它的low来更新这个节点的low,但因为通向的节点被先遍历过,通向的节点dfn必然大于这个节点所在子树的所有节点dfn,因此直接用其dfn来更新当前节点dfn也是一样的效果,都比当前子树所有节点dfn小。
综上,就用其dfn来更新当前节点dfn即可。
- 被遍历过,并且没在栈中。那么,那个节点就已经完成了。既然当前节点并没有成为它的子节点,就说明那个点没有通向这个点的边。\(单向奔赴是没有结果的\),所以不用处理,二者必然不属于同一个强连通分量,因为无法从对方通向这里。
模板
int scnt;//强连通编号
int scc[N],siz[N];//储存一个节点属于的强连通分量、强连通分量的大小
int dcnt;//dfn编号
int dfn[N],low[N];//见思路
bool ins[N];//是否在栈中
stack<int> stk;
void tarjan(int rt){
low[rt]=dfn[rt]=++dcnt;
stk.push(rt);ins[rt]=true;
for(int i=hd[rt];i;i=ne[i]){
if(!dfn[to[i]]){
tarjan(to[i]);
low[rt]=min(low[rt],low[to[i]]);
}
else if(ins[to[i]])low[rt]=min(low[rt],dfn[to[i]]);
}
if(dfn[rt]==low[rt]){
scnt++;
while(stk.top()!=rt){
int tp=stk.top();stk.pop();
ins[tp]=false;
scc[tp]=scnt;
siz[scnt]++;
}
stk.pop();
ins[rt]=false;
scc[rt]=scnt;
siz[scnt]++;
}
}
标签:rt,Tarjan,连通,dfn,low,当前,节点,求强
From: https://www.cnblogs.com/HarlemBlog/p/18467103