连通性问题
强连通分量
介绍:在有向图 $ G $ 中,任意两个顶点 $ u $ 和 $ v $,存在两条路径 $ u \to v $ 和 $ v \to u$,则这个图为一个强连通图,一个有向图的极大(最大的)强连通子图称为强连通分量。
Kosaraju算法
两遍dfs,第一次求每个点访问完的顺序\(d[i]\)、第二次以第一次dfs访问从晚到早的顺序对反向图进行dfs,删除遍历的节点,每次删除的节点构成一个强连通分量。时间复杂度 $ O(n+m) $。
void dfs(int x){
vis[x]=1;
for(int i=1;i<=n;i++)
if(a[x][i] && !vis[i]) dfs(i);
d[++cnt]=x;
}
void dfs(int x){
vis[x]=cnt;
for(int i=1;i<=n;i++)
if(a[i][x] && !vis[i]) dfs(i);
}
void kos(){
for(int i=1;i<=n;i++)
if(!vis[i]) dfs(i);
memset(vis,0,sizeof(vis));
cnt=0;
for(int i=n;i>=1;i--)
if(!vis[d[i]]) {++cnt;dfs2(d[i]);}
}
Tarjan 算法
统一的,对于所有的 $ tarjan $ 算法,都以一个 $ u $ 为根建一颗搜索树,对于遍历到的节点,维护两个值 $ dfn $ 和 $ low $。其中 $ dfn $ 为时间戳,$ low $ 是以这个点为根的搜索树可以访问到最小的 $ dfn $。
对于 $ low $ 其实包含两种,第一种是他的子孙的 $ low $,第二种是他的子孙可以访问到的点的 $ dfn $。如果说一个点的 $ dfn $ 等于他的 $ low $,也就是说这个点 $ u $ 可以通向子孙 $ v $。并且他的子孙 $ v $ 也存在一条路径到 $ u $。
时间复杂度 $ O(n+m) $。
void tarjan(int u){
dfn[u]=low[u]=++cnt;
vis[u]=1;stk[++top]=u;//vis=0 未访问 =1 在此搜索树内 =2在其他的强连通分量中
int len=g[u].size();
for(int i=0;i<len;i++) {
int v=g[u][i];
if(vis[v]==2) continue;
if(!vis[v])
tarjan(v);
if(vis[v]==1) low[u]=min(low[u],dfn[v]);
else low[u]=min(low[u],low[v]);
}
if(dfn[u]==low[u]){
++cnt2;int v;
do{
v=stk[top--];
belong[v]=cnt2;
ans[cnt2].push_back(v);
vis[v]=2;
}
while(u^v) ;
}
}
点双
割点
当一个点删掉后,图的联通块数量改变,这个点为割点。
利用 $ tarjan $ 求解。当 $ low_v \ge dfn_u$ 时,说明 $ v $ 能到达的点只能是 $ u $ 和其子孙而时间戳小于 $ u $ 的点则无法访问,删去 $ u $ 后,时间戳小于 $ u $ 的点就无法访问了,故此点为割点(不为根)。
搜索树的根为特例,如果此点为根,且树边(连向他的子孙)大于一,否则反之。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e4+5;
int n,m;
int head[maxn],tot;
struct edge{
int nt,to;
}g[maxn*10];
void add(int x,int y){
g[++tot].to=y;
g[tot].nt=head[x];
head[x]=tot;
}
int dfn[maxn],low[maxn];
int vis[maxn],cnt,stk[maxn],top,ans;
int p,l[maxn];
bool cut[maxn];
void tarjan(int u,int rt){
dfn[u]=low[u]=++cnt;vis[u]=1;
int son=0;
for(int i=head[u];i;i=g[i].nt){
int v=g[i].to;
if(vis[v]==0) {
son++;
tarjan(v,rt);
if(u!=rt && low[v]>=dfn[u]) cut[u]=1;
low[u]=min(low[u],low[v]);
}
else low[u]=min(low[u],dfn[v]);
}
if(u==rt && son>=2) {
cut[u]=1;
}
}
vector<int>q[maxn];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i,i);
for(int i=1;i<=n;i++)
if(cut[i]) ans++;
printf("%d\n",ans);
for(int i=1;i<=n;i++)
if(cut[i]) printf("%d ",i);
return 0;
}
点双联通分量
点双联通:没有割点的图。
割点有几个性质,割点属于多个点双联通分量,连接几个点双连通分量。非割点则只属于一个点双联通分量(多个就是割点了)。
和求割点相似,但一个割点属于多个点双联通分量,出栈时注意判不弹出割点。
#include<bits/stdc++.h>
using namespace std;
int n,m,low[500005],dfn[500005],ans,cnt;
int nxt[4000005],head[500005],go[4000005],k;
vector<int> dcc[500005];
stack<int>sta;
void add(int u,int v)
{
nxt[++k]=head[u];
head[u]=k;
go[k]=v;
}
void tarjan(int x,int root)//求割点的改版(其实不需要root)
{
dfn[x]=low[x]=++cnt;
if(x==root&&!head[x])//孤立点判定
{
dcc[++ans].push_back(x);
}
sta.push(x);
for(int i=head[x];i;i=nxt[i])
{
int g=go[i];
if(!dfn[g])
{
tarjan(g,root);
low[x]=min(low[x],low[g]);
if(low[g]>=dfn[x])
{
ans++;
int p;
do{//弹栈
p=sta.top();
sta.pop();
dcc[ans].push_back(p);
}while(p!=g);//注意此处,因为要求是不到达出点
dcc[ans].push_back(x);//别忘了加入源点!
}
}
else
low[x]=min(low[x],dfn[g]);
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
if(x==y) continue;//重边
add(x,y);
add(y,x);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i]) tarjan(i,i);//注意图可能不连通
}
cout<<ans<<endl;
for(int i=1;i<=ans;i++)
{
cout<<dcc[i].size()<<" ";
for(int j=0;j<dcc[i].size();j++)
cout<<dcc[i][j]<<" ";
cout<<endl;
}
}
边双
割边
如果联通图中一条边被删去后图不连通,那么这条边为割边(桥)。
tarjan求解。当 $ low_v > dfn_u $,从 $ v $ 没有第二条路径可以走到 $ u $ 了,所以$ u \to v $ 这条边为割边。
int dfn[maxn],low[maxn],cnt;
bool bj[maxn];//是否为桥
void tarjian(int now,int in) {
dfn[now]=low[now]=++cnt;
for(int i=head[now];i;i=nex[i]) {
int st=to[i];
if(!dfn[st]) {
tarjian(st,i);
low[now]=min(low[now],low[st]);
if(low[st]>dfn[now]) bj[i]=true,bj[i^1]=true;
}
else if(i!=(in^1)) low[now]=min(low[now],dfn[st]);
}
}
边双联通分量
与求桥差别不大,一个边双联通图,其所有边都至少存在于一个环中,而桥联通多个联通分量,删掉桥以后,每个联通分量都是边双联通分量,$ dfs $搞即可。

浙公网安备 33010602011771号