tarjan(强连通分量、边双、点双)
强连通分量
注意到一个强连通分量中 dfn 序最小的点一定有 \(dfn_u=low_u\)。因此条件成立时就将自己和栈上方的点全部压入一个强连通分量中。
而如果枚举的 \(v\) 有 \(dfn\) 序,但其 \(vis\) 已经不为 1 了,也就是说 \(v\) 已经不在栈里面了,那其已经被弹出栈了,就不用管它了。
void tar(int u){
low[u]=dfn[u]=++dfncnt;vis[u]=1,stk[++top]=u;
for(int i=0;i<tot[u];i++){
int v=q[u][i];
if(!dfn[v]){
tar(v);low[u]=min(low[u],low[v]);
}
else if(vis[v]) low[u]=min(dfn[v],low[u]);
}
if(dfn[u]==low[u]){
++blockcnt;
do{scc[blockcnt].push_back(stk[top]);vis[stk[top]]=0;}while(stk[top--]!=u);
}
}
边双
可以发现求边双与求强连通分量是本质相同的。只是边双由于是无向图需要考虑反边。
void tar(int u,int fa){
low[u]=dfn[u]=++dfncnt;vis[u]=1,stk[++top]=u;
for(int i=0;i<tot[u];i++){
int v=q[u][i];if(v==fa) continue;
if(!dfn[v]){
tar(v,u);low[u]=min(low[u],low[v]);
}
else if(vis[v]) low[u]=min(dfn[v],low[u]);
}
if(dfn[u]==low[u]){
++blockcnt;
do{scc[blockcnt].push_back(stk[top]);vis[stk[top]]=0;}while(stk[top--]!=u);
}
}
点双
求点双需要注意根节点的问题。还有因为某一个点很有可能是多个点双间的割点,因此需要在循环里判定,同时注意不要把割点给弹出栈。
同时特别注意在弹出节点的时候判断的是栈顶是否是 \(v\),因为判断是否是 \(u\) 的话可能将其他点双中的点一并删掉,也就是说 \(u\) 在栈中的位置不一定恰好在 \(v\) 的下方。具体的,可能对于一个点 \(u\),其儿子 \(v1\) 中有一条连向 \(u\) 祖先的返祖边,因此 \(v1\) 子树可能还没有被清空。因此当遍历 \(v2\) 子树的时候就需要去判断 \(v2\) 是否在栈顶而非 \(u\)。
void tar(int u){
low[u]=dfn[u]=++dfncnt;stk[++top]=u;int tmp=0;
if(u==rt&&tot[u]==0){scc[++blockcnt].push_back(u);return;}
for(int i=0;i<tot[u];i++){
int v=q[u][i];
if(!dfn[v]){
tar(v);low[u]=min(low[u],low[v]);
if(low[v]==dfn[u]){
blockcnt++;
do{scc[blockcnt].push_back(stk[top--])}while(stk[top+1]!=v);scc[blockcnt].push_back(u);
//注意到是判断是否是 v,因为有可能会将其他点双中的点删掉
}
}
else low[u]=min(low[u],dfn[v]);
}
}

浙公网安备 33010602011771号