Tarjan算法求解图的强连通分量

基础知识

一个有向图的强连通分量是这个有向图的一个子图,在这个子图内,任意两结点相互可达,且不存在子图外的某结点和子图中的某结点相互可达。

Tarjan \text{Tarjan} Tarjan 算法是为有向图划分强连通分量的算法,它能在 O ( V + E ) O(V+E) O(V+E) 时间内完成划分。

算法思想

该算法基于一个事实:当我们按照 dfs \text{dfs} dfs 序遍历一个图并打上时间戳,若能从一个时间戳靠后的结点到达时间戳靠前的结点,则这两个结点相互可达,不仅如此,通过这两个结点的回路上任意两个结点都相互可达。

因此,算法使用数组 dfn [ u ] \text{dfn}[u] dfn[u] 保存结点 u u u 的时间戳,使用数组 low [ u ] \text{low}[u] low[u] 保存结点 u u u 可以到达的结点中最小的时间戳,用栈保存访问的结点,并用数组 vis [ u ] \text{vis}[u] vis[u] 表示结点 u u u 是否在栈中。

初始时, dfn [ ] \text{dfn}[] dfn[] low [ ] \text{low}[] low[] 都没有意义,因此被初始化为 − 1 -1 1 vis [ ] \text{vis}[] vis[] 被初始化为 false \text{false} false ,表示所有结点都没有被访问。算法运行时,按照 dfs \text{dfs} dfs 的顺序访问结点,访问到结点 u u u 时,设置 dfn [ u ] = low [ u ] = idx \text{dfn}[u]=\text{low}[u]=\text{idx} dfn[u]=low[u]=idx ,这里 idx \text{idx} idx 是一个全局变量,表示当前访问的时间,同时,把结点 u u u 压栈, vis [ u ] \text{vis}[u] vis[u] 设为 true \text{true} true 。在向后延伸到结点 v v v 的时候,若结点 v v v 未被访问,则用 min ⁡ ( low [ v ] , low [ u ] ) \min(\text{low}[v],\text{low}[u]) min(low[v],low[u]) 更新 low [ u ] \text{low}[u] low[u] ,维护 low \text{low} low 最小的性质;如果不是第一次访问 v v v ,则用 min ⁡ ( dfn [ v ] , low [ u ] ) \min(\text{dfn}[v],\text{low}[u]) min(dfn[v],low[u]) 更新 low [ u ] \text{low}[u] low[u] 。当访问完毕结点 u u u 时,需要检验是否有 dfn [ u ] = low [ u ] \text{dfn}[u]=\text{low}[u] dfn[u]=low[u] ,若是,则表示结点 u u u 无法到达它前面的结点,因此它是某个强连通分量的“根”。此时,栈中所有在 u u u 上方的结点都处于结点 u u u 的强连通分量中,一个个弹栈直到把 u u u 自己也弹出即可。

代码实现

以下代码用 scc \text{scc} scc 对有向图的强连通分量标号,从 0 0 0 开始; dfn \text{dfn} dfn low \text{low} low idx \text{idx} idx 也都从 0 0 0 开始。 belong [ u ] \text{belong}[u] belong[u] 数组存储结点 u u u 所在的强连通分量。

const int MAXN=1e4+10;
const int MAXM=1e5+10;
struct Edge{
    int to,next;
}edge[MAXM];
int head[MAXN],tot;
void init(){
    tot=0;
    memset(head,-1,sizeof(head));
}
void addedge(int u,int v){
    edge[tot]=(Edge){v,head[u]};
    head[u]=tot++;
}
int low[MAXN],dfn[MAXN],belong[MAXN];
int idx,scc;
int Stack[MAXN],top;
bool vis[MAXN];
void Tarjan(int u){
    low[u]=dfn[u]=idx++;
    Stack[top++]=u;
    vis[u]=true;
    for (int i=head[u];~i;i=edge[i].next){
        int v=edge[i].to;
        if (dfn[v]==-1){
            Tarjan(v);
            if (low[v]<low[u])
                low[u]=low[v];
        }
        else if (vis[v]&&dfn[v]<low[u])
            low[u]=dfn[v];
    }
    if (low[u]==dfn[u]){
        int v;
        do{
            v=Stack[--top];
            vis[v]=false;
            belong[v]=scc;
        }while (v!=u);
        ++scc;
    }
}
void solve(int n){
    memset(dfn,-1,sizeof(dfn));
    memset(vis,false,sizeof(vis));
    idx=scc=top=0;
    for (int i=1;i<=n;++i)
        if(dfn[i]==-1) Tarjan(i);
}

总结

不得不说, Tarjan \text{Tarjan} Tarjan 真的是把 dfs \text{dfs} dfs 运用到了极致,不管是求强连通分量,还是求最近公共祖先,都是一遍 dfs \text{dfs} dfs ,直接达到了理论上的复杂度下界。这也从一方面说明了图论中基础算法的重要性。

你可能感兴趣的:(算法笔记,ACM,图论)