图的连通性学习笔记
定义
强联通分量:在有向图中任意两个节点连通的有向图(SCC)
点双连通分量:删除任意节点仍然连通(v-DCC)
边双连通分量:删除任意边仍然连通(e-DCC)
割点:删除这个点以后图的连通性会发生改变的点
桥:删除这条边后图的连通性会发生改变的边
Tarjan
Tarjan 算法可以求出上面的所有东西。
主要思路
将图转变为 dfs 树,然后再通过记录数组 low 来判定是否拥有不需要经过其祖先就能够到达祖先上方的图。
Tarjan求强联通分量
众所周知强联通分量内的点连通性都是一样的
因此在部分问题中可以用这种方式来将环缩成一个节点,方便处理
那么就用 Tarjan 达到求出图中每一个是强联通分量的子图
先建树,然后用dfs序的特点去判断
强联通分量中,如果所连接的点是祖先,那么又因为祖先一定连向这个点,所以这个点到祖先的这一段一定会形成强联通。
那么我们记录最接近祖先的点,如果能连接到的最大祖先就是这个点本身,那么就将这些点搞成一个强联通分量即可。
我们用栈记录即可
代码:
void dfs(int p){
low[p]=dfn[p]=++cnt;
st[++top]=p;
in[p]=1;
for(int i:e[p]){
if(!dfn[i]){
dfs(i);
low[p]=min(low[p],low[i]);
}
else if(in[i])low[p]=min(low[p],dfn[i]);
}
if(dfn[p]==low[p]){
id++;
do{
in[st[top]]=0;
g[st[top]]=id;
scc[id].push_back(st[top]);
top--;
}while(st[top+1]!=p);
}
}
Tarjan求割点
无向图建树只会有连向祖先的边和树边
对于一个割点,判断是否下面的子树在我这个点删除后会被孤立
我们还是记录下可以连接到的最大祖先在哪里,如果可以连接到的最大祖先比我们此时遍历到的祖先要深,那么这个点就可以成为割点
但是还有特例,就是根的情况。
如果根被删除,它的上面是没有其它的树去连接的,那么就不一定形成割点
所以根的情况必须有两个子树才满足
代码:
void tarjan(int p){
dfn[p]=low[p]=++cnt;
vis[p]=1;
int sum=0;
for(int i:e[p]){
if(!dfn[i]){
tarjan(i);
low[p]=min(low[p],low[i]);
if(low[i]>=dfn[p])sum++;
}
else low[p]=min(low[p],dfn[i]);
}
if(sum>=2||sum==1&&p!=rt)f[p]=1;
}
Tarjan求割边
首先所有的非树边一定不会是割边
所以只考虑树边
如果既然都只有树边了,那么和割点在辨析上没有太大区别
只需要在上一步的基础上只考虑边的情况即可
代码就不放了
Tarjan求点双连通分量
点双需要这个点删了以后仍然连通
因为无向图,所以点双一定还是在子树上
如果这个点子树最浅位置不在父亲祖先,那么此时的连通已经无法扩展,只要这个父亲不选择就会直接拆开,所以此时就可以直接建立强联通分量
需要注意的是要保留这个父节点在栈里
一个节点也是点双,如果根不在任何点双里,自己建一个点双就可以了
代码:
void tarjan(int p,int fa){
vis[p]=1;
dfn[p]=low[p]=++cnt;
st[++top]=p;
int son=0;
for(int i:e[p]){
if(!dfn[i]){
tarjan(i,p);
son++;
low[p]=min(low[p],low[i]);
if(low[i]>=dfn[p]){
id++;
do{
ans[id].push_back(st[top]);
top--;
}while(st[top+1]!=i);
ans[id].push_back(p);
}
}
else if(i!=fa)low[p]=min(low[p],dfn[i]);
}
if(!son&&!fa)ans[++id].push_back(p);
}
Tarjan求边双
边双连通分量可能会有重边,对最后的结果产生影响
还是求最浅祖先,如果删除了这一条边不产生影响,那么就是边双了
还是看什么时候最浅等于dfs序
代码:
void tarjan(int p,int fa){
vis[p]=1;
dfn[p]=low[p]=++cnt;
st[++top]=p;
for(auto tmp:e[p]){
int v=tmp.first,i=tmp.second;
if(i==fa)continue;
if(!dfn[v]){
tarjan(v,i);
low[p]=min(low[p],low[v]);
}
else low[p]=min(low[p],dfn[v]);
}
if(dfn[p]==low[p]){
id++;
do{
ans[id].push_back(st[top]);
top--;
}while(st[top+1]!=p);
}
}
常见解法
强连通分量缩点
由于强连通分量每一个点都可以互相到达,具有相同连通性,我们可以将所有强连通分量缩成一个点。
此时我们就得到了一个 有向无环图,可以直接用 拓扑排序 解决很多问题,并且时间复杂度仅仅是\(O(n)\) 非常高效
点/边 双连通分量缩点
解决一些删除某点或者边是否能够正常连通的问题
配合差分约束
如果形成一个环,那么就必定相等,直接 tarjan 缩点即可
配合染色
二分染色图中,若在一个强连通分量中存在一个奇数环,那么整个强联通分量都可以形成一个奇数环
统计不互相连通的数量
直接跑连通,得到的强连通分量数量即为答案
选择单向边方向
先跑边双,每一个边双都可以转化为一个强连通分量,在保证强连通分量越多越好的情况下,然后可以对有向无环图进行分析。
其它
欧拉回路
欧拉回路在无向图中需要满足每个点的边数为偶数且连通,有向图需要满足连通且出度入度相等。
然后直接dfs即可,每个dfs结束后加入即可,每次要删边
代码:
void dfs(int p){
for(int &i=now[p];i<e[p].size();){
i++;
dfs(e[p][i-1]);
}
st[++top]=p;
}
仙人掌
任意一条边至多只出现在一条简单回路的无向连通图称为仙人掌。
问题求两个点之间距离
我们可以使用圆方树来解决
我们将一个环上的点连向一个方点,此时的距离就是根可到达的点到这个点的距离
维护完成,跑最近公共祖先,前缀和求距离
由于祖先可能是方点,需要得到进入方点的前一个圆点,并得到两个圆点的距离,通过环上前缀和。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=2e4+5;
int n,m,q,id,dfn[N],low[N],cnt,fa[N],faw[N],pre[N],sum[N],st[N][16],dep[N],dis[N],sa,sb;
vector<pair<int,int>> e[N],e2[N];
void build(int x,int y,int w){
int s=w;
for(int i=y;i!=x;i=fa[i]){
pre[i]=s;
s+=faw[i];
}
sum[x]=pre[x]=s;
e2[x].push_back({++id,0});
for(int i=y;i!=x;i=fa[i]){
sum[i]=s;
e2[id].push_back({i,min(pre[i],s-pre[i])});
}
}
void tarjan(int p,int f){
dfn[p]=low[p]=++cnt;
for(auto tmp:e[p]){
int i=tmp.first,w=tmp.second;
if(!dfn[i]){
fa[i]=p,faw[i]=w;
tarjan(i,p);
low[p]=min(low[p],low[i]);
if(low[i]>dfn[p])e2[p].push_back({i,w});
}
else if(i!=f)low[p]=min(low[p],dfn[i]);
}
for(auto tmp:e[p]){
int i=tmp.first,w=tmp.second;
if(fa[i]!=p&&dfn[i]>dfn[p])build(p,i,w);
}
}
void dfs(int p,int f){
dep[p]=dep[f]+1;
st[p][0]=f;
for(int i=1;i<=15;i++)st[p][i]=st[st[p][i-1]][i-1];
for(auto tmp:e2[p]){
int v=tmp.first,w=tmp.second;
if(v==f)continue;
dis[v]=dis[p]+w;
dfs(v,p);
}
}
int LCA(int l,int r){
if(dep[l]<dep[r])swap(l,r);
for(int i=15;i>=0;i--)if(dep[st[l][i]]>=dep[r])l=st[l][i];
if(l==r)return l;
for(int i=15;i>=0;i--){
if(st[l][i]!=st[r][i]){
l=st[l][i];
r=st[r][i];
}
}
sa=l,sb=r;
return st[l][0];
}
signed main(){
cin>>n>>m>>q;
for(int i=1,u,v,w;i<=m;i++){
cin>>u>>v>>w;
e[u].push_back({v,w});
e[v].push_back({u,w});
}
id=n;
tarjan(1,0);
dfs(1,0);
for(int i=1,a,b;i<=q;i++){
cin>>a>>b;
int lca=LCA(a,b);
if(lca<=n)cout<<dis[a]+dis[b]-2*dis[lca]<<endl;
else cout<<dis[a]-dis[sa]+dis[b]-dis[sb]+min(abs(pre[sa]-pre[sb]),sum[sa]-abs(pre[sa]-pre[sb]))<<endl;
}
return 0;
}

浙公网安备 33010602011771号