Tarjan

Tarjan

割点 & 割边(桥)

先引入两个概念:时间戳追溯值

时间戳

在 $ dfs $ 遍历图时,每个节点第一次被访问的顺序,就是这个点的 时间戳,通常用 (dfn[u]) 表示;

追溯值

节点 $ u $ 的追溯值 定义为以下节点的 时间戳 的最小值;

1.$ subtree(u) $ 中的节点;

2.通过 1 条不在搜索树上的边,能够到达 $ subtree(u) $ 的节点;

通常用 $ low[u] $ 表示;

割边

无向连通图中,去掉一条边,图中的连通分量数增加,则这条边,称为桥或者割边,当然也是在无向图。

无向边 $ (x,y) $是桥, 当且仅当搜索树上存在 $ x $ 的一个节点 $ y $ ,满足: $ dfn[x] < low[y] $

需要注意的是,因为我们遍历的是无向图,所以从每个点 $ x $ 出发,总能访问到它的父节点 $ fa $ ,根据 $ low $ 的计算方法,$ (x,fa) $ 属于搜索树上的边

且 $ fa $ 不是 $ x $ 的子节点,所以不能用 $ fa $ 的时间戳来更新 $ low[x] $ ;

但是如果有重边的情况,那么 $ fa $ 与 $ x $ 之间不在搜索树上的边是可以更新 $ low[x] $ 的;

所以我们需要知道 $ fa $ 与 $ x $ 之间的边是不是搜索树上的边,那么我们可以利用成对变换技巧就可以了;

void tarjan(int u,int in){
	dfn[u]=low[u]=++tot;
	for(int i=head[u];i;i=e[i].nxt){
		int to=e[i].v;
		if(!dfn[to]){ //是否是子树上的点
			tarjan(to,i);
			low[u]=min(low[u],low[to]);
			if(dfn[u]<low[to]){
                //.......
			}
		}else if(i!=(in^1)){ //判断是否是搜索树上的边
			low[u]=min(low[u],dfn[to]);
		}
	}
}

割点

如果在一个图中,如果把一个点删除,那么这个图不再连通,那么这个点就是割点(割顶),当然是在无向图。

若 $ x $ 不是搜索树的根节点,则 $ x $ 是割点当且仅当搜索树上存在 $ x $ 的一个子节点 $ y $ ,满足:$ dfn[x] <= low[y] $

若 $ x $ 是搜索树的根节点,则 $ x $ 是割点当且仅当搜索树上存在至少两个子节点 $ y1,y2 $ ,满足以上条件;

可以发现父节点和重边的情况是不必考虑的 ,想想为什么

void tarjan(int u){
	dfn[u]=low[u]=++tot;
	int flag=0;
	for(int i=head[u];i;i=e[i].nxt){
		int to=e[i].v;
		if(!dfn[to]){
			tarjan(to);
			low[u]=min(low[u],low[to]);
			if(dfn[u]<=low[to]){
				flag++;
				if(u!=1||flag>1) cut[u]=1;// 根节点特殊的情况
			}
		}else low[u]=min(low[u],dfn[to]);
	}
}

P3388 【模板】割点(割顶)

P3225 [HNOI2012]矿场搭建

模板:割点和桥

边双连通分量

在一张连通的无向图中,对于两个点 $u $ 和 $ v $ ,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 (u)(v) 边双连通

边双连通的极大子图称为边双连通分量

任意一条边至少在一个简单环中

边双连通分量怎么求?

先求出那些边是桥,再 $ dfs $ 就可以了

#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+7;
const int M=3e5+7;
struct edge{
	int v,nxt;
}e[M<<2];
int n,m,tot,cnt=1,ans;
int head[N],dfn[N],low[N],bri[N],vis[10000000];
void add_edge(int u,int v){
	e[++cnt]=(edge){v,head[u]};
	head[u]=cnt;
}

void tarjan(int u,int in){
	dfn[u]=low[u]=++tot;
	for(int i=head[u];i;i=e[i].nxt){
		int to=e[i].v;
		if(!dfn[to]){
			tarjan(to,i);
			low[u]=min(low[u],low[to]);
			if(dfn[u]<low[to]){
				bri[i]=bri[i^1]=1;
			}
		}else if(i!=(in^1)){
			low[u]=min(low[u],dfn[to]);
		}
	}
}

void dfs(int u,int fa){
	vis[u]=fa;
	for(int i=head[u];i;i=e[i].nxt){
		int to=e[i].v;
		if(vis[to]||bri[i]) continue;
		dfs(to,fa);
	}
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		add_edge(x,y);
		add_edge(y,x); 
	}
	for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			dfs(i,++ans);
		}
	}
	cout<<ans; 
}

点双连通分量

点双连通分量是一个极大的子图,满足图中删去任何一个点都不会改变图的连通性。

也就是不存在割点

性质

1.点双连通分量中没有割点。

2.若干个点双连通分量以割点相连接。

3.原图中的每个割点可能属于多个点双连通分量,其它点只可能属于一个点双连通分量。所以出栈时不能全部出栈

4.任意两点至少存在两条边不重复路径.

求法

1.当一个节点第一次被访问时,将该点入栈。

2.当找到割点时,无论 $ u $ 是否为根,从栈中不断弹出节点,知道 $ to $ 节点被弹出;

3.弹出的节点与 $ u $ 构成一个 $ v - DCC $ 。

看代码

void tarjan(int u){
	dfn[u]=low[u]=++tot;
	if(u==rt&&head[u]==0){
		dcc[++cnt].push_back(u); //孤立点
		return;
	}
	st[++h]=u; //入栈
	for(int i=head[u];i;i=e[i].nxt){
		int to=e[i].v;
		if(!dfn[to]){
			tarjan(to);
			low[u]=min(low[u],low[to]);
			if(dfn[u]<=low[to]){ //是割点
				cnt++;
				int k;
				do{
					k=st[h];
					h--;
					dcc[cnt].push_back(k);
				}while(k!=to); // 注意这里是to,不是u,因为割点可能在多个 v-DCC 中
				dcc[cnt].push_back(u);
			}
		}else low[u]=min(low[u],dfn[to]);
	}
}

UVA1364 Knights of the Round Table

圆方树

在圆方树中,原来的每个点对应一个圆点,每一个点双对应一个方点
所以共有 $ n+c $ 个点,其中 $ n $ 是原图点数,$ c $ 是原图点双连通分量的个数。

而对于每一个点双连通分量,它对应的方点向这个点双连通分量中的每个点连边。
每个点双形成一个“菊花图”,多个“菊花图”通过原图中的割点连接在一起(因为点双的分隔点是割点)。

显然,圆方树中每条边连接一个圆点和一个方点。

圆方树的点数小于 $ 2n $,这是因为割点的数量小于 $ n $,所以请注意各种数组大小要开两倍。

其实,如果原图连通,则“圆方树”才是一棵树,如果原图有 $ k $ 个连通分量,则它的圆方树也会形成 $ k $ 棵树形成的森林。

就把图上问题转化成了树上问题,那怎么构建圆方树呢,重新建边就好了,见代码;

void tarjan(int u){
	dfn[u]=low[u]=++tot;
	st[++h]=u;
	num++;
	for(int i=head[u];i;i=e[i].nxt){
		int to=e[i].v;
		if(!dfn[to]){
			tarjan(to);
			low[u]=min(low[u],low[to]);
			if(dfn[u]<=low[to]){
				int k;
				++n;
				w[n]++;
				add(u,n); // 重新建边
				add(n,u);
				do{
					k=st[h];
					h--;
					w[n]++;
					add(k,n); 
					add(n,k); 
				}while(to!=k);
			}
		}else low[u]=min(low[u],dfn[to]);
	}
}

P4630 [APIO2018] Duathlon 铁人两项

P4606 [SDOI2018]战略游戏

有向图的强连通分量

有向图 G 强连通是指,G 中任意两个结点连通。强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图。

tarjan 学习网址

$ dfn $ : 在 $ dfs $ 遍历图时,每个节点第一次被访问的顺序,就是这个点的 时间戳

追溯值

节点 $ u $ 的追溯值 定义为以下节点的 时间戳 的最小值;

1.栈中的节点;

2.存在一条从子树出发的有向边,以改点为终点;

可以发现,$ dfn[u] $ 必然大于等于 $ low[u] $ ,当 $ dfn[u] = low[u] $ 时 说明这个节点是当前这个强联通量的最小的 $ dfn $ ,就可以弹栈了;

void tarjan(int now){
	dfn[now]=low[now]=++cnt;
	st[++h]=now;
	vis[now]=1; //判断是否在栈中
	for(int i=head[now];i;i=e[i].nxt){
		int to=e[i].v;
		if(!dfn[to]){
			tarjan(to);
			low[now]=min(low[now],low[to]);
		}else if(vis[to]){
			low[now]=min(low[now],dfn[to]);
		}
	}
	int k;
	if(dfn[now]==low[now]){
		++cur;
		do{
			k=st[h],h--;
			vis[k]=0;
			id[k]=cur,sum[cur]+=a[k];
		}while(now!=k);
	}
}

P3119 [USACO15JAN]Grass Cownoisseur G

P5008 [yLOI2018] 锦鲤抄

原文地址:https://www.cnblogs.com/Aswert/p/14273854.html