zoukankan      html  css  js  c++  java
  • [模板] tarjan/连通分量/dfs树

    边的分类

    有向图

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

    上图:

    判定

    对图进行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) 不在当前栈内.

    无向图

    如上图, 边仅分为两种:

    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]); 
    

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

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

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

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

    栈中维护的是:

    1. 当前点到根的路径
    2. 当前点所在点双中的点
    //由于可能有重边, 搜索时传递的应为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 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) 属于多个点双的点.

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

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

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

    //cf962f
    //无自环
    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的根不是割点
    	}
    }
    
    //debug
    	rep(i,1,n){
    		printf("p=%d iscut=%d
    ",i,iscut[i]);
    		for(int v:inbcc[i])printf("%d ",v);
    		printf("
    ");
    	}
    //3 2 1 2 2 3
    //p=1 iscut=0
    //2
    //p=2 iscut=1
    //1 2
    //p=3 iscut=0
    //1
    
    //6 5
    //1 6 6 2 6 3 2 4 2 5
    //p=1 iscut=0
    //5
    //p=2 iscut=1
    //2 3 4
    //p=3 iscut=0
    //1
    //p=4 iscut=0
    //3
    //p=5 iscut=0
    //2
    //p=6 iscut=1
    //1 4 5
    
    
    //5 6
    //1 2 1 3 3 4 5 3 5 1 3 5
    //p=1 iscut=1
    //2 3
    //p=2 iscut=0
    //3
    //p=3 iscut=1
    //1 2
    //p=4 iscut=0
    //1
    //p=5 iscut=0
    //2
    
  • 相关阅读:
    Java JPA 入门
    Java JSP 常用标签及自定义标签
    Collection 之 Set、List和Map
    Socket 详解
    MyEclipse 配置SQL Server 2008数据库驱动
    IO 常用IO操作类继承结构 及简单简介
    JSP 相关对象
    Android 入门体验
    C#访问修饰符总结
    C# Socket编程 同步以及异步通信
  • 原文地址:https://www.cnblogs.com/ubospica/p/10548822.html
Copyright © 2011-2022 走看看