1 无向图的连通性

  如果无向图G=<E,V>中任意两点u、v间存在通路,即u和v连通,则称G为连通图。

  容易发现,无向图G顶点间的连通关系构成了一个V上的等价关系。我们可以依照这一等价关系对图G进行划分。不妨设U是V关于顶点间连通关系构成的一个等价类,称导出子图G[U]为G的一个连通分支。

  我们可以利用dfs或者并查集来判断连通分支。

1.1 割点

  对于无向图G,在多数情况下,我们可以通过删掉一部分点使得原本连通的部分变得不连通。设U是V的子集,如果删掉U可以使G的连通分支数增加,并且仅删去U的任何子集都不能使G的连通分支数增加,则称U为G的一个点割集。特别的,若U={u},我们称u为G的一个割点。

  接下来讨论如何求出无向图的割点。

  很容易想出一种暴力的做法,即枚举无向图G中的每个点u,删去u以及与u相邻的边,然后用dfs判断G的连通分支数是否增多。效率O(n*(n+m))。

  如何更高效地解决这一问题?下面介绍效率为O(n+m)的Tarjan算法。

  容易发现,当我们对无向图G中的一个连通分量做dfs时,按照遍历的次序,我们可以得到一棵dfs树(当然,如果G不连通,得到的会是dfs森林)。按照这棵dfs树,我们可以将图G中的边分为两类:树边和非树边。同时我们也能注意到,这些非树边一定是从子结点连向它的祖先的,称之为返祖边。观察这棵dfs树,我们还可以发现,一个点可能称为割点,只可能是一下两种情况:

  ·这个点是根,并且它有至少两棵子树。

  ·这个点存在至少一棵不能通过非树边回到它的祖先的子树。

  因此我们引入时间戳,即访问某个节点的时间,我们用dft数组来维护这个时间戳。此外,我们在维护一个low数组,用来维护每个点能回到的祖先的最小时间戳。如果一个点的儿子的low不小于它的时间戳,这就说明这个儿子所在的子树无法通过非树边回到它的祖先,这个点就是割点(当然,根要特判)。

 1 void tarjan(int u,int fa){
 2     int son=0;
 3     dft[u]=low[u]=++tot;
 4     for(int i=f[u];i;i=e[i].nxt){
 5         int v=e[i].v;
 6         if(!dft[v]){
 7             son++;
 8             dfs(v,u);
 9             low[u]=min(low[u],low[v]);
10             if(low[v]>=dft[u]) flag[u]=1;
11         }
12         else if(v!=fa) low[u]=min(low[u],dft[v]);
13     }
14     if(u==root&&son==1) flag[u]=0;
15 }//tarjan求割点

1.2 割边

  仿照点割集和割点的定义,我们可以定义无向图G的边割集和割边。

  类似的,我们也可以用tarjan算法求解无向图G的割边。

  容易发现,割边一定是dfs上的一条树边,并且它所连接的子结点无法通过其子树中的边和非树边回到其祖先,换言之,它所连接的子结点u满足low[u]==dft[u]。

1.3 无向图的双连通分量

  对于无向图G上两点,如果它们之间存在至少两条点不重复的路径,我们称它们为点双连通的。G的点双连通的极大子图称为G的点双连通分量。

  对于无向图G上两点,如果它们之间存在至少两条边不重复的路径,我们称它们为边双连通的。G的边双连通的极大子图称为G的边双连通分量。

  下面讨论如何求解无向图G的边双连通分量。

  回忆上文提到的割边,我们很容易发现这样一个性质:两个结点是边双连通的,当且仅当它们之间存在通路,并且通路上没有割边。因此,割边可以被用作区分不同点双连通分量的标志。于是,我们只用对求割边的tarjan算法稍作修改,便能求解出无向图的边双连通分量。我们在dfs的同时维护一个栈,将G中的结点按照dfs的顺序放入栈中。当我们找到一条割边时,便将栈中的结点弹出,知道这条割边的所连接的子结点被弹出为止。

 1 void tarjan(int u,int fa){
 2     dft[u]=low[u]=++tot;q[++top]=u;
 3     for(int i=f[u];i;i=e[i].nxt){
 4         int v=e[i].v;
 5         if(v==fa) continue;
 6         if(!dft[v]){
 7             tarjan(v,u);
 8             low[u]=min(low[u],low[v]);
 9         }
10         else if(vis[v]==-1) low[u]=min(low[u],dft[v]);
11     }
12     if(dft[u]==low[u]){//找到割边 
13         int x;cnt++;
14         do{
15             x=q[top];top--;vis[x]=cnt;
16         }while(x!=u);
17     }
18 }//tarjan求边双连通分量

  用类似的方法,我们也可以求解无向图的点双连通分量。

2 有向图的连通性

2.1 有向图的强连通分量

  如果有向图G的基图是连通图,我们称G为弱连通图,简称为连通图。

  如果有向图G中任意两点u、v,G中既有从u到v的路径,又有从v到u的路径,即u与v相互可达,则称G为强连通图。对于非强连通图,称其极大强连通子图为强连通分量。

  我们仍然采用tarjan算法来求解有向图的强连通分量。不同于无向连通图,从有向图某一结点出发未必能遍历所有结点,故通常dfs所得到的并非一棵dfs树,而是一座dfs森林。并且较之于无向图的dfs树,在这座dfs森林中还多了一类被称为横叉边的边。横叉边定义如下:对于某棵dfs树上两结点u、v,二者没有直系亲属关系,并且dft[u]<dft[v]在,则称这条从u到v的边为横叉边。容易发现,横叉边对于强连通分量的求解并无影响,我们要关注仍然是那些返祖边。考虑某棵dfs树上任意两结点u、v,二者是相互可达的,当且仅当dfs树上从u到v的路径上没有dft==low的结点。因此我们可以将满足这种条件的结点最为划分强连通分量的依据,用类似求解无向图边双连通分量的方法求解出有向图的强连通分量。

 1 void tarjan(int u){
 2     dft[u]=low[u]=++tot;q[++top]=u;
 3     for(int i=f[u];i;i=e[i].nxt){
 4         int v=e[i].v;
 5         if(!dft[v]){
 6             tarjan(v);
 7             low[u]=min(low[u],low[v]);
 8         }
 9         else if(!vis[v]) low[u]=min(low[u],dft[v]);
10     }
11     if(low[u]==dft[u]){
12         vis[u]=++cnt;
13         while(q[top]!=u){
14             vis[q[top]]=cnt;--top;
15         }
16         --top;
17     }
18 }//tarjan求强连通分量

  可以发现这段代码与1.3中的代码非常相似。