Tarjan算法求强连通分量
前置知识
有向图:一个只由有向边构成的图,Tarjan算法只适用于有向图。
强连通:
对于两个点 ,如果他们之间可以相互到达,那么就称点 强联通。
对于一个图 ,如果其任意两个顶点都是强联通的,那么这个图就是一个强联通图。
对于一个非强联通图 ,如果其某一子图 为强联通图,那么 就被称为图 的强连通分量。
算法实现
先来看一些定义(下图摘自oi-wiki):
右图叫做左图的dfs生成树。
而有向图的dfs生成树有 种边:
树边(tree edge):绿色边,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
反祖边(back edge):黄色边,也被叫做回边,即指向祖先结点的边。
横叉边(cross edge):红色边,它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先时形成的。
前向边(forward edge):蓝色边,它是在搜索的时候遇到子树中的结点的时候形成的。
下面给出一个详细解释:
树边
在搜索未被访问过的点时经过的边,即在下面的代码中,如果我们搜到了点 ,且对于一条边 , 点未被访问过,那么边 即为一条树边。
所有的树边构成一棵搜索树。
void dfs(int u){
for(int e=first[u];e;e=nxt[e]){
int v=to[e];
if(!vis[v]) dfs(v);
}
}
反祖边
由于递归本质上是在一个栈中进行的,我们搜索一个点时将其压入栈,结束时将其弹出。如果我们搜到了点 ,且对于一条边 , 点还在搜索栈中,那么边 即为一条反祖边。
更通俗一点来说,对于在一次dfs中搜到的边 来说,如果对于 的一条出边 , 中的一个,那么该边即为一条反祖边。
横叉边
如果我们搜到了点 ,且对于一条边 , 点已被访问过但不在搜索栈中,那么边 即为一条横叉边。
更通俗一点来说,对于在一次dfs中搜到的边 来说,如果对于 的一条出边 , 中的一个,那么该边即为一条横叉边。
前向边
当我们递归完一个结点 的一条边指向的点 的一个可以到达的点的集合(即一棵搜索树)时,如果 的另外一条边直接指向该集合中的点,那么该边被称为一条前向边。
找到强连通分量的方法
首先有一个性质:我们假设 是某一个强连通分量在搜索树中第一个访问到的结点,那么强连通分量一定存在于以 为根的子树中。
证明:我们假设某一点 在该强连通分量中但不在以 为根的子树里,那么由于 连通,那么从 到 的路径里一定存在一条不在子树里的边。根据上文的定义,该边一定为一条横叉边或返祖边,那么 已经被访问过,与 是第一个访问到的结点矛盾,故上述结论成立。
tarjan算法
定义两个数组: 。
表示点 被搜索到时的时间戳。
表示点 通过一些边能够到达的搜索栈里的最早的时间戳。
其中 是在一开始就已确定,不再改变吗,那么下面我们着重讨论如何更新 。
对于一个点 ,我们考虑它自己的另外没有被搜索过的边,如果 被搜索过且 小于 ,那么 的访问时间一定比 早,更新
然后考虑 子树中的结点 ,如果 可以到达一个点,且该点的 小于 ,且在搜索栈里,那么该点必定在 之前被搜索到并且 可以通过 来到达该点。
如果点 不在搜索栈里,那么 所在的强连通分量一定被处理过,所以我们不考虑。
我们来看这样一张图,箭头代表了每个点的
值(边的方向为
)
该图中只有一个点 ,也就是 号点。由于上面的点能够到达任意一个下面的点,所以如果下面的点 可以到达上面的点,那么这些点可以构成一个强连通分量。
当一个点的 ,那么它必定无法回到上面的点,也就是它就是一个强连通分量的顶点。
Code HAOI2006受欢迎的牛
#include<bits/stdc++.h>
using namespace std;
int Read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+ch-'0';
ch=getchar();
}
return x*f;
}
int low[100005],dfn[100005],vis[100005],ind=0;
int first[200005],nxt[200005],to[200005],tot=0;
int col[100005],sz[100005],cnt=0,n,m,cd[100005];
int X[200005],Y[200005];
void Add(int x,int y){
nxt[++tot]=first[x];
first[x]=tot;
to[tot]=y;
}
stack<int> s;
void tarjan(int u){
s.push(u);
vis[u]=1;
dfn[u]=low[u]=++ind;
for(int e=first[u];e;e=nxt[e]){
int v=to[e];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
cnt++;
int x;
do{
x=s.top();
s.pop();
col[x]=cnt;
sz[cnt]++;
vis[x]=0;
}while(x!=u);
}
}
int main(){
n=Read(),m=Read();
for(int i=1;i<=m;i++){
X[i]=Read(),Y[i]=Read();
Add(X[i],Y[i]);
}
for(int i=1;i<=n;i++){
if(!dfn[i]) tarjan(i);
}
for(int i=1;i<=m;i++){
if(col[X[i]]!=col[Y[i]]){
cd[col[X[i]]]++;
}
}
int ans=0,ff=0;
for(int i=1;i<=cnt;i++){
if(cd[i]==0) ff++,ans+=sz[i];
}
if(ff>1) cout<<0<<endl;
else cout<<ans<<endl;
}
割点与割边
割点
定义:在无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通,那么这个点就叫做割点。
还是上面这张图。
我们想象如果一个点的下面的点的 值大于等于该点的 ,那么该点下面的点只能到达在该点之后搜索的点,即以该点为根节点的子树。这就意味着如果删掉那个点,这个图将不再连通。
所以我们求割点是只需要判断low[v]>=dfn[u]
,然后标记该点即可。
注意特判根节点是否有多于一个孩子,如果是,那根节点也是割点。
#include<bits/stdc++.h>
using namespace std;
inline int Read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+ch-'0';
ch=getchar();
}
return x*f;
}
inline void Write(int x){
if(x<0){
putchar('-');
x=-x;
}
if(x>9){
Write(x/10);
}
putchar(x%10+'0');
}
int first[200005],nxt[200005],to[200005],tot=0;
int dfn[200005],low[200005],ind=0,cut[200005];
inline void Add(int x,int y){
nxt[++tot]=first[x];
first[x]=tot;
to[tot]=y;
}
inline void tarjan(int u,int rt){
int child=0;
dfn[u]=low[u]=++ind;
for(int e=first[u];e;e=nxt[e]){
int v=to[e];
if(!dfn[v]){
tarjan(v,rt);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]&&u!=rt) cut[u]=1;
if(u==rt) child++;
}
low[u]=min(low[u],dfn[v]);
}
if(u==rt&&child>=2) cut[rt]=true;
}
int main(){
int n,m,ans=0;
n=Read(),m=Read();
for(int i=1;i<=m;i++){
int x=Read(),y=Read();
Add(x,y);
Add(y,x);
}
for(int i=1;i<=n;i++){
if(!dfn[i]) tarjan(i,i);
}
for(int i=1;i<=n;i++){
if(cut[i]) ans++;
}
cout<<ans<<endl;
for(int i=1;i<=n;i++){
if(cut[i]) cout<<i<<" ";
}
}
割边
定义大致同割点,一条边为割边的条件为割掉该边后使原图不再连通。
继续使用刚才的方法进行理解。
如果需要割掉一条边使得图不再连通,那么该边的另一个结点必定无法通过其他边来到达上面的结点,也就是以
为根的子树里的点只能到达比
更后面的结点,即low[v]>dfn[u]
,对割点程序稍加改动即可。
割点和割边对有向图和无向图均适用
双连通分量
强连通分量是关于有向图的,那么对于无向图有没有上述性质呢?
在一张连通的无向图中,对于两个点 和 ,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 和 边双连通 。
在一张连通的无向图中,对于两个点 和 ,如果无论删去哪个点(只能删去一个,且不能删 和 自己)都不能使它们不连通,我们就说 和 点双连通 。
双连通分量的定义类似于强连通分量的定义。
求出双连通分量
方法很简单。
割点是点双连通分量的交点。
割边连接两个边双连通分量。
求出割点和割边,然后进行缩点即可。
几个重要性质
一个割点同时属于多个点双连通分量。
边双连通分量和割边共同组成一棵树。