[关闭]
@DATASOURCE 2015-09-09T08:13:04.000000Z 字数 8418 阅读 7000

图的连通性问题

图论 连通性


基本概念

无向图

有向图

无向图点连通性的求解及应用

求割点

Tarjan 算法只需从某个顶点出发进行一次遍历,就可以求得图中所有的关节点,因此其复杂度为O(n^2)。接下来以图(a)所示的无向图为例介绍这种方法。
在图(a)中,对该图从顶点 4 出发进行深度优先搜索,实线表示搜索前进方向,虚线表示 回退方向,顶点旁的数字标明了进行深度优先搜索时各顶点的访问次序,即深度优先数。在 DFS 搜索过程中,可以将各顶点的深度优先数记录在数组dfn 中。 图(b)是进行DFS 搜索后得到的根为顶点4 的深度优先生成树。为了更加直观地描述树形结 构,将此生成树改画成图(d)所示的树形形状。在图(d)中,还用虚线画出了两条虽然属于图G、但 不属于生成树的边,即(4, 5)和(6, 8)。 请注意:在深度优先生成树中,如果u 和v 是2 个顶点,且在生成树中u 是v 的祖先,则必 有dfn[u] < dfn[v],表明u 的深度优先数小于v,u 先于 v 被访问。

图G 中的边可以分为3 种:

顶点u 是关节点的充要条件:
1) 如果顶点u 是深度优先搜索生成树的根,则u 至少有2 个子女。为什么呢?因为删除u,它的子女所在的子树就断开了,你不用担心这些子树之间(在原图中)可能存在边,因为交叉边是不存在的。

2) 如果 u 不是生成树的根,则它至少有一个子女 w,从 w 出发,不可能通过w、w 的子孙,以及一条回边组成的路径到达 u 的祖先。为什么呢?这是因为如果删除顶点 u 及其 所关联的边,则以顶点 w 为根的子树就从搜索树中脱离了。例如,顶点6 为什么是关节 点?这是因为它的一个子女顶点,如图(d)所示,即顶点7,不存在如前所述的路径到达顶点6 的祖先结点,这样,一旦顶点6 删除了,则以顶点7 为根结点的子树就断开了。 又如,顶点7 为什么不是关节点?这是因为它的所有子女顶点,当然在图(d)中只有顶点 8,存在如前所述的路径到达顶点7 的祖先结点,即顶点6,这样,一旦顶点7 删除了, 则以顶点8 为根结点的子树仍然跟图G 连通。

因此,可对图 G 的每个顶点 u 定义一个 low 值:low[u]是从 u 或 u 的子孙出发通过回边可以到达的最低深度优先数。low[u]的定义如下:

low[u] = Min
{
dfn[u],
Min{ low[w] | w 是u 的一个子女}, (8-2)
Min{ dfn[v] | v 与u 邻接,且(u,v)是一条回边 }
}

即low[u]是取以上三项的最小值,其中:

因此,顶点u 是关节点的充要条件是:u 或者是具有两个以上子女的深度优先生成树的根, 或者虽然不是一个根,但它有一个子女w,使得 low[w]>=dfn[u]。
其中,“low[w]>=dfn[u]”的含义是:顶点u 的子女顶点w,能够通过如前所述的路径到达顶 点的最低深度优先数大于等于顶点u 的深度优先数(注意在深度优先生成树中,顶点m 是顶点n 的祖先,则必有dfn[m] 接下来结合图和表解释在回退过程中计算每个顶点 n 的low[n]值的方法 (当前计算出来的low[n]值用粗体、斜体及下划线标明):

  1. // 当 res[i] > 0 时说明 i 是割点, 并且去掉 i 之后图的连通分量的个数为 res[i];
  2. void Tarjan(int u, int fa){
  3. int son = 0;
  4. dfn[u] = low[u] = ++tdfn;
  5. for(int i = head[u]; i + 1; i = edge[i].nxt){
  6. int v = edge[i].v;
  7. if((fa ^ 1) == i) continue;
  8. if(!dfn[v]){
  9. Tarjan(v, i);
  10. low[u] = min(low[u], low[v]);
  11. if(low[v] >= dfn[u]) son++;
  12. }else low[u] = min(low[u], dfn[v]);
  13. }
  14. if(u != root && son) res[u] = son + 1;
  15. else if(u == root && son > 1) res[u] = son;
  16. else res[u] = 0;
  17. }
  18. // 调用时
  19. root = 1;
  20. Tarjan(root, -1);

点双连通分量的求解

在求关节点的过程中就能顺便把每个重连通分量求出。方法是:建立一个栈,存储当前重连通分量,在 DFS 过程中,每找到一条生成树的边或回边,就把这条边加入栈中。如果遇到某个顶点 u 的子女顶点 v 满足 dfn[u] <= low[v],说明 u 是一个割点,同时把边从栈顶一条条取出,直到遇到了边(u, v),取出的这些边与其关联的顶点,组成一个重连通分量。割点可以属于多个重连通分量,其余顶点和每条边属于且只属于一个重连通分量。

  1. // bcc_cnt 即连通分支数目
  2. void Tarjan(int u, int fa){
  3. sta.push(u);
  4. instack[u] = true;
  5. low[u] = dfn[u] = ++tdfn;
  6. for(int i = head[u]; i + 1; i = edge[i].nxt){
  7. int v = edge[i].v;
  8. if((fa ^ 1) == i) continue;
  9. if(!dfn[v]){
  10. Tarjan(v, i);
  11. low[u] = min(low[u], low[v]);
  12. }else low[u] = min(low[u], dfn[v]);
  13. }
  14. if(low[u] == dfn[u]){
  15. bcc_cnt++;
  16. int top;
  17. do{
  18. top = sta.top();
  19. sta.pop();
  20. instack[top] = false;
  21. belong[top] = bcc_cnt;
  22. }while(u != top);
  23. }
  24. }

顶点连通度求解

割边的求解

割边的求解过程与求割点的过程类似,判断方法是:无向图中的一条边(u, v)是桥,当且仅当(u, v)为生成树中的边,且满足dfn[u] < low[v]。
例如,图(a)所示的无向图,如果从顶点 4 开始进行DFS 搜索,各顶点的 dfn[] 值和 low[] 值如图(a)所示(每个顶点旁的两个数值分别表示 dfn[] 值和 low[] 值),深度优先搜索树如图(b)所 示。根据上述判断方法,可判断出边(1, 5)、(4, 6)、(8, 9)和(9, 10)为无向图中的割边。

  1. // 求桥的模板, res数组存储的是桥的编号
  2. void Tarjan(int u, int fa){
  3. low[u] = dfn[u] = ++tdfn;
  4. for(int i = head[u]; i + 1; i = edge[i].nxt){
  5. int v = edge[i].v;
  6. if((fa ^ 1) == i) continue;
  7. if(!dfn[v]){
  8. Tarjan(v, i);
  9. low[u] = min(low[u], low[v]);
  10. if(low[v] > dfn[u]) res[cnt++] = edge[i].id;
  11. }else low[u] = min(low[u], dfn[v]);
  12. }
  13. }
  14. // 调用时 Tarjan(1, -1);

边双连通分量的求解

在求出所有的桥以后,把桥删除,原图变成了多个连通块,则每个连通块就是一个边双连通分量。桥不属于任何一个边双连通分量,其余的边和每个顶点都属于且只属于一个边双连通分量。

边连通度的求解

有向图强连通性的求解及应用

有向图强连通分量的求解算法

Tarjan 算法

Tarjan 算法是基于 DFS 算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索 树中未处理的节点加入一个栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。当 dfn(u) = low(u)时,以 u 为根的搜索子树上所有节点是一个强连通分量。
接下来以图(a)所示的有向图为例解释 Tarjan 算法的思想和执行过程,在该有向图中,{ 1, 2, 5, 3 }为一个强连通分量,{ 4 }、{ 6 }也分别是强连通分量。 图(b)为从顶点1 出发进行深度优先搜索后得到的深度优先搜索树。约定:如果某个顶点有多 个未访问过的邻接顶点,按顶点序号从小到大的顺序进行选择。各顶点旁边的两个数值分别为顶 点的深度优先数(dfn[])值和low[]值。在图(b)中,虚线表示非生成树的边,其中边<5, 6>为交 叉边,边<5, 1>和<3, 5>是回边

图(c)~(f)演示了 Tarjan 算法的执行过程。在图(c)中,沿着实线箭头所指示的方向搜索到顶点 6,此时无法再前进下去了,并且因为此时 dfn[6] = low[6] = 4,所以找到了一个强连通分量。退栈到u == v 为止,{ 6 }为一个强连通分量。
在图(d)中,沿着虚线箭头所指示的方向回退到顶点4,发现dfn[4] == low[4],为3,退栈后{ 4 } 为一个强连通分量。
在图(e)中,回退到顶点2 并继续搜索到顶点5,把顶点5 加入栈。发现顶点5 有到顶点 1 的有向边,顶点 1 还在栈中,所以 low[5] = 1,有向边 <5, 1> 为回边。顶点 6 已经出栈,所以 <5, 6> 是交叉边,返回顶点 2,<2, 5>为生成树的边,所以low[2] = low[5] = 1。
在图(f)中,先回退到顶点 1,接着访问顶点 3。发现顶点 3 到顶点有一条有向边,顶点 5 已经访问过了、且 5 还在栈中,因此边 <3, 5> 为回边,所以low[3] = dfn[5] = 5。返回顶点 1 后,发现 dfn[1] == low[1],把栈中的顶点全部弹出,组成一个连通分量{ 3, 5, 2, 1 }。 至此,Tarjan 算法结束,求出了图中全部的三个强连通分量为{ 6 }、{ 4 }和{ 3, 5, 2, 1 }。
Tarjan 算法的时间复杂度分析:假设用邻接表存储图,在 Tarjan 算法的执行过程中,每个顶点都被访问了一次,且只进出了一次栈,每条边也只被访问了一次,所以该算法的时间复杂度为 O(n + m)。

  1. // 代码模板
  2. void Tarjan(int u){
  3. sta.push(u);
  4. instack[u] = true;
  5. dfn[u] = low[u] = ++tdfn;
  6. for(int i = head[u]; i + 1; i = edge[i].nxt){
  7. int v = edge[i].v;
  8. if(!dfn[v]){
  9. Tarjan(v);
  10. low[u] = min(low[u], low[v]);
  11. }else if(instack[v]) low[u] = min(low[u], dfn[v]);
  12. }
  13. if(low[u] == dfn[u]){
  14. int top;
  15. scc_cnt++;
  16. do{
  17. top = sta.top();
  18. sta.pop();
  19. instack[top] = false;
  20. belong[top] = scc_cnt;
  21. }while(u != top);
  22. }
  23. }
  24. // 调用
  25. for(int i = 1; i <= n; i++)
  26. if(!dfn[i]) Tarjan(i);

Kosaraju 算法

Kosaraju 算法是基于对有向图 G 及其逆图 GT(各边反向得到的有向图)进行两次 DFS 的方 法,其时间复杂度也是 O(n + m)。与 Trajan 算法相比,Kosaraju 算法的思想更为直观。
Kosaraju 算法的原理为:如果有向图 G 的一个子图 G' 是强连通子图,那么各边反向后没有任何影响,G' 内各顶点间仍然连通,G' 仍然是强连通子图。但如果子图G'是单向连通的,那么各边反向后可能某些顶点间就不连通了,因此,各边的反向处理是对非强连通块的过滤。
Kosaraju 算法的执行过程为:

  1. // 算法模板
  2. void dfs(int u){
  3. vis[u] = true;
  4. for(int i = head[u]; i + 1; i = edge[i].nxt)
  5. if(!vis[edge[i].v]) dfs(edge[i].v);
  6. vs[vscnt++] = u;
  7. }
  8. void rdfs(int u, int k){
  9. vis[u] = true;
  10. belong[u] = k;
  11. for(int i = rhead[u]; i + 1; i = redge[i].nxt)
  12. if(!vis[redge[i].v]) rdfs(redge[i].v, k);
  13. }
  14. int scc(){
  15. memset(vis, 0, sizeof(vis));
  16. vscnt = 0;
  17. for(int i = 1; i <= n; i++)
  18. if(!vis[i]) dfs(i);
  19. int scc_cnt = 0;
  20. memset(vis, 0, sizeof(vis));
  21. for(int i = vscnt - 1; i >= 0; i--)
  22. if(!vis[vs[i]]) rdfs(vs[i], scc_cnt++);
  23. return scc_cnt;
  24. }

连通性算法的应用2_SAT

简介

现有一个由 N 个布尔值组成的序列 A,给出一些限制关系,比如 A[x] AND A[y]=0 A[x] OR A[y] OR A[z] = 1 等,要确定 A[0..N-1] 的值,使得其满足所有限制关系。这个称为 SAT 问题,特别的,若每种限制关系中最多只对两个元素进行限制,则称为 2-SAT 问题。

展开

由于在2-SAT问题中,最多只对两个元素进行限制,所以可能的限制关系共有11种:
A[x]
NOT A[x]
A[x] AND A[y]
A[x] AND NOT A[y]
A[x] OR A[y]
A[x] OR NOT A[y]
NOT (A[x] AND A[y])
NOT (A[x] OR A[y])
A[x] XOR A[y]
NOT (A[x] XOR A[y])
A[x] XOR NOT A[y]
进一步,A[x] AND A[y]相当于(A[x]) AND (A[y])(也就是可以拆分成A[x]与A[y]两个限制关系),NOT(A[x] OR A[y])相当于NOT A[x] AND NOT A[y](也就是可以拆分成NOT A[x]与NOT A[y]两个限制关系)。因此,可能的限制关系最多只有9种。

在实际问题中,2-SAT问题在大多数时候表现成以下形式:有N对物品,每对物品中必须选取一个,也只能选取一个,并且它们之间存在某些限制关系(如某两个物品不能都选,某两个物品不能都不选,某两个物品必须且只能选一个,某个物品必选)等,这时,可以将每对物品当成一个布尔值(选取第一个物品相当于0,选取第二个相当于1),如果所有的限制关系最多只对两个物品进行限制,则它们都可以转化成9种基本限制关系,从而转化为2-SAT模型。

建模

其实 2-SAT 问题的建模是和实际问题非常相似的。建立一个 2N 阶的有向图,其中的点分为 N 对,每对点表示布尔序列 A 的一个元素的 0、1 取值(以下将代表 A[i] 的 0 取值的点称为 i,代表 A[i] 的 1 取值的点称为i')。显然每对点必须且只能选取一个。然后,图中的边具有特定含义。若图中存在边 ,则表示若选了 i 必须选 j。可以发现,上面的 9 种限制关系中,后7种二元限制关系都可以用连边实现,比如NOT(A[x] AND A[y])需要连两条边和,A[x] OR A[y]需要连两条边和。而前两种一元关系,对于A[x](即x必选),可以通过连边来实现,而NOT A[x](即x不能选),可以通过连边来实现。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注