• 连通性相关


    强联通分量

    强连通:有向图 (G) 强连通表示,(G) 中任意两个结点连通。

    强连通分量( Strongly Connected Components ,简称 (operatorname{SCC}) ):极大的 强连通子图。

    Tarjan

    维护了以下两个变量:

    • (dfn) :深度优先搜索遍历时结点 (u) 被搜索的次序 。

    • (low) :设以 (u) 为根的子树为 (subtree(u))(low) 定义为以下结点的 (dfn) 的最小值: (subtree(u)) 中的结点;从 (subtree(u)) 通过 一条 不在搜索树上的边能到达的结点 。

    从根开始的一条路径上的 (dfn) 严格递增,(low) 严格非降。

    对于一个连通分量图,我们很容易想到,在该连通图中有且仅有一个 (dfn[u]=low[u]) 。该结点一定是在深度遍历的过程中,该连通分量中第一个被访问过的结点,因为它的 (dfn) 值和 (low) 值最小,不会被该连通分量中的其他结点所影响。

    因此,在回溯的过程中,判定 (dfn[u]=low[u]) 的条件是否成立,如果成立,则栈中从 后面的结点构成一个 (operatorname{SCC})

    P2341 [HAOI2006]受欢迎的牛 G (-) 模板

    $ exttt{code}$
    #define Maxn 10005
    #define Maxm 50005
    void tarjan(int u)
    {
    	 dfn[u]=low[u]=++Time; s.push(u),ins[u]=true;
    	 for(int i=hea[u];i;i=nex[i])
    	 {
    	 	 if(!dfn[ver[i]]) tarjan(ver[i]),low[u]=min(low[ver[i]],low[u]);
    		 else if(ins[ver[i]]) low[u]=min(dfn[ver[i]],low[u]);
    	 }
    	 if(dfn[u]==low[u])
    	 {
    	 	 sum+=1;
    	 	 do
    	 	 {
    	 	 	 belong[u]=sum;
    	 	 	 u=s.top(); s.pop(); ins[u]=false;
    	 	 	 cnt[sum]+=1;
    		 } while(dfn[u]!=low[u]);
    	 }
    }
    
    for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
    

    时间复杂度 (O(n+m))

    Kosaraju

    复杂度 (O(n+m))

    Garbow

    复杂度 (O(n+m))

    我们可以利用强联通分量将一张图的每个强连通分量都缩成一个点。

    然后这张图会变成一个 (operatorname{DAG}),可以进行拓扑排序以及更多其他操作 。

    应用 (-) 缩点

    P3387 【模板】缩点

    for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
    for(int i=1;i<=tot[0];i++)
    	 if(belong[fro[0][i]]!=belong[ver[0][i]])
    	 	 add(1,belong[fro[0][i]],belong[ver[0][i]]),ind[belong[ver[0][i]]]++;
    topo();
    

    割点与桥

    在无向图中删去这个点 (/) 边会使极大强联通增大,那么这个点 (/) 边为割点 (/) 桥 。

    注意这里的 (dfn) 表示不经过父亲,能到达的最小的 (dfn)

    割点

    P3388 【模板】割点(割顶)

    关键条件:

    • (u) 是根节点,当至少存在 (2) 条边满足 (low[v] >= dfn[u])(u) 是割点 。

    • (u) 不是根节点,当至少存在 (1) 条边满足 (low[v] >= dfn[u])(u) 是割点 。

    $ exttt{code}$
    void tarjan(int u,int fa)
    {
    	 dfn[u]=low[u]=++Time;
    	 for(int i=hea[u];i;i=nex[i])
    	 {
    	 	 if(!dfn[ver[i]])
    		 {
    		 	 tarjan(ver[i],u),low[u]=min(low[ver[i]],low[u]);
    		 	 if(low[ver[i]]>=dfn[u]) cnt[u]+=1;
    		 }
    		 else if(ver[i]!=fa) low[u]=min(dfn[ver[i]],low[u]);
    	 }
    }
    
    for(int i=1;i<=n;i++) if(!dfn[i]) cnt[i]-=1,tarjan(i,0);
    for(int i=1;i<=n;i++) if(cnt[i]>=1) ans+=1;
    

    割边(桥)

    关键条件:

    • 当存在一条边条边满足 (low[v] > dfn[u]) 则边 (i) 是割边

    关键部分的代码:

    注意:记录上一个访问的边时要记录边的编号,不能记录上一个过来的节点(因为会有重边)!!!

    $ exttt{code}$
    void tarjan(int x,int Last_edg)
    {
    	 dfn[x]=low[x]=++Time;
    	 for(int i=hea[x];i;i=nex[i])
    	 {
    	 	 if(!dfn[ver[i]])
    	 	 {
    	 	 	 tarjan(ver[i],i);
    	 	 	 low[x]=min(low[x],low[ver[i]]);
    	 	 	 if(low[ver[i]]>dfn[x]) edg[i]=edg[i^1]=1;
    		 }
    		 else if(i!=(Last_edg^1)) low[x]=min(low[x],dfn[ver[i]]);
    	 }
    }
    
    for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
    for(int j=2;j<=tot;j+=2) ans+=tag[j];
    

    双联通分量

    边双联通分量

    显然,找出每一个桥,去掉这些桥之后的每一个联通块都是一个边双联通字图。

    注意:用边双缩点的时候先处理出割边,之后用 ( ext{dfs}) 求出每一个双联通分量,不用栈!!

    例题:P2860 [USACO06JAN]Redundant Paths G

    $ exttt{solution}$

    一句话题意:要将原图转化为边双联通图需要添加的最少边数

    我们可以先将所有的桥找出来,并同时对所有边双缩点,会得到一颗缩完点的、由桥构成的“树”。

    我们发现这棵“树”上“叶子结点”的个数除二向上取整就是需要添加的边的条数。

    点双连通分量

    小粉兔的圆方树——点双详解

    【模板】点双连通分量

    回忆 (low) 的定义,就是 (x) 的子树内最多经过一条反祖边或一条向父亲的边能够到达的最小的 (dfn) 值。

    (x) 不是这个连通块的根时,如果存在一条边满足 (low(ver)ge dfn(x)),那么 (x) 就是一个个点。

    (x) 是根时,(x) 需要存在至少两条边满足以上条件。

    这是因为当 (x) 为根时,没有父亲与之相连。

    那么当我们在求点双时,只需要判断子树内的 (low) 是否会 (<dfn(x)),若会,则说明子树中的点能够到达 (x) 的祖先,(x) 必然不是个点。而若不会,即 (low(ver)ge dfn(x)),则说明 (x) 是这个点双的顶端个点,这时可以将递归进入的点都弹出,直至栈顶元素变为 (ver),再将 (ver) 从栈中弹出,加上 (x) 就是这个点双联通分量了。

    还要注意孤立点的情况。

    如果需要处理点双中点的个数,那么可以在栈中存放点,例如一下代码:

    void tarjan(int x)
    {
    	 dfn[x]=low[x]=++Time,sta[++tp]=x;
    	 if(tp==1 && !hea[x]) { SCC[++sum].pb(x); return; }
    	 for(int i=hea[x];i;i=nex[i])
    	 {
    	 	 if(!dfn[ver[i]])
    	 	 {
    	 	 	 tarjan(ver[i]);
    			 low[x]=min(low[x],low[ver[i]]);
    	 	 	 if(low[ver[i]]>=dfn[x])
    	 	 	 {
    	 	 	 	 sum++;
    	 	 	 	 do { SCC[sum].pb(sta[tp--]); }while(sta[tp+1]!=ver[i]);
    	 	 	 	 SCC[sum].pb(x);
    			 }
    		 }
    		 else low[x]=min(low[x],dfn[ver[i]]);
    	 }
    }
    

    而如果需要处理点双中的边,那么就需要在栈中存边,如以下代码:

    void tarjan(int x,int fa)
    {
    	 dfn[x]=low[x]=++Time;
    	 int cntson=0;
    	 for(int i=hea[x];i;i=nex[i])
    	 {
    	 	 if(!dfn[ver[i]])
    		 {
    		 	 sta[++tp]=i,tarjan(ver[i],i),cntson++;
    		 	 low[x]=min(low[x],low[ver[i]]);
    		 	 if(low[ver[i]]>=dfn[x])
    		 	 {
    		 	 	 isdian[x]=true,sum++;
    		 	 	 int tmp=0;
    		 	 	 do
    		 	 	 {
    		 	 	 	 tmp=sta[tp--];
    		 	 	 	 scc[sum].pb(fro[tmp]);
    		 	 	 	 scc[sum].pb(ver[tmp]);
    				 }while(tmp!=i);
    			 }
    		 }
    		 else if(i!=(fa^1)) low[x]=min(low[x],dfn[ver[i]]); 
    	 }
    	 if(fa==0 && cntson==1) isdian[x]=false;
    }
    
  • 相关阅读:
    【JavaScript框架封装】数据类型检测模块功能封装
    【JavaScript框架封装】数据类型检测模块功能封装
    JavaScript进阶【五】利用JavaScript实现动画的基本思路
    JavaScript进阶【四】JavaScript中的this,apply,call的深入剖析
    JavaScript进阶【三】JavaScript面向对象的基础知识复习
    JavaScript进阶【二】JavaScript 严格模式(use strict)的使用
    JavaScript进阶【一】JavaScript模块化开发的基础知识
    OPENGL学习【一】VS2008开发OPENGL程序开发环境搭建
    WEBGL学习【十五】利用WEBGL实现三维场景的一般思路总结
    Blender软件导出的obj数据格式文件内容解读
  • 原文地址:https://www.cnblogs.com/EricQian/p/15374091.html
Copyright © 2020-2023  润新知