无向图的割点与桥
在看Tarjan算法之前先看以下概念:
给定无向图连通图G=(V,E)。
若对于 x∈V ,从图中删除节点x以及与x关联的所有边之后,G 分裂为两个或者以上的连通块,则称节点 x 为无向连通图 G 的割点。
若对于 e∈E ,删除边 e 之后,G 分裂为两个不相连的子图,则称 e 为 G 的桥或割边。
Tarjan算法能够在线性的时间内求出无向图的割点和桥。
时间戳
在图的dfs过程中,按照每个节点被第一次访问的时间顺序,依次给 N 个节点 1~N 的整数标记,这个标记就是时间戳,节点 x 的时间戳记录为 dfn[x]。假设 x 节点 dfn[x]=3,意思就是在dfs过程中,访问到x节点之前有两个节点被访问过,x节点是第3个被访问到的节点。
搜索树
在无向连通图中,以任意一个节点为根节点进行dfs,每个节点访问一次,所有经过的边与点构成的树称为搜索树。下图给出一颗搜索树,其中灰色的点为选择的根节点,蓝色的边为dfs所经过的边,右图就是所构成的搜索树,节点上的编号是对应的时间戳。
追溯值
除了时间戳以外,Tarjan算法还引入了追溯值 low[x] 的概念。设subtree(x)为搜索树中以x为根节点的子树。low[x] 定义为以下节点集合时间戳的最小值。
1.subtree(x)中的节点。
2.与subtree(x)中的节点通过某一条边不在搜索树上的边相连的节点。
根据定义,为了计算 low[x],应该先令 low[x]=dfn[x],然后dfs的过程,考虑从x出发的每条无向边边(x,y):
如果无向边在搜索树上,则 low[x]=min(low[x],low[y]).
如果无向边不在搜索树,则 low[x]=min(low[x],dfn[y])
割边判定法则
无向边(x,y)是割边,当且仅当搜索树上存在一个节点x和它的子节点y,满足 dfn[x]<low[y]。
我们来看一下 dfn[x]>=low[y] 会是什么情况,如果 dfn[x]>=low[y] ,说明以节点 y 为根节点的子树中的某个节点经过某条不在搜索树的边回到了 x 或者x之前的点,那么无向边(x,y)必定参与形成了某个环,反之 dfn[x]<low[y],说明节点y无法通过无向边(x,y)以外的任何一条路径回到节点x或节点x之前的节点,那么说明要从节点x到它的子节点y只有无向边(x,y)这一条,所以这一条边一定为割边。
根据上面追溯值和时间戳的概念再加上割边判定法则,我们可以有如下判断割边的Tarjan算法:以某个节点为根节点dfs搜索,在计算时间戳dfn和追溯值low的时候,对于搜索时的某条边(x,y)在回溯的时候dfn[x]<low[y]说明这条边为割边,说的可能不太清除具体看如下代码示例:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000005;
const int inf=0x3f3f3f3f;
int n,m;//节点数n和边数m
int tot;
int head[1005],Next[maxn],ver[maxn],bridge[maxn],w[maxn];
//bridge[x]如果为1说明边x为割边
int dfn[1005],low[1005];
int deep;//记录目前节点访问顺序
void init()//初始化
{
for(int i=1;i<=n;i++)
head[i]=0;
memset(bridge,0,sizeof(bridge));
memset(dfn,0,sizeof(dfn));
tot=1;
deep=0;
}
void add(int x,int y,int z)//链式前向星添加边
{
ver[++tot]=y,w[tot]=z,Next[tot]=head[x],head[x]=tot;
}
void tarjan(int x,int in_edge)//tarjan算法求割边
{
dfn[x]=low[x]=++deep;//求当前节点的时间戳
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(!dfn[y])//如果节点y没有被访问过
{
tarjan(y,i);
low[x]=min(low[x],low[y]);//dfs回溯的时候更新low[x]
if(dfn[x]<low[y])//割边判定法则
{
bridge[i]=bridge[i^1]=1;
}
}else if(i!=(in_edge^1))//节点y不是x的父节点
{
low[x]=min(low[x],dfn[y]);
}
}
}
int main()
{
while(~scanf("%d%d",&n,&m)&&(m||n))//输入节点数n,边数m
{
init();
for(int i=1;i<=m;i++)//输入边
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
add(y,x,z);
//tot从2开始存储边,对于某个边i,边i^1为边i的反方向边
//比如第2条边为(x,y),第2^1条边为(y,x)
}
int num=0;
int f=0;
int ans=inf;
for(int i=1;i<=n;i++)
{
if(!dfn[i])
{
num++;//连通块个数
tarjan(i,0);
}
}
for(int i=2;i<=2*m;i+=2)//输出割边
{
if(bridge[i])
{
//tot从2开始存储边,对于某个边i,边i^1为边i的反方向边
//比如第2条边为(x,y),第2^1条边为(y,x)
printf("%d %d\n",ver[i^1],ver[i]);
}
}
}
return 0;
}
模板题hdu4738
割点判定法则
如果x不是搜索树的根节点(dfs的起点),则x为割点当且仅当搜索树上存在一个节点 x 和它的子节点 y 满足如下条件:
dfn[x] <= low[y]
特别的,如果 x 是搜索树的根节点,则 x 是割点当且仅当搜索树上存在至少两个节点 y1 和 y2 是x 的子节点,并且满足上述条件。
对于非根节点的节点x,满足上述条件,当dfn[x]<low[y]时,无向边(x,y)为割边,所以x一定为割点,如果dfn[x] == low[y]时说明,搜索树中以y为根节点的子树中,能通过某条不在搜索树的边回到节点x,但是回不到x以前的节点,所以删除节点x和与x关联的所有边之后,x之前的节点就没办法通过其他边到达y了。dfn[x]>low[y] 的时候说明搜索树中以y为根节点的子树中,能通过某条不在搜索树的边e回到x之前的节点,那么删除节点x和与x关联的所有边之后,x之前的节点还是能通过那条边e到达y。
所以对于非根节点的节点 x 满足 dfn[x]<=low[y] 的条件说明x是割点。
对于根节点 x ,它的两个子节点 y1 和 y2 有dfn[x] <= low[y1]&&dfn[x] <= low[y2] 因为在搜索树上,主要看y1 和 y2 同时为 x 的子节点这个条件,dfn[x] <= low[y1]&&dfn[x] <= low[y2] 这个条件其实也就是这个意思,因为x为根节点,所以 dfn[x] 肯定是1,是最小的,那么对于任何节点y都有low[y]>=dfn[x],所以其实dfn[x] <= low[y1]&&dfn[x] <= low[y2] 这个条件就是搜索树上根节点x至少有两个子节点。搜索树上为根节点的字节点的两个节点一定要通过x这个节点才能够互相到达,所以当根节点x在搜索树上有两个子及以上的子节点时根节点为割点。代码示例如下:
#include<bits/stdc++.h>
using namespace std;
const int maxn=2*1e4+10;
const int maxm=2*1e5+10;
int n,m;//节点数n,边数m
int tot=0;
int head[maxn],Next[maxm],ver[maxm];
int dfn[maxn],low[maxn];
int ans[maxn];//答案
int cut[maxn];//cut[x]=1说明x为割点
int deep=0;//当前时间戳
int root=0;//根节点
void add(int x,int y)
{
ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
}
void tarjan(int x)
{
dfn[x]=low[x]=++deep;
int sub_root=0;//子节点个数
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(!dfn[y])
{
sub_root++;
tarjan(y);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x])
{
if(root!=x||sub_root>1)//root==x的情况下在搜索树中根节点有两个子节点才算割点
cut[x]=1;
}
}else//因为dfn[x]<=low[y]时判断x为割点,所以不需要管这条便是不是入边的反向边了
low[x]=min(low[x],dfn[y]);
}
}
int main()
{
int num=0;//割点个数
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i])//tarjan图不一定联通的情况下
{
root=i;
tarjan(i);//求割点
}
}
for(int i=1;i<=n;i++)
{
if(cut[i])//cut[i]为1说明i为割点
ans[++num]=i;
}
printf("%d\n",num);//割点个数
if(num)
{
for(int i=1;i<=num;i++)
{
if(i!=num)
printf("%d ",ans[i]);
else
printf("%d\n",ans[i]);
}
}
return 0;
}
模板题洛谷割点模板题P3388