在求割点 / 割边前,首先要理解trajan算法中两个重要数组的意义,dfn[]和low[]。
dfn[]为时间戳,表示这个点在dfs中是第几个被访问到的。
low[]表示该节点通过回边(非父子边)可以回溯到的最早节点。
割边
在遍历一个点x所有边的过程中,这条边是割边的充要条件是low[i]>low[x],即点i无法回溯到比x更早的点(如下图所示),那么此时删除这条边一定会让x和i无法连接,即其为割边。
而当low[i]<=low[x]时,即i可以回溯到早于x的节点,那么通过上图可以很清晰看出删除x到i这条边连通性不发生改变。
求割边代码如下
ll dfn[maxn],low[maxn],fa[maxn],cnt;
vector<ll>g[maxn];
void tarjan(ll x){
low[x]=dfn[x]=++cnt;
for(auto i:g[x]){
if(!dfn[i]){
fa[i]=x;//标记i的父节点为x
tarjan(i);
low[x]=min(low[x],low[i]);//更新low数组
}
else if(i!=fa[x]){//当这个点访问过且不是父节点时更新low数组
low[x]=min(low[x],dfn[i]);
}
}
}
割点
当遍历到一个点x时,这个点为割点的情况有两种:一种是该节点为根节点且子节点数大于等于2(如左图所示),则删掉这个节点后必将导致两个子节点不连通;第二种情况是该节点不为根节点且low[i]>=low[x](如右图所示),即子节点i可回溯到的最早节点不早于x点,那么删去x点一定会导致x的父节点与x的子节点不连通。
ll dfn[maxn],low[maxn],fa[maxn],cnt;
vector<ll>g[maxn];
set<ll>s;
void tarjan(ll x){
low[x]=dfn[x]=++cnt;
ll child=0;//统计x的子节点
for(auto i:g[x]){
if(!dfn[i]){
fa[i]=x;//标记点i的父节点为x
child++;
tarjan(i);
low[x]=min(low[x],low[i]);
if(low[i]>=low[x]&&fa[x])s.insert(x);
//无法回溯到早于x的节点则x为割点
}
else if(i!=fa[x]){//这里的if其实可以去掉
low[x]=min(low[x],dfn[i]);
}
}
if(!fa[x]&&child>=2)s.insert(x);
//x为根节点且有两个子节点则x为割点
}
值得注意的是x可能会在循环中反复被判定为割点,所以这里我把第一种情况的判断移到循环外(大部分题解都是两个if放在循环内)而且还用set容器去重,题目中更常用的应该是用标记数组vis[],这样时间复杂度会降低许多。同时 else if 这一句可以直接写成else,因为回溯到父节点也是可以被判定为割点的,如果能回溯更早而导致x不被判定为割点,这个low数组也是会被更新的,所以直接写成else是没问题的(而且我看到大部分人都直接写的else),但我这里为了和割边的代码统一起来以及为了符合low数组的意义所以还是写的 else if 。