[模板] tarjan/联通分量/dfs树

边的分类

有向图

有向图边分为四类: 树边, 前向边, 返祖边(后向边), 横叉边.

上图:

1326473-20190317204820171-2076756199.png

判定

对图进行dfs, 不考虑已经遍历过的点, 得到dfs序 \(dfn_i\).

在dfs过程中, 记录当前dfs栈. 对于边\((u,v)\),

  1. 树边: \(vis_v=0\);
  2. 前向边: \(vis_v=1\)\(dfn_v > dfn_u\);
  3. 返祖边: \(vis_v=1\)\(dfn_v < dfn_u\), 且 \(v\) 在当前栈内;
  4. 横叉边: \(vis_v=1\)\(dfn_v < dfn_u\), 且 \(v\) 不在当前栈内.

无向图

1326473-20190510210243578-1995870905.png

如上图, 边仅分为两种:

  1. 树边: \(vis_v=0\) ;
  2. 前向边/返祖边, 这两个其实是同一条边. 可以通过 dfs 序区分.

有向图的强连通分量 && 缩点 (Tarjan)

Tarjan算法寻找有向图的强连通分量 – Miskcoo's Space

简介

Tarjan 强连通分量算法可以找出图的所有强连通分量, 并为每个点标记所在的强连通分量.

定义:

  • \(dfn_i\) : \(i\) 节点的dfs序;
  • \(low_i\) : \(i\) 节点通过最多一条返祖边能到达的最小dfs序.
  • 栈, 维护:
    1. 当前节点到根的链;
    2. 在上面节点所在的强连通分量中, 且之前遍历过的点.
  • \(vi_i\): 表示节点是否在栈内.

应用

缩点.

tarjan求出的强连通分量标号为逆拓扑序.

代码

int dfn[nsz],low[nsz],pd=0,scc[nsz],ps=0;
int stk[nsz],top=0,vi[nsz];
// get scc
void tar(int p){
    dfn[p]=low[p]=++pd;
    stk[++top]=p,vi[p]=1;
    for(int i=g1.hd[p],v;i;i=g1.edge[i].pr){
        v=g1.edge[i].t;
        if(dfn[v]==0){
            tar(v);
            low[p]=min(low[p],low[v]);
        }
        else if(vi[v])low[p]=min(low[p],dfn[v]); // 返祖边; 横叉边不更新
    }
    if(low[p]==dfn[p]){
        ++ps;
        do{
            vi[stk[top]]=0;
            scc[stk[top]]=ps;
        }while(stk[top--]!=p);
    }
}
//缩点
void sol(){
    rep(i,1,n)if(scc[i]==0)tar(i);
    rep(i,1,n){
        for(int j=g1.hd[i],v;j;j=g1.edge[j].pr){
            v=g1.edge[j].t;
            if(scc[i]!=scc[v])g2.adde(scc[i],scc[v]);
        }
    }
}

其他

事实上, 将 \(low_i\) 定义为 "\(i\) 节点通过任意条返祖边能到达的最小dfs序" 也是合法的, 即将

        else if(vi[v])low[p]=min(low[p],dfn[v]); 

改为

        else if(vi[v])low[p]=min(low[p],low[v]); 

不难发现这两个做法是等价的; 但在求双连通分量时这么写是错误的.

无向图的边双联通分量,割边,缩点

点双连通分量为对点的划分.

求边双/缩点代码和强连通分量几乎相同.

//由于可能有重边, 搜索时传递的应为e(fa,p)这条边, 而非父亲这个点
int dfn[nsz],low[nsz],pd=0,ebcc[nsz],pb=0;
int stk[nsz],top=0,vi[nsz];
void tar(int p,int efa){
    dfn[p]=low[p]=++pd;
    stk[++top]=p,vi[p]=1;
    forg(p,i,v){
        if(i==(efa^1))continue;
        if(dfn[v]==0){
            tar(v,i); //warning
            low[p]=min(low[p],low[v]);
        }
        else if(vi[v])low[p]=min(low[p],dfn[v]);
    }
    if(dfn[p]==low[p]){ //efa is cut edge
        ++pb;
        do{
            ebcc[stk[top]]=pb;
            vi[stk[top]]=0;
        }while(stk[top--]!=p);
    }
}
int in[nsz];
int sol(){
    rep(i,1,n)if(dfn[i]==0)tar(i,0);
    rep(i,1,n){
        forg(i,j,v){
            if(ebcc[v]!=ebcc[i])++in[ebcc[v]]; //a cut edge
        }
    }
}

无向图的点双联通分量,割点,缩点

点双连通分量为对边的划分, 因此栈中存边, 而非点.

割点 \(\iff\) 属于多个点双的点.

所有点双和割点形成一棵树结构.

缩点: 在新图中对每个割点和点双各建一个点, 将每个割点连向所在的点双.

代码较长, 但比较容易理解.

int dfn[nsz],pd=0,low[nsz],iscut[nsz],curbcc[nsz],pbcc=0;
int bccsz[nsz];
set<int> inbcc[nsz];
pair<int,int> stk[msz];
int top=0;
void color(int p,int pbcc){
    if(curbcc[p]!=pbcc){
        inbcc[p].insert(pbcc);
        curbcc[p]=pbcc;
        ++bccsz[pbcc];
    }
}
//could not find  
void tarj(int p,int fe){
    dfn[p]=low[p]=++pd;
    for(int i=hd[p],v;i;i=edge[i].pr){
        if(i==fe)continue;
        v=edge[i].t;
        if(dfn[v]==0){
            stk[++top]=make_pair(p,v);
            tarj(v,i^1);
            low[p]=min(low[p],low[v]);
            if(low[v]>=dfn[p]){//点双
                iscut[p]=1,++pbcc;
                int x,y;
                do{
                    x=stk[top].first,y=stk[top].second,--top;
                    color(y,pbcc);
                }while(!(x==p&&y==v));
                color(p,pbcc);
            }
        }
        else if(dfn[v]<dfn[p]){//返祖边 
            low[p]=min(low[p],dfn[v]);
            stk[++top]=make_pair(p,v);
        }
    }
    if(fe==0){
        if(hd[p]==0)color(p,++pbcc); //只有一个点的图
        if(edge[hd[p]].pr==0)iscut[p]=0;//度数<=1的根不是割点
    }
}

转载于:https://www.cnblogs.com/ubospica/p/10548822.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值