tarjan2
1tarjan
可以用来求割点,缩点与桥
2.无向图
2.1一些定义
-
割点与桥
给定无向图\(G=(V,E)\)
若对于\(x\in V\),从图中删去节点x以及所有与x相关联的边后,G分裂成两个及以上不相连的子图,则称x为G的割点。
若对于\(e\in E\),从图中删去边e后,G分裂成两个不相邻的子图,则称e为G的桥或割边。
一般无向图的割点与桥就是其连通块的割点与桥。
tarjan算法基于无向图的深度优先遍历。
-
时间戳:在图的深度优先遍历过程中,按照每一个节点的一次被访问的时间顺序,依次给予n个节点1到n的整数标记,该标记就被称为时间戳,记为$ dfn_x$
-
搜索树,在无向连通图中任选一个节点出发进行深度优先遍历,每个点只访问一次,所有发生递归的边构成一颗树,记为搜索树。
-
追溯值,设\(sub_x\)表示搜索树中以x为根的子树,\(low_x\) 定义为以下节点时间戳的最小值:
- \(sub_x\)中的节点
- 通过一条不在搜索树上的边,能够到达 \(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[to]);
if(low[to]>dfn[k]) bridge[i]=bridge[i^1]=1;
}
else if(i!=(in_edge^1)) low[k]=Min(low[k],dfn[to]);
}
}
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”,
无向图的极大点双联通子图为点双联通分量。
无向图的极大边双联通子图被称为边双联通分量。
若无向图不存在割点,则称它为点双联通图。
若无向图不存在桥,则称它为边双联通图。
一张无向图是点双联通图,当且仅当满足下面两个条件之一:
- 图的顶点数不超过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算法的同时维护一个栈,并维护栈中元素,方法如下:
- 当一个节点第一次被访问时,入栈。
- 当割点判定法则中的条件\(dfn_x\le low_y\)成立时,无论x是否为根,都要:
- 从栈顶不断弹出节点,直到y被弹出,
- 刚才弹出的所有节点与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) cut[k]=1;
cnt++;
int z;
do{
z=stack[top--];
dcc[cnt].push_bask(z);
}while(z!=to);
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(cut[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为流图中的源点。
流图搜索树,时间戳的概念与无向图类似。
对于一张有向图,如果任意两点能够相互到达,则称该有向图为强连通图。
追溯值:
- 设\(sub_x\)表示流图的搜索树中以x为根的子树,x的追溯值\(low_x\)定义为满足以下条件的节点的最小时间戳:
- 该点在栈中,
- 存在一条从\(sub_x\)出发的有向边,以该点为终点。
3.1强联通分量(SCC)判断法则
在追溯值得计算过程中,若从x回溯前,有\(low_x=dfn_x\)成立,则栈中从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一些小的性质
- 边双联通是一种等价关系,称两个点边双联通当且仅当从一个点出发有两条边不相交的路径到达另一个点。
- 对边双联通分量中的边进行定向,可以得到一个强联通分量。
- 在做强联通分量 tarjan 结束后,得到一张 dag,注意这张图的拓扑序为 scc,scc-1 一直到 1,其中 scc 为强连通分量个数,也就是说,我们做 tarjan 的过程中取到强联通分量的顺序,正好是拓扑序反过来。
5引用
- 《算法竞赛进阶指南》