tarjan

1tarjan

可以用来求割点,缩点与桥

2.无向图

2.1一些定义

  1. 割点与桥

    给定无向图\(G=(V,E)\)

    若对于\(x\in V\),从图中删去节点x以及所有与x相关联的边后,G分裂成两个及以上不相连的子图,则称x为G的割点。

    若对于\(e\in E\),从图中删去边e后,G分裂成两个不相邻的子图,则称e为G的桥或割边。

    一般无向图的割点与桥就是其连通块的割点与桥。

    tarjan算法基于无向图的深度优先遍历。

  2. 时间戳:在图的深度优先遍历过程中,按照每一个节点的一次被访问的时间顺序,依次给予n个节点1到n的整数标记,该标记就被称为时间戳,记为\(dfn_x\)

  3. 搜索树,在无向连通图中任选一个节点出发进行深度优先遍历,每个点只访问一次,所有发生递归的边构成一颗树,记为搜索树。

  4. 追溯值,设\(sub_x\)表示搜索树中以x为根的子树,\(low_x\)定义为以下节点时间戳的最小值:

    1. \(sub_x\)中的节点
    2. 通过一条不在搜索树上的边,能够到达\(sub_x\)的节点。

2.2割边判定法则

无向图\((x,y)\)是桥,当且仅当搜索树上存在x的一个子节点y,满足:\(dfs_x<low_y\)

容易发现,桥一定是搜索树中的边,并且一个简单环中的边一定都不是桥。

注:注意处理父节点和重边。

代码:

inline void tarjan(int k,int in_edge){
	dfn[k]=low[k]=++num;
	for(int x=head[k];x;x=li[x].next){
		int to=li[x].to;
		if(!dfn[to]){
			tarjan(to,x);
			low[k]=Min(low[k],low[y]);
			if(low[y]>dfn[k]) bridge[i]=bridge[i^1]=1;
		}
		else if(i!=(in_edge^1)) low[k]=Min(low[k],dfn[y]);
	}
}

2.3割点判定法则

若x不是搜索树的根节点,则x是割点当且仅当搜索树上存在x的一个子节点y,满足:\(dfs_x\leq low_y\)

特别的,若x是搜索树的根节点,则x是割点当且仅当搜索树上存在至少两个子节点\(y_1,y_2\)满足上述条件。

因为是小于等于号,所以不用考虑父节点和重边的问题。

代码:

inline void tarjan(int k){
	dfn[k]=low[k]=++num;
	int flag=0;
	for(int x=head[k];x;x=li[x].next){
		int to=li[x].to;
		if(!dfn[to]){
			tarjan(y);
			low[k]=Min(low[k],low[y]);
			if(low[y]>=dfn[k]){
				flag++;
				if(k!=root||flag>1) cut[k]=1;
			}
		}
		else low[k]=Min(low[k],dfn[to]);
	}
}

2.4无向图的双联通分量

无向图的双联通分量包括点双联通分量和边双联通分量,分别简称为“v-DCC”和“e-DCC”,

无向图的极大点双联通子图为点双联通分量。

无向图的极大边双联通子图被称为边双联通分量。

若无向图不存在割点,则称它为点双联通图。

若无向图不存在桥,则称它为边双联通图。

一张无向图是点双联通图,当且仅当满足下面两个条件之一:

  1. 图的顶点数不超过2
  2. 图中任意两点都同时包含在至少一个简单环中。其中简单环指的是不自交的环。

一张无向图是 边双联通图,当且仅当任意一条边都包含在至少一个简单环中。

2.4.1e-DCC求法

先用tarjan算法标记所有的桥边,然后在对整个图执行深度优先遍历,划分出每一个连通块

inline void dfs(int x){
	c[x]=dcc;
	for(int x=head[k];x;x=li[x].next){
		int to=li[x].to;
		if(c[to]||bridge[x]) continue;
		dfs(to);
	}
}

2.4.2e-DCC缩点

把每个e-DCC看做一个节点,桥边看做无向边,整个无向图变成一棵树,可以在建一张图,构建这颗树。

for(int i=2;i<=tail;i++){
	int x=li[i].to,y=li[i^1].to;
	if(c[x]==c[y]) continue;
	add(c[x],c[y]);
}

2.4.3v-DCC求法

需要在tarjan算法的同时维护一个栈,并维护栈中元素,方法如下:

  1. 当一个节点第一次被访问时,入栈。
  2. 当割点判定法则中的条件\(dfn_x\le low_y\)成立时,无论x是否为根,都要:
    1. 从栈顶不断弹出节点,直到y被弹出,
    2. 刚才弹出的所有节点与x一起构成v-DCC

注:不要忘记孤立点的特判

inline void tarjan(int k){
	dfn[k]=low[k]=num;
	stack[++top]=k;
	if(k==root&&head[k]==0){
		dcc[++cnt].push_back(x);
		return;
	}
	int flag=0;
	for(int x=head[k];x;x=li[x].next){
		int to=li[x].to;
		if(!dfn(to)){
			tarjan(to);
			low[k]=Min(low[k],low[to]);
			if(low[to]>=dfn[k]){
				flag++;
				if(k!=root||flag>1) cnt[k]=1;
				cnt++;
				int z;
				do{
					z=stack[top--];
					dcc[cnt].push_bask(z);
				}while(z!=y);
				dcc[cnt].push_back(x);
			}
		}
		else low[x]=Min(low[x],dfn[y]);
	}
}

2.4.4v-DCC缩点

较为麻烦,因为一个割点可能属于多个v-DCC,

设图*有p个割点,t个v-DCC,我们建立一张包含p+t个节点的图。把每个v-DCC和每个割点都作为新图中的节点,并在每个割点个它所有的v-DCC连边,这张新图实际上是一棵树(或森林,上面同理)

num=cnt;
for(int i=1;i<=n;i++)
	if(cnt[i]) new_id[i]=++num;
tc=1;
for(int i=1;i<=cnt;i++){
	for(int j=0;j<dcc[i].size();j++){
		int x=dcc[i][j];
		if(cut[x]){
			add_c(i,new_id[x]);
			add_c(new_id[x],i);
		}
		else c[x]=i;
	}
}

3有向图

给定有向图\(G=(V,E)\),若存在\(r\in V\),满足从r出发能够到达V中所有的点,则称G是一个流图。记为\((G,r)\),其中r为流图中的源点。

流图搜索树,时间戳的概念与无向图类似。

对于一张有向图,如果任意两点能够相互到达,则称该有向图为强连通图。

追溯值:

  1. 设\(sub_x\)表示流图的搜索树中以x为根的子树,x的追溯值\(low_x\)定义为满足以下条件的节点的最小时间戳:
    1. 该点在栈中,
    2. 存在一条从\(sub_x\)出发的有向边,以该店为中点。

3.1强联通分量(SCC)判断法则

在追溯值得计算过程中,若从x回溯前,有\(low_x=dfn_y\)成立,则栈中从x到栈顶的所有节点构成一个强联通分量。

inline void tarjan(int k){
	dfn[k]=low[k]=++num;
	stack[++top]=k;ins[k]=1;
	for(int x=head[k];x;x=li[x].next){
		int to=li[x].to;
		if(!dfn[to]){
			tarjan(to);
			low[k]=Min(low[k],low[to])
		}
		else if(ins[to])
			low[k]=Min(low[k],dfn[to]);
		if(dfn[k]==low[k]){
			cnt++;int y;
			do{
				y=stack[top--];ins[y]=0;
				c[y]=cnt;scc[cnt].push_back(y);
			}while(k!=y);
		}
	}
}

缩点完后变成DAG

for(int x=1;x<=n;x++){
	for(int i=head[x];i;i=li[x].next){
		int to=li[x].to;
		if(c[x]==c[to]) continue;
		add_c(c[x],c[to]);
	}
}

4引用

  1. 《算法竞赛进阶指南》
上一篇:全网最最详细!一文讲懂Tarjan算法&缩点


下一篇:Tarjan离线求lca