【复习笔记】tarjan算法
写点东西好复习,主要是tarjan这个东西学了容易忘,忘了也不难捡起来,但捡起来了又容易忘。
tarjan的前置知识dfs树就暂且咕咕了,因为这东西没什么模板,变化挺多的,估计是写不完。
强连通分量
在有向图 \(G\) 中,如果两个顶点间至少存在一条路径,称两个顶点强连通。
如果有向图 \(G\) 的每两个顶点都强连通,称 \(G\) 是一个强连通图。
非强连通图有向图的极大强连通子图,称为强连通分量。
不严谨一些,强连通分量就是一个包含的节点能相互到达的子图。
\(Tarjan\) 算法是基于对图深度优先搜索的算法。它维护了一个栈,搜索时,把与当前点相连的未访问的节点入栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
该算法引入了两个很重要的变量:
\(dfn_u\)是节点 \(u\) 的时间戳,表示点 \(u\) 是第几个被访问的节点。(就是所谓的 \(dfs\) 序)。
\(low_u\) 是 \(u\) 或 \(u\) 的子树能够追溯到的最早的栈中节点的时间戳。
当 \(dfn_u=low_u\) 时,以 \(u\) 为根的搜索子树上所有节点(从栈顶到 \(u\) 的所有节点)是一个强连通分量。
注意的是,点 \(u\) 到点 \(v\) 的边若是树边(\(dfs\) 树上的边),low[u] = min(low[u], low[v])
。
若是返祖边(点 \(v\) 在栈中,即 \(v\) 是 \(u\) 在 \(dfs\) 树中的祖先),low[u] = min(low[u], dfn[v])
。
若是横叉边(点 \(v\) 已经出栈,即 \(u\) 和 \(v\) 在 \(dfs\) 树中不属于一颗子树),啥也不用干。
算法演示懒得写了,反正这是写给自己看的,课件也挂上去了,咕了。
板子
Code
void Tarjan(int u){
low[u] = dfn[u] = ++num;
vis[u] = true; //标记是否在栈里
stk[++top] = u; //入栈
for(register int i = head[u]; i; i = e[i].next){
int v = e[i].to;
if(!dfn[v]){ //树边
Tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(vis[v]) low[u] = min(low[u], dfn[v]); //返祖边
}
if(low[u] == dfn[u]){ //找到了一个强连通分量
++tot; //强连通分量的数量
int t;
do{
t = stk[top--];
vis[t] = false; //出栈记得清标记
belong[t] = tot;
}while(t != u);
}
}
题
懒得挂了。
割点
无向图上的东西。
割点:若删掉某点后,原连通图分裂为多个子图,则称该点为割点。
割点集合:在一个无向连通图中,如果有一个顶点集合,删除这个顶点集合,以及这个集合中所有顶点相关联的边以后,原图变成多个连通块,就称这个点集为割点集合。
点连通度:最小割点集合中的顶点数即为点连通度。
就第一个定义有丶用,别的没用到过。
若 \(low[v]>=dfn[u]\),则 \(u\) 为割点,节点 \(v\) 的子孙和节点 \(u\) 形成一个块。
因为这说明 \(v\) 的子孙不能够通过其他边到达u的祖先,这样去掉 \(u\) 之后,图必然分裂为两个子图。
这样我们处理点 \(u\) 时,首先递归 \(u\) 的子节点 \(v\),然后从 \(v\) 回溯至 \(u\) 后,如果发现上述不等式成立,则找到了一个割点 \(u\),并且 \(u\) 和 \(v\) 的子树构成一个块。
当然,需要注意的是如果点 \(u\) 是起点(\(dfs\) 树的根节点),至少要有两棵子树才能构成割点。
板子
Code
void Tarjan(int u){
dfn[u] = low[u] = ++num;
int son = 0;
for(register int i = head[u]; i; i = e[i].next){
int v = e[i].to;
if(!dfn[v]){
son++;
Tarjan(v);
low[u] = min(low[u], low[v]);
if(dfn[u] <= low[v]){
if(son > 1 || u != root)
cut[u] = true;
}
}
else
low[u] = min(low[u], dfn[v]);
}
}
题
《胡适日记》
割边
割边(桥):删掉它之后,图必然会分裂为两个或两个以上的子图。
割边集合:如果有一个边集合,删除这个边集合以后,原图变成多个连通块,就称这个点集为割边集合。
边连通度:一个图的边连通度的定义为,最小割边集合中的边数即为边连通度。
类比割点吧。
若 \(low[v]>dfn[u]\),则 \((u,v)\) 为割边。
但是实际处理时我们并不这样判断,因为有的图上可能有重边,这样不好处理。我们记录每条边的标号(一条无向边拆成的两条有向边标号相同),记录每个点的父亲到它的边的标号,如果边 \((u,v)\) 是 \(v\) 的父亲边,就不能用 \(dfn[u]\) 更新 \(low[v]\)。这样如果遍历完 \(v\) 的所有子节点后,发现 \(low[v]=dfn[v]\),说明 \(u\) 的父亲边 \((u,v)\) 为割边。
板子
这个好像没怎么写过,把虎哥写的搬一下。
方法一:判断low[v]==dfn[v]
方法二:判断dfn[u] < low[v]
方法三:前面两种综合一下
题
你知道我要说什么。
点双联通分量
若一个无向图中的去掉任意一个节点都不会改变此图的连通性,即不存在割点,则称作点双连通图。
一个无向图中的每一个极大点双连通子图称作此无向图的点双连通分量。
就是删掉其中任意一个点,不影响其连通性的子图。
求点双联通分量实际上在求割点的过程中就能求出,建立一个栈,存储当前双连通分支,访问每个点时把这个点入栈。
如果某时满足 \(dfn[u]<=low[v]\),说明 \(u\) 是一个割点,那么把点从栈顶一个个取出,直到遇到了点 \(u\)(\(u\) 不取出),取出的这些点和 \(u\) 组成一个点双连通分量。
割点可以属于多个点双连通分量,其余点和每条边只属于且属于一个点双连通分量。
板子
Code
void Tarjan(int u, int fa){
dfn[u] = low[u] = ++num;
vis[u] = true;
stk[++top] = u;
int son = 0;
bool first = true;
for(register int i = head[u]; i; i = e[i].next){
int v = e[i].to;
if(first && v == fa){
first = false;
continue;
}
if(!dfn[v]){
++son;
Tarjan(v, u);
low[u] = min(low[u], low[v]);
if(dfn[u] <= low[v]){
cut[u] = true;
++tot;
point[tot].push_back(u); //存点双联通分量中的每个点
int t;
do{
t = stk[top--];
point[tot].push_back(t);
}while(t != v);
}
}
else low[u] = min(low[u], dfn[v]);
}
if(fa == 0 && son == 1) cut[u] = false; //特判根节点
}
题
\(Z^ZZ^ZZ^ZZ^ZZ^ZZ^Z\)
边双联通分量
类比点双联通分量,把点改成边就行了。
删掉其中任意一个边,不影响其连通性的子图。
对于边双连通分量,只需在求出所有的桥以后,把桥边删除,原图变成了多个连通块,则每个连通块就是一个边双连通分量。
桥不属于任何一个边双连通分量,其余的边和每个顶点都属于且只属于一个边双连通分量。
板子
这个更没写过,直接搬虎哥的了。
Code
找割点,栈中now及上面的点构成一个边双。
由于要记录桥的编号,因此处理重边问题就直接用边的编号来处理了,当然也可以用上个代码中的first判断。
题
找不到。
以下为博客签名,与博文无关。
只要你们不停下来,那前面就一定有我。所以啊,不要停下来~
本文来自博客园,作者:TSTYFST,转载请注明原文链接:https://www.cnblogs.com/TSTYFST/p/16758550.html