这是基于DFS 的算法所以这些东西很重要
int dfn[MAXN] (dfs的时间戳,就是dfn[i]即为i点第几次被搜到的)
int low[MAXN](表示 i号点 它能走到的dfn最小的点的编号,注意:不是dfn号)
stack<int>s 一个栈,用来收集和回溯
int cnt 全局计数器,记录dfn的初始变换,可持续保持数值,不受递归影响
bool st[MAXN] (记录是否在栈里)
设一个点 U 为联通子图 G 在 dfs 搜索树里的根,那么他就是在 G 中dfn的min对应的点(根都是最小的,因为先找到了它啊)
dfn[u]=cnt++; low[u]=dfn[u];
在没有扩展之前,每个点自己都是一个联通子图。
在搜索过程中,对于结点 u 和它指向的节点 v 考虑 2 种情况:
判断标准:dfn 没有被定义,为0,压入到栈里st变为true
几何意义:传参前找到了前向边或者树枝边,回溯时后向边造成有效 l o w low low 修改
处理方法:
判断标准:st [v]
几何意义:横叉边
处理方法:v 即已经被访问过,根据 low 值的定义,使用min函数,可以取到与之相连的栈里最早被dfs的点, l o w [ u ] low[ u ] low[u]则用 d f n [ v ] dfn[ v ] dfn[v]尝试更新
void tarjan(int x)
{
low[x]=dfn[x]=++cnt;
st[x]=1;
s.push(x);
for(int i=h[x];~i;i=nxt[i])
{
int j=e[i];
if(!dfn[j])tarjan(j),low[x]=min(low[x],low[j]);
else if(st[j])low[x]=min(low[x],dfn[j]);
}
if(dfn[x]==low[x])
{
int y;
++scc_cnt;
do
{
y=s.top(); s.pop(); st[y]=0;
id[y]=scc_cnt;
}while (y!=x);
}
}
以下是引用别的巨佬的伪代~
tarjan(u)
{
DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值
Stack.push(u) // 将节点u压入栈中
for each (u, v) in E // 枚举每一条边
if (v is not visted) // 如果节点v未被访问过
tarjan(v) // 继续向下找
Low[u] = min(Low[u], Low[v])
else if (v in S) // 如果节点u还在栈内
Low[u] = min(Low[u], DFN[v])
if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
repeat v = S.pop // 将v退栈,为该强连通分量中一个顶点
print v
until (u== v)
}
int id[MAXN] (一个小点所属的大点序号)
vector<int>new_g[MAXN] (建立一个缩点之后的新图)
核心:
for(each e in color(x)){
if(e.to is not in subgraph(x)) new_g[x].push_back(color[e.to]); }
for(int k=1;k<=n;k++)
{
for(int i=h[k];~i;i=nxt[i])
{
int j=e[i];
if(id[k]!=id[j])addx(id[k],id[j]),cout<<id[k]<<" --> "<<id[j]<<endl;
}
}
割边:无向图中删去此边,全图不再联通(图的极大连通分量数增加了)
割点:无向图中删去此点及其相连的边,全图不再联通(图的极大连通分量数增加了)
点双联通分量(V - DCC):不含割点的极大联通子图
边双联通分量(E - DCC):不含割边的极大联通子图
e-dcc性质:
v-dcc性质:
割边相连的两点不一定是割点
两个割点的连边不一定是割边
延续tarjan(有向版的框架)
小细节:无向图的边是双向的,记录下来边可以防止远地不动
void tarjan_edcc(int x,int from)
{
low[x]=dfn[x]=++cnt;
s.push(x);
for(int i=h[x];~i;i=nxt[i])
{
int j=e[i];
if(!dfn[j])
{
tarjan_edcc(j,i);
low[x]=min(low[x],low[j]);
if(low[j]>dfn[x])cut_edge[i]=cut_edge[i^1]=1;
}
else if(i!=(from^1) )low[x]=min(low[x],dfn[j]);
}
if(low[x]==dfn[x])
{
int y;
++edcc_cnt;
do{
y=s.top(); s.pop();
id[y]=edcc_cnt;
}while (y!=x);
}
}
问题描述:将求完edcc的全图构造为一个大的edcc
做法:统计所有度为1的节点数 c n t cnt cnt ,易证明: a n s ≥ ( c n t + 1 ) / 2 ans\geq{} (cnt+1)/2 ans≥(cnt+1)/2
几何状态:edcc之后是只有桥的树,度为1的点均为叶子节点,考虑这些叶子节点互联,从底层构造环,抵消桥特性
具体方法:首先把两个lca最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。然后再找两个lca最远的两个叶节点,这样一对一对找完,恰好是 ( c n t + 1 ) / 2 (cnt+1)/2 (cnt+1)/2次,把所有点收缩到了一起。
1 ,对于搜索树根结点(找到dfn=0 的递归入口),只有根有大于等于2个的子节点时,他才可以是割点(详见图一和二的比较)
2,对于子节点,若在无向图G中,当且仅当点u存在一个可遍历到的后代v,且点v无法走回点u的前辈时,点u就为割点。
(详见图三的v1和v2对比)
1,根节点设为全局变量,对于根结点,统计可以搜索到的子节点(son_num
为子树个数)由于是根节点,所以一定存在low==dfn,若是 son_num>1很好,是个割点了
2,子节点判断方法
tarjan(v);
这一步后,此时v的low一定已经确定,在循环里,介入与dfn的比较,如果v最早只能到x或者更晚的点,那就是不靠x回不到祖先了,那么就是割点了3,注意,对于割点的判断可能会有很多次,如果把割点计数嵌入tarjan里,就会很多次重复(经验细节谈)
下面的代码嵌入了删除该点之后可在该点所在联通块内可以形成新的联通块的个数
bool cut[N];
void tarjan_cutdot(int x)
{
low[x]=dfn[x]=++cnt;
int s=0;
for(int i=h[x];~i;i=nxt[i])
{
int j=e[i];
if(!dfn[j])
{
tarjan_cutdot(j);
low[x]=min(low[x],low[j]);
if(low[j]>=dfn[x])s++;
}
else low[x]=min(low[x],dfn[j]);
}
if(s>0&&x!=root)cut[x]=1,s++,num=max(num,s); 不是根节点且删除可以产生至少一个子树
else if(x==root&&s>1)cut[x]=1,num=max(num,s); 是根节点且删除产生大于1的子树
}
例题:ac wing 1183
u一定和新分支 j 组成一个dcc 也和旧连通块组成dcc
那么当前最高点u还要被用在更高的包含u的旧连通块
所以如果这个时候出栈了 回溯到比u高的点的时候 u就加不进旧连通块里
void tarjan_vdcc(int x)
{
low[x]=dfn[x]=++cnt;
int num=0;
s.push(x);
if(x==root&&h[x]=-1)
{
vdcc_cnt++;
dcc[vdcc_cnt].push_back(x);
return ;
}
for(int i=h[x];~i;i=nxt[i])
{
int j=e[i];
if(!dfn[j])
{
trajan_vdcc(j);
low[x]=min(low[x],low[j]);
if(low[j]>=dfn[x])
{
num++;
if(num>1||x!=root) cut[x]=1;
++vdcc_cnt;
int y;
do{
y=s.top(); s.pop();
dcc[vdcc_cnt].push_back(y);
}while (y!=j);
dcc[vdcc+cnt].push_back(x);
}
}
else low[x]=min(low[x],dfn[j]);
}
}
星 梦 的 b l o g
OI Wiki 的 解 释 这个很棒棒哎,宝藏推荐(原理讲解很好)
算 法 的 手 动 模 拟 走 这 边 !
割 点 的 素 材 来 源 图一上,就很清晰唉