深度优先搜索
- 深度优先搜索(depth-first search) 是对先序遍历(preorder traversal)的推广,我们从某个顶点v开始处理v,然后递归的遍历所有与v邻接顶点。如果这个过程是对一棵树进行,那么,由于E=O(V),因此树的所有顶点在总时间O(E)内都会被访问到。
- 方法:
- 当访问顶点v时候,我们标记该节点以及访问过,并且对于未被标记的所有邻接顶点递归调用深度优先搜索,
- 加上我们对无向图每条边(v,w)在邻接表中出现两次:一次(v,w),另外一次(w,v)
- 如下图过程中执行一次深度优先搜索,什么都没有操作的一次遍历,只是深度优先搜索的案例:
/*** @author liaojiamin* @Date:Created in 16:29 2021/1/8*/
public class GraphDepthFirstSearch {//递归调用深度优先搜索实现public void depthFirst(Vertex v){v.setKnow(true);for (Vertex vertex : v.getVertexList()) {if(!vertex.isKnow()){depthFirst(vertex);}}}
}
- 以上算法中,对每个顶点的known都是初始化为false,通过只对未访问过的节点进行进行递归调用,我们以此保证不进入无线循环。
- 如果图是无向的且不连通的,或者是有向的非强连通的(上一节中有对应说明)不能保证所有节点都能范文到,比如我们的v节点是从中途选取的一个节点,并不是图的入口节点。
- 次数我们搜索一个未被标记的节点,然后以这个节点为入口节点进行深度优先搜索,继续执行这两个步骤直到所有节点都被标记为止
无向图
- 当我们从无向图的任何一个节点开始深度优先搜索的时候能访问到图中的所有节点,那么这个无向图是连通无向图。
- 假设我们所处理的图都是连通的,如果不连通我们可以找出所有连通分支并将我们的算法一次运用到每个分支上即可(同上步骤)
- 作为深度优先搜索的案例,在下图中,我们从A点开始。用以下遍历流程:
- 从A节点出发,A已经被访问过,标记A为true,并递归调用dfs(B)
- dfs(B)标记B节点为true并且递归调用dfs(C)
- dfs(C)标记C节点为true并且递归调用dfs(D)
- dfs(D)遇到A,B但是A与B都已经被标记为true,因此没有递归调用可以进行,此时D-A,D-B为非必要的路径
- dfs(D)同时也是邻接C的顶点,但是C也是被标记为true,因此这里也没有递归调用,此时D-C也是非必要路径
- 于是按照递归dfs(D)返回到dfs(C)
- dfs(C)看到B是邻接节点,但是B已经是true,并且E也是C的邻接节点,因此调用dfs(E)。
- dfs(E)将E标记为true,邻接节点是A,C,都是true,因此忽略A,C,所以E-A,E-C是非必要路径,因为C-E已经是被范问过的已经存在的路径,所以剩下E-A是非必要路径,继续返回上一层dfs(C)
- dfs(C)返回dfs(B),dfs(B)忽略A,D,同理BA是已知路径,上面已经走过,所以BD是非必要路径
- 返回上一层dfs(A)忽略D,E,两个都是非必要路径
- 按以上算法递归得到,我们实际上对每个表都范问了两次,一次作为边(v,w),一次作为边(w,v),这实际上是每个邻接表遍历一次而已
深度优先生成树
- 我们用深度优先生成树(depth-first spanning tree)一图形的方式来标识上面的递归步骤,树的根节点是入口节点A,第一个被访问到的顶点,图中每一条表(v,w)都出现在树上
- 如果我们处理(v,w)的时候发现w是为未被标记,或者处理(w,v)的时候发现v是未被标记,那么我们就用树的一条边来标识他
- 如果我们处理(v,w)时候发现w已经被标记,并且处理(w,v)时候发现v已经有标记,那么我们就用虚线称为背向边(上述步骤中的非必要路径),标识这条边实际上不是这棵树的一部分,也就是我们去掉这些边也可以全部遍历所有节点。
- 如下图
- 如果图不是连通图,我们上面讨论过,需要多次执行这个算法,直到所有节点都已经被访问,每次都按照算法生成一颗树,整个集合就是深度优先生成森林(depth-first spanning forest)
双连通性
- 一个连通的无向图如果不存在被删除之后使得剩下的图有不在连通的顶点,那么这样的无向连通图就称为双连通(biconnected)的。上面案例中的图是双连通的。
- 案例: 如果节点标识计算机,边是链路,如果一台计算机宕机,则网络是不受影响的,当然这台坏的计算机除外。
- 如果存在一个图不是双连通的,那么将其删除后使得图不在连通的那些顶点叫做割点(articulation point)。这些节点在许多应用中很重要。下图中所示不是双连通的:顶点C,D都是割点。删除顶点C,使的G不在连通,删除D使得E,F单独分离开
- 深度优先所搜提供一种找出连通图中的所有割点的线性时间算法:
- 首先从图中任意一顶点开始,执行深度优先搜索,并在顶点被访问的时候编号。对每个顶点v,我们称为先序编号为Num(v)。然后对于深度优先搜索生成树
- 然后对于深度优先搜索生成树上的每个顶点v,计算编号最低的顶点,也就是在邻接表中每个节点的邻接节点中Num(v)的最小值,我们称为Low(v),该节点可以从v开始通过树的0条边(节点本身),或者多条表且有一条背向边而达到
- 如下图中,依据深度优先搜索树首先得出先序编号,然后支出在上述方法下面可以达到的最低的编号。
-
如上图,如A 顶点数据(1/1),第一个数据表示Num(v),是先序遍历的顺序字段,图中树结构的先序遍历顺序是A,B,C,D,E,F,G。
-
第二个数据表示的是Low(v),所有顶点的Low(v)开始都等于Num(v),顶点A的low(v)是本身,
-
D的邻接节点中存在A节点,所以按以上规则Low(v)是最小的Num(v)所以取A 顶点的Num(v)
-
同理C节点邻接节点是D也是1,B节点邻接C也是1
-
E节点邻接F,F背向边到D,所以F,E,都是D节点的Num(v) = 4.
-
G没有背向节点,所以是本身 7
-
我们依据以上Low定义可以指定Low(v)是满足如下规则:
- 初始值Num(v)
- 如果有背向边的话,取所有背向边(v,w)中最低的Num(w)
- 取树的所有边(v,w)中最低的Low(w)中的最小者
-
以上规则中第一条是不选取边,本身就是最小,第二条规则是不选取树的边而是选取一条背向边,第三种规则我们可以用递归调用简单的描述,由于我们需要对v的所有子节点算出Low值后才能得到最小Low(v),因此这是一个后续遍历。对于任意的(v,w),只要检查Num(v)和Num(w)就知道他是树的一条边还是一条背向边,(因为按树生成算法背向边总数从大的 num值 到小的num值)。因此Low(v)容易计算,我们只需要扫描v的邻接节点,然后记住最小值。时间复杂度在O(E+V)。
-
接着我们需要做的就是利用以上信息找到所有割点。
- 根是割点的时候只有在根节点有大于一个儿子节点的时候成立,如果有2个儿子节点,删除根则使得不同子树上节点不连通
- 对于任何其他顶点v,他是割点的虫咬条件是,当他的某个儿子节点w使得Low(w) >= Num(v),满足
- 还是上图中的案例,C和D是割点,D有一个儿子节点E,且Lov(E) >= Num(D),两个都是4
- 同理C也是割点Low(G) >= Num©
算法实现
- 最后我们给出以下代码实现该算法,我们需要在之前的Vertex图节点中修改几个属性,num, low,沿用之前的known,path(父节点),用类变了counter做统计为先序遍历num编号,如下:
/*** @author liaojiamin* @Date:Created in 16:29 2021/1/8*/
public class GraphDepthFirstSearch {//假设图基本数据结构已经被读入邻接表中private static final List<Vertex_v1> vertices = new ArrayList<>();private Integer counter = 0;/*** 深度优先递归模板* */public void depthFirst(Vertex_v1 v){v.setKnow(true);for (Vertex_v1 vertex : v.getVertexList()) {if(!vertex.isKnow()){depthFirst(vertex);}}}/*** 深度优先递归赋值nnum* */public void assignNum(Vertex_v1 v){v.setNum(counter++);v.setKnow(true);for (Vertex_v1 vertex_v1 : v.getVertexList()) {if(!vertex_v1.isKnow()){vertex_v1.setPath(v);assignNum(vertex_v1);}}}/*** 同样的算法,深度优先对Low赋值* */public void assignLow(Vertex_v1 v){v.setLow(v.getNum());for (Vertex_v1 w : v.getVertexList()) {//w > v 标识w是v的子节点if(w.getNum() > v.getNum()){assignLow(w);//割点规则if(w.getLow() > v.getNum()){System.out.println(v.getNum() + " is an articulation point");}v.setLow(Math.min(v.getLow(), w.getLow()));}else if(v.getPath().compareTo(w) != 0){//此处不能写成: v.setLow(Math.min(v.getLow(), w.getLow()));因为此种情况是在最后二次访问边路径的时候得到,可能w的Low已经被修改成更小的值v.setLow(Math.min(v.getLow(), w.getNum()));}}}/*** 还是用深度优先搜索,结合以上两种规则,值进行一次遍历得到* */public void findArt(Vertex_v1 v){v.setKnow(true);v.setNum(counter++);v.setLow(v.getNum());for (Vertex_v1 w : v.getVertexList()) {if(!w.isKnow()){w.setPath(v);findArt(w);if(w.getLow() >= v.getNum()){System.out.println(v.getNum() + " is an articulation point");}v.setLow(Math.min(v.getLow(), w.getLow()));}else if(v.getPath().compareTo(w) != 0){//此处不能写成: v.setLow(Math.min(v.getLow(), w.getLow()));因为此种情况是在最后二次范文边路径的时候得到,可能w的Low已经被修改成更小的值v.setLow(Math.min(v.getLow(), w.getNum()));}}}}
- 以上算法中assignNum通过执行一次先序遍历计算Num,然后在后续遍历计算Low,第三次遍历可以用来检查哪些顶点满足割点的标准,但是执行了三次遍历比较浪费,我们将两个方法合成为findArt,在同一个递归中完成,得出算法。
上一篇:数据结构与算法–图论-最短路径算法应用
下一篇:数据结构与算法–贪婪算法:模拟调度问题