常用两种算法: tarjan和korasaju算法。
学习资料:
https://www.byvoid.com/blog/scc-tarjan/
https://zh.wikipedia.org/wiki/Tarjan%E7%AE%97%E6%B3%95
挑战P320
定义:
如果一个有向图S,在图中任取两个点u,v,都存在一条u到v的路径,那么称这个图是强连通的。而有向图的一个强连通分量就是该图的一个极大强连通子图。任意的有向图都可以分解成若干个不相交的强连通分量,就是强连通分量的分解。将分解后的强连通分量缩成一个点,就可以得到一个DAG(有向无环图)
korasaju算法:
通过简单的两次dfs实现,一次对原图进行dfs,一次对反向图进行dfs。
首先选取任意节点作为起点,对原图进行dfs,遍历所有没有访问过的节点,并且在回溯前对顶点进行标号,(就是后序遍历)。对剩下的没有访问过的节点不断重复这个过程(因为这个有向图也有可能本身是不连通的)
完成第一次dfs后,可以知道越靠近图的尾部(也就是搜索树的叶子),顶点的编号就越小,这是由于后序遍历的特性。
进行第二次dfs,对反向图进行dfs,这时候选择标号最大的顶点作为起点进行dfs,这样dfs所遍历的顶点的集合就构成了一个强连通分量。同样,对于还有没有访问过的节点,再次将此时编号最大的顶点不断重复上述过程。
可以简单的理解这样做的理由。从编号大的开始进行搜索,每一个节点都属于一个强连通分量,将边反向过后,就不可以沿着边访问到这个强连通分量之外的顶点了,而对于强连通分量的其他顶点可达性不会受到影响。
void add_edge(int u,int v)
{
edge[u].push_back(v);
redge[v].push_back(u);
}
void dfs(int u)
{
used[u] = true;
for(int i = 0;i < edge[u].size();i++)
{
int v = edge[u][i];
if(!used[v]) dfs(v);
}
vs.push_back(u);
}
void rdfs(int u,int k)
{
used[u] = true;
topo[u] = k;
for(int i = 0;i < redge[u].size();i++)
{
int v = redge[u][i];
if(!used[v]) rdfs(v,k);
}
}
int scc()
{
for(int i = 1;i <= n;i++)
{
if(!used[i]) dfs(i);
}
memset(used,false,sizeof(used));
int k = 0;
for(int i = vs.size() - 1;i >= 0;i--)
{
int u = vs[i];
if(!used[u]) rdfs(u,k++);
}
return k;
}
tarjan算法:
tarjan算法也是基于dfs的,但是只需要对原图进行一次dfs就可以了。每个强连通分量都是dfs搜索树中的一颗子树。并且在dfs的时候,把当前搜索树中的还没有处理过的节点压入一个栈中,在回溯的时候就可以判断栈顶的节点到栈中的节点是否存在一个强连通分量。
定义DFN(u)为节点u搜索的次序标号,Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序标号。
Low(u)=Min
{
DFN(u),
Low(v),(u,v)为树枝边,u为v的父节点(即v没有被访问过,不在栈中)
DFN(v),(u,v)为指向栈中节点的后向边(非横叉边)
}
伪代码:
这里引入一个强连通分量的根,只针对于这个算法,表示这个强连通分量中最早被访问到的节点。
当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。