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]);
}
}
边双连通分量
在一张连通的无向图中,对于两个点 $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 铁人两项
有向图的强连通分量
有向图 G 强连通是指,G 中任意两个结点连通。强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图。
$ 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);
}
}