简介:html
割边和割点的定义仅限于无向图中。咱们能够经过定义以蛮力方式求解出无向图的全部割点和割边,但这样的求解方式效率低。Tarjan提出了一种快速求解的方式,经过一次DFS就求解出图中全部的割点和割边。java
欢迎探讨,若有错误敬请指正 算法
如需转载,请注明出处 http://www.cnblogs.com/nullzx/数组
在无向图中才有割边和割点的定义ide
割点:无向连通图中,去掉一个顶点及和它相邻的全部边,图中的连通份量数增长,则该顶点称为割点。函数
桥(割边):无向联通图中,去掉一条边,图中的连通份量数增长,则这条边,称为桥或者割边。this
割点与桥(割边)的关系:spa
1)有割点不必定有桥,有桥必定存在割点.net
2)桥必定是割点依附的边。3d
下图中顶点C为割点,但和C相连的边都不是桥。
暴力法的原理就是经过定义求解割点和割边。在图中去掉某个顶点,而后进行DFS遍历,若是连通份量增长,那么该顶点就是割点。若是在图中去掉某条边,而后进行DFS遍历,若是连通份量增长,那么该边就是割边。对每一个顶点或者每一个边进行一次上述操做,就能够求出这个图的全部割点和割边,咱们称之为这个图的割点集和割边集。
在具体的代码实现中,并不须要真正删除该顶点和删除依附于该顶点全部边。对于割点,咱们只须要在DFS前,将该顶点对应是否已访问的标记置为ture,而后从其它顶点为根进行DFS便可。对于割边,咱们只须要禁止从这条边进行DFS后,若是联通份量增长了,那么这条边就是割边。
判断一个顶点是否是割点除了从定义,还能够从DFS(深度优先遍历)的角度出发。咱们先经过DFS定义两个概念。
假设DFS中咱们从顶点U访问到了顶点V(此时顶点V还未被访问过),那么咱们称顶点U为顶点V的父顶点,V为U的孩子顶点。在顶点U以前被访问过的顶点,咱们就称之为U的祖先顶点。
显然若是顶点U的全部孩子顶点能够不经过父顶点U而访问到U的祖先顶点,那么说明此时去掉顶点U不影响图的连通性,U就不是割点。相反,若是顶点U至少存在一个孩子顶点,必须经过父顶点U才能访问到U的祖先顶点,那么去掉顶点U后,顶点U的祖先顶点和孩子顶点就不连通了,说明U是一个割点。
上图中的箭头表示DFS访问的顺序(而不表示有向图),对于顶点D而言,D的孩子顶点能够经过连通区域1红色的边回到D的祖先顶点C(此时C已被访问过),因此此时D不是割点。
上图中的连通区域2中的顶点,必须经过D才能访问到D的祖先顶点,因此说此时D为割点。再次强调一遍,箭头仅仅表示DFS的访问顺序,而不是表示该图是有向图。
这里咱们还须要考虑一个特殊状况,就是DFS的根顶点(通常状况下是编号为0的顶点),由于根顶点没有祖先顶点。其实根顶点是否是割点也很好判断,若是从根顶点出发,一次DFS就能访问到全部的顶点,那么根顶点就不是割点。反之,若是回溯到根顶点后,还有未访问过的顶点,须要在邻接顶点上再次进行DFS,根顶点就是割点。
在具体实现Tarjan算法上,咱们须要在DFS(深度优先遍历)中,额外定义三个数组dfn[],low[],parent[]
4.1 dfn数组
dnf数组的下标表示顶点的编号,数组中的值表示该顶点在DFS中的遍历顺序(或者说时间戳),每访问到一个未访问过的顶点,访问顺序的值(时间戳)就增长1。子顶点的dfn值必定比父顶点的dfn值大(但不必定刚好大1,好比父顶点有两个及两个以上分支的状况)。在访问一个顶点后,它的dfn的值就肯定下来了,不会再改变。
4.2 low数组
low数组的下标表示顶点的编号,数组中的值表示DFS中该顶点不经过父顶点能访问到的祖先顶点中最小的顺序值(或者说时间戳)。
每一个顶点初始的low值和dfn值应该同样,在DFS中,咱们根据状况不断更新low的值。
假设由顶点U访问到顶点V。当从顶点V回溯到顶点U时,
若是
dfn[v] < low[u]
那么
low[u] = dfn[v]
若是顶点U还有它分支,每一个分支回溯时都进行上述操做,那么顶点low[u]就表示了不经过顶点U的父节点所能访问到的最先祖先节点。
4.3 parent数组
parent[]:下标表示顶点的编号,数组中的值表示该顶点的父顶点编号,它主要用于更新low值的时候排除父顶点,固然也能够其它的办法实现相同的功能。
4.4 一个具体的例子
如今咱们来看一个例子,模仿程序计算各个顶点的dfn值和low值。下图中蓝色实线箭头表示已访问过的路径,无箭头虚线表示未访问路径。已访问过的顶点用黄色标记,未访问的顶点用白色标记,DFS当前正在处理的顶点用绿色表示。带箭头的蓝色虚线表示DFS回溯时的返回路径。
1)
当DFS走到顶点H时,有三个分支,咱们假设咱们先走H-I,而后走H-F,最后走H-J。从H访问I时,顶点I未被访问过,因此I的dfn和low都为9。根据DFS的遍历顺序,咱们应该从顶点I继续访问。
2)
上图表示由顶点I访问顶点D,而此时发现D已被访问,当从D回溯到I时,因为
dfn[D] < dfn[I]
说明D是I的祖先顶点,因此到如今为止,顶点I不通过父顶点H能访问到的小时间戳为4。
3)
根据DFS的原理,咱们从顶点I回到顶点H,显然到目前为止顶点H能访问到的最小时间戳也是4(由于咱们到如今为止只知道能从H能够经过I访问到D),因此low[H] = 4
4)
如今咱们继续执行DFS,走H-F路径,发现顶点F已被访问且dfn[F] < dfn[H],说明F是H的祖先顶点,但此时顶点H能访问的最先时间戳是4,而F的时间戳是6,依据low值定义low[H]仍然为4。
5)
最后咱们走H-J路径,顶点J未被访问过因此 dfn[J] = 10 low[J] = 10
6)
同理,由DFS访问顶点B,dfn[J] > dfn[B],B为祖先顶点,顶点J不通过父顶点H能访问到的最先时间戳就是dfn[B],即low[J] = 2
7)
咱们从顶点J回溯到顶点H,显然到目前为止顶点H能访问到的最先时间戳就更新为2(由于咱们到如今为止知道了能从H访问到J),因此low[H] = 2
8)
根据DFS原理,咱们从H回退到顶点E(H回退到G,G回退到F,F回退到E的过程省略),所通过的顶点都会更新low值,由于这些顶点不用经过本身的父顶点就能够和顶点B相连。当回溯到顶点E时,还有未访问过的顶点,那么继续进行E-K分支的DFS。
9)
从E-K分支访问到顶点L时,顶点k和L的的dfn值和low值如图上图所示
10)
接着咱们继续回溯到了顶点D(中间过程有所省略),并更新low[D]
11)
最后,按照DFS的原理,咱们回退到顶点A,而且求出来了每一个顶点的dfn值和low值。
4.5 割点及桥的断定方法
割点:判断顶点U是否为割点,用U顶点的dnf值和它的全部的孩子顶点的low值进行比较,若是存在至少一个孩子顶点V知足low[v] >= dnf[u],就说明顶点V访问顶点U的祖先顶点,必须经过顶点U,而不存在顶点V到顶点U祖先顶点的其它路径,因此顶点U就是一个割点。对于没有孩子顶点的顶点,显然不会是割点。
桥(割边):low[v] > dnf[u] 就说明V-U是桥
须要说明的是,Tarjan算法从图的任意顶点进行DFS均可以得出割点集和割边集。
从上图的结果中咱们能够看出,顶点B,顶点E和顶点K为割点,A-B以及E-K和K-L为割边。
package datastruct; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.PrintWriter; import java.io.Reader; import java.io.StringWriter; import java.util.LinkedList; import java.util.List; import java.util.Scanner; public class CutVerEdge { /*用于标记已访问过的顶点*/ private boolean[] marked; /*三个数组的做用再也不解释*/ private int[] low; private int[] dfn; private int[] parent; /*用于标记是不是割点*/ private boolean[] isCutVer; /*存储割点集的容器*/ private List<Integer> listV; /*存储割边的容器,容器中存储的是数组,每一个数组只有两个元素,表示这个边依附的两个顶点*/ private List<int[]> listE; private UndirectedGraph ug; private int visitOrder;/*时间戳变量*/ /*定义图的边*/ public static class Edge{ /*边起始顶点*/ private final int from; /*边终结顶点*/ private final int to; public Edge(int from, int to){ this.from = from; this.to= to; } public int from(){ return this.from; } public int to(){ return this.to; } public String toString(){ return "[" + from + ", " + to +"] "; } } /*定义无向图*/ public static class UndirectedGraph{ private int vtxNum;/*顶点数量*/ private int edgeNum;/*边数量*/ /*临接表*/ private LinkedList<Edge>[] adj; /*无向图的构造函数,经过txt文件构造图,无权值*/ @SuppressWarnings("unchecked") public UndirectedGraph(Reader r){ BufferedReader br = new BufferedReader(r); Scanner scn = new Scanner(br); /*图中顶点数*/ vtxNum = scn.nextInt(); /*图中边数*/ edgeNum = scn.nextInt(); adj = (LinkedList<Edge>[])new LinkedList[vtxNum]; for(int i = 0; i < vtxNum; i++){ adj[i] = new LinkedList<Edge>(); } /*无向图,同一条边,添加两次*/ for(int i = 0; i < edgeNum; i++){ int from = scn.nextInt(); int to = scn.nextInt(); Edge e1 = new Edge(from, to); Edge e2 = new Edge(to, from); adj[from].add(e1); adj[to].add(e2); } scn.close(); } /*图的显示方法*/ @Override public String toString(){ StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); for (int i = 0; i < vtxNum; i++) { pw.printf(" %-3d: ", i); for (Edge e : adj[i]) { pw.print(e); } pw.println(); } return sw.getBuffer().toString(); } /*返回顶点个数*/ public int vtxNum(){ return vtxNum; } /*返回边的数量*/ public int edgeNum(){ return edgeNum; } } public CutVerEdge(UndirectedGraph ug){ this.ug = ug; marked = new boolean[ug.vtxNum()]; low = new int[ug.vtxNum()]; dfn = new int[ug.vtxNum()]; parent = new int[ug.vtxNum()]; isCutVer = new boolean[ug.vtxNum()]; listV = new LinkedList<Integer>(); listE = new LinkedList<int[]>(); /*调用深度优先遍历,求解各个顶点的dfn值和low值*/ dfs(); } private void dfs(){ int childTree = 0; marked[0] = true; visitOrder = 1; parent[0] = -1; for(Edge e : ug.adj[0]){ int w = e.to(); if(!marked[w]){ marked[w] = true; parent[w] = 0; dfs0(w); /*根顶点相连的边是不是桥*/ if(low[w] > dfn[0]){ listE.add(new int[]{0, w}); } childTree++; } } /*单独处理根顶点*/ if(childTree >= 2){/*根顶点是割点的条件*/ isCutVer[0] = true; } } /*除了根顶点的其它状况*/ private void dfs0(int v){ dfn[v] = low[v] = ++visitOrder; for(Edge e : ug.adj[v]){ int w = e.to(); if(!marked[w]){ marked[w] = true; parent[w] = v; dfs0(w); low[v] = Math.min(low[v], low[w]); /*判断割点*/ if(low[w] >= dfn[v]){ isCutVer[v] = true; /*判断桥*/ if(low[w] > dfn[v]){ listE.add(new int[]{v, w}); } } }else if(parent[v] != w && dfn[w] < dfn[v]){ low[v] = Math.min(low[v], dfn[w]); } } } /*返回全部割点*/ public List<Integer> allCutVer(){ for(int i = 0; i < isCutVer.length; i++){ if(isCutVer[i]){ listV.add(i); } } return listV; } /*返回全部割边*/ public List<int[]> allCutEdge(){ return listE; } /*判断顶点v是不是割点*/ public boolean isCutVer(int v){ return isCutVer[v]; } public static void main(String[] args) throws FileNotFoundException{ File path = new File(System.getProperties() .getProperty("user.dir")) .getParentFile(); File f = new File(path, "algs4-data/tinyG2.txt"); FileReader fr = new FileReader(f); UndirectedGraph ug = new UndirectedGraph(fr); System.out.println("\n-------图的邻接表示法-------"); System.out.println(ug); System.out.println("\n-------图中的割点-------"); CutVerEdge cve = new CutVerEdge(ug); for(int i : cve.allCutVer()){ System.out.println(i); } System.out.println("\n-------图中的割边-----"); for(int[] a : cve.allCutEdge()){ System.out.println(a[0]+" "+ a[1]); } } }
运行结果
------图的邻接表示法------- 0 : [0, 5] [0, 1] [0, 2] [0, 6] 1 : [1, 0] 2 : [2, 0] 3 : [3, 4] [3, 5] 4 : [4, 3] [4, 6] [4, 5] 5 : [5, 0] [5, 4] [5, 3] 6 : [6, 4] [6, 7] [6, 9] [6, 0] 7 : [7, 8] [7, 6] 8 : [8, 7] 9 : [9, 12] [9, 10] [9, 11] [9, 6] 10 : [10, 9] 11 : [11, 12] [11, 9] 12 : [12, 9] [12, 11] -------图中的割点------- 0 6 7 9 -------图中的割边----- 7 8 6 7 9 10 6 9 0 1 0 2
[1]. http://www.cnblogs.com/en-heng/p/4002658.html
[2]. http://blog.csdn.net/wtyvhreal/article/details/43530613
[3]. http://www.cppblog.com/ZAKIR/archive/2010/08/30/124869.html?opt=admin