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]);
    }
}
posted @ 2025-04-03 18:59  all_for_god  阅读(41)  评论(0)    收藏  举报