[复习] 图连通性
[复习] 图连通性
搜索生成树
定义(无向边方向是边第一次被遍历时所指的方向)
- 树边,搜索到一个新的点连的边,构成生成树。
- 返祖边,搜索到一个指向当前点到根的路径上的一个点的边。
- 前向边,指向生成树子树内一个点的边。
- 横叉边,其他边,指向兄弟子树。
有向图dfs生成树 以上四种边都有。
无向图dfs生成树 只有树边和返祖边。
无向图bfs生成树 只有树边和横叉边。
Tarjan 算法
SCC 强连通分量
在有向图中,两个点强连通当且仅当两点互相可达。
强连通分量是极大的满足每个点两两强联通的子图。
注意到在 \(dfs\) 树上,一个环只有可能由树边和返祖边构成。
定义 \(dfn\) 表示 \(dfs\) 序,\(low\) 表示经过树边和返祖边能到达的最小的 \(dfn\)。
如果一个点的 \(dfn=low\) 那么它就是这个 SCC 中深度最小的点。
我们到达一个新的点,将它加入栈,之后只用栈中的 \(dfn\) 更新 \(low\)。
同时当我们找到一个 \(dfn_x=low_x\) 时,此时栈中在 \(x\) 以上的点都和 \(x\) 在用一个 SCC 中。
vector<int> g[N];
int dfn[N],low[N],dfn1;
int stk[N],bz[N],top; // bz:是否在栈中
int scc[N],sc; // 所在 SCC 的标号
void dfs(int x){
dfn[x]=low[x]=++dfn1,stk[++top]=x,bz[x]=1;
for(int v:g[x])
if(!dfn[v])dfs(v),low[x]=min(low[x],low[v]);
else if(bz[v])low[x]=min(low[x],dfn[v]);
if(dfn[x]==low[x]){
++sc;
while(bz[x])scc[stk[top]]=sc,bz[stk[top--]]=0;
}
}
SCC 标号与拓扑序
SCC 的标号是 SCC 缩点后形成的 DAG 的拓扑序的逆序。
边双连通分量和割边
无向图中,分量内,删除任意一条边后,任意两个点可达。
连接分量的边叫做桥,或割边。
点的边双连通性具有传递性。
在无向图中,\(dfn\) 表示 \(dfs\) 序,\(low\) 表示不经过父边能到达的最小 \(dfn\)。
因此我们需要在 \(dfs\) 中记录父亲。
如果树边 \((x,v)\) 满足 \(low_v>dfn_x\) 则这条边是桥。
事实上 \(v\) 也满足 \(dfn_v=low_v\),它是边双的根。当然 \(dfs\) 树的根也是边双的根,也满足 \(dfn=low\)。
用栈记录分量内的点,发现除了更新 \(low\),其他和 SCC 的过程一样。
vector<int> g[N];
int dfn[N],low[N],dfn1;
int stk[N],top;
int num[N],num1;
//int cut[N]; // (x,fa_x) 是割边
void dfs(int x,int y){
dfn[x]=low[x]=++dfn1,stk[++top]=x;
for(int v:g[x]){
if(!dfn[v]){
dfs(v),low[x]=min(low[v],low[x]);
// if(low[v]>dfn[x]) cut[v]=1;
}
else if(v!=y)low[x]=min(dfn[v],low[x]);
}
if(low[x]==dfn[x]){
num[x]=++num1;
while(stk[top]!=x)num[stk[top--]]=num1;
--top;
}
}
点双连通分量和割点
无向图中,删掉一个点后,分量内任意点两点可达。
单独一个点没有边是一个点双,两个点一条边且没有包含两点的环的子图是点双。
割点:删掉后连通块数增加。
一个点可能属于多个点双,这个点是割点。
点的点双连通性不具有传递性,因为一个点可能属于多个点双。
但一条边一定只属于一个点双。
这里 \(dfn\) 和 \(low\) 与边双的定义一样。
如果树边 \((x,v)\) 满足 \(low_v\ge dfn_x\) 那么 \(x\) 是点双的根,此时:
- 如果 \(x\) 不是 \(dfs\) 树的根,那么 \(x\) 是割点。
- \(x\) 是 \(dfs\) 树的根:
- 有两个及以上子树,它是割点,是每个子树点双的根。
- 有一个子树,它不是割点,它是这个子树点双的根。
- 它没有子树,它不是割点,自成点双。
即要多判断根的情况。
由于割的是当前点而不是边,所以加入点双应该在遍历一个新子树之前。
由于割点属于多个点双,考虑每个点双建一个新点,把新点连向点双中的所有点,即构建圆方树。
这里我们暂时不讨论单独一个点的情况。
vector<int> g[N]; // 原无向图的边
vector<int> h[N*2] // 圆方树
int dfn[N],low[N],dfn1;
int stk[N],top;
int root,tot; // tot:圆方树新点的编号
//int cut[N]; // 是否是割点
void add(int x,int y){
h[x].push_back(y);
h[y].push_back(x);
}
void dfs(int x,int y){
dfn[x]=low[x]=++dfn1,stk[++top]=x;
int du=0; // 子树个数
if(x==root&&g[x].size()==0); // 这里我们不考虑单独一个点的情况
for(int v:g[x]){
if(!dfn[v]){
dfs(v);
low[x]=min(low[v],low[x]);
if(low[v]>=dfn[x]){
// if(++du>1||root!=x)cut[x]=1; // 求割点
++tot;
while(stk[top]!=v) add(stk[top--],tot);
add(stk[top--],tot); // !!!!! 注意这里一定要弹到 v 而不是弹到 x !!!!!
add(x,tot);
}
}
else if(v!=y)low[x]=min(low[x],dfn[v]);
}
}
int main(){
make_praph();
tot=n;
for(int i=1;i<=n;i++)if(!dfn[i])root=i,dfs(i,0),top=0; // 栈中剩下树根可能需将 top 清空
}
解释为什么 \((x,v)\) 满足条件时栈要弹到 \(v\) 而不是 \(x\)。
因为会出现在栈中 \(x\) 到 \(v\) 之间有一些与 \(x\) 同一个点双的点,因为 \(x\) 点双的根节点比 \(x\) 还要前,于是 \(x\) 的点双还没计算,这些点也没弹出。