图论算法——环和有向无环图

图论算法——环和有向无环图环和有向无环图在有向图相关的实际应用中 有向环特别重要 一幅图可能含有大量的环 通常 我们一般只关注它们是否存在 调度问题给定一组任务并安排它们的执行顺序 限制条件为这些任务的执行方法 起始时间以及任务的消耗等 最重要的一种限制条件叫做优先级限制 它指定了哪些任务必须在哪些任务之前完成 不同类型的限制条件会产生不同难度的调度问题 以一个正在安排课程的大学生为例 有些课程是其他课程的先导课程

引言

在有向图相关的实际应用中,有向环特别重要。一幅图可能含有大量的环,通常,我们一般只关注它们是否存在。

有关图的概念可参考博文数据结构之图的概述

在学习环之前,我们一起来学习下调度问题。

调度问题

给定一组任务并安排它们的执行顺序,限制条件为这些任务的执行方法、起始时间以及任务的消耗等。最重要的一种限制条件叫做优先级限制,它指定了哪些任务必须在哪些任务之前完成。不同类型的限制条件会产生不同难度的调度问题。

在这里插入图片描述以一个正在安排课程的大学生为例,有些课程是其他课程的先导课程。如上图所示。

比如要学习机器学习(Machine Learning),得先学习人工智能(Artificial Intelligence)。

再假设该同学一次只能选修一门课程,就会遇到下面的问题:

优先级限制下的调度问题:在满足限制条件的情况下如何安排并完成所有任务。我们可以画出一张有向图,顶点对应任务(通过数组索引来表示课程),有向边对应优先级顺序。

在这里插入图片描述

上图9代表人工智能,11代表机器学习。

而在有向图中,优先级限制下的调度问题等价于拓扑排序:

拓扑排序:给定一副有向图,将所有的顶点排序,使得所有的有向边均从排在前面的顶点指向排在后面的顶点。

在这里插入图片描述
上图为示例的拓扑排序,所有的边都是向下的。按照这个顺序,该同学可以在满足先导课程限制的条件下选修完所有课程。

有向图中的环

如果任务x必须在任务y之前完成:x→y,而y→z,同时z→x。那么就会形成一个环:x→y→z→x。如果一个有优先级限制的问题中存在有向环,那么这个问题肯定是无解的。因此我们需要首先进行有向环检测。

有向环检测:检测给定的有向图是否包含环,若有环,通常只需找出一个即可。

有向无环图(DAG):不含环的有向图

可以转换为检查一副有向图是否为有向无环图。

在这里插入图片描述

对课程图进行一些修改,增加一些环如上所示。

寻找环利用了DFS方法,维护一个递归调用期间已访问的顶点的栈,若(在递归调用期间,通过判断onStack标记数组)两次访问了某个顶点,则说明有环;若DFS递归调用完毕,说明无环

该递归调用期间只是递归dfs调用它的邻接顶点期间(如果有邻接顶点,它的邻接顶点也会执行同样的过程,所以会越来越深),一旦递归完它的所有邻接顶点,会把该顶点从onStack数组中移除

我们暂且关注0-5六个顶点。执行图解如下:

dfs(1),将1标记为已访问,并将1的调用栈标记置为true,因为1无邻接点,dfs(1)调用完毕,并将1的调用栈标记置为false(在图中用空白表示false),回退到dfs(0)

在这里插入图片描述

递归调用0的下一个邻接点5,将5的上一节点置为0

递归调用dfs(5)

5标记为已访问,并将5的调用栈标记置为true

在这里插入图片描述递归的访问5的邻接点4。观察到4没有访问过,将其上一节点置为5,然后调用

dfs(4),将4标记为已访问,并将4的调用栈标记置为true,递归的访问4的邻接点2,观察到2没有访问过

在这里插入图片描述2上一节点置为4,递归调用
dfs(2)
2标记为已访问,并将2的调用栈标记置为true,递归的访问2的邻接点0观察到0已经访问过,且0在调用栈中,因此发现了环。 此时已经得出结果可以返回了,但别急,我们将环的路径存入调用栈中,方便后面输出。




在这里插入图片描述
cycle环栈输入顺序为:2,4,5,0

检测有向图是否有环代码

package com.algorithms.graph; import com.algorithms.stack.Stack; / * @author yjw * @date 2019/5/20/020 */ public class DirectedCycle { 
    private boolean[] marked; private int[] edgeTo; / * 有向环中的所有顶点(如果存在) */ private Stack<Integer> cycle; / * 保存递归调用期间栈上的所有顶点 */ private boolean[] onStack; public DirectedCycle(DiGraph g) { 
    marked = new boolean[g.vertexNum()]; edgeTo = new int[g.vertexNum()]; onStack = new boolean[g.vertexNum()]; for (int v = 0; v < g.vertexNum(); v++) { 
    if (!marked[v]) { 
    dfs(g, v); } } } private void dfs(DiGraph g, int v) { 
    marked[v] = true; onStack[v] = true; for (int w : g.adj(v)) { 
    if (hasCycle()) { 
    //有环直接退出 return; } else if (!marked[w]) { 
    edgeTo[w] = v; dfs(g, w); } else if (onStack[w]) { 
    //如果w已经在栈中,说明再一次访问到了w,因此此时发现了环 cycle = new Stack<>(); for (int x = v; x != w; x = edgeTo[x]) { 
    cycle.push(x); } cycle.push(w); cycle.push(v); } } onStack[v] = false; } public boolean hasCycle() { 
    return cycle != null; } public Iterable<Integer> cycle(int v) { 
    return cycle; } public static void main(String[] args) { 
    DiGraph g = new DiGraph(13); g.addDiEdges(0, 5, 1); g.addDiEdges(2, 3, 0); g.addDiEdges(3, 2, 5); g.addDiEdges(4, 3, 2); g.addDiEdges(5, 4); g.addDiEdges(6, 0, 4, 9); g.addDiEdges(7, 6, 8); g.addDiEdges(8, 7, 9); g.addDiEdges(9, 10, 11); g.addDiEdges(10, 12); g.addDiEdges(11, 4, 12); g.addDiEdges(12, 9); //System.out.println(g); DirectedCycle dc = new DirectedCycle(g); if (dc.hasCycle()) { 
    for (int w : dc.cycle) { 
    System.out.print(w + "->"); } } } } 

顶点的深度优先顺序

图的深度优先搜索只会访问每个顶点一次,如果将dfs(int v)的参数顶点v保存在一个数据结构中,
遍历这个数据结构实际上就能访问图中的所有顶点。

遍历的顺序取决于该数据结构的性质(栈的先进后出、队列的先进先出),以及是在递归调用之前还是之后存入该数据结构。

常见的的3种遍历顺序如下:

  • 前序:在递归调用之前将顶点加入队列
  • 后序:在递归调用之后将顶点加入队列
  • 逆后序:在递归调用之后将顶点压入

在这里插入图片描述我们以上面这个图为例,对该图进行3种遍历:

在这里插入图片描述prepost是队列,reversePost是栈,红色元素为最近插入的元素。从左到右,可以看到队列是先进先出,而栈是先进后出。

分析下执行过程,首先调用dfs(0),0的邻接点为{2},在递归调用dfs(2)之前,将0入前序队列;
调用dfs(2)2的邻接点为{0,1,3},因为0刚才已经访问过,不会递归调用0,因此会继续递归调用1(将它入前序队列),它没有邻接点,递归dfs调用1的邻接点完毕,将1入后序队列;

然后继续调用2的下一个邻接点3,而3又会导致dfs(4)dfs(5),该两个顶点都没有邻接点,因此很快就可以返回,3的所有邻接点递归调用完毕,到了3 done。后面也是一样,20依次调用完毕。

有向图的前序、后序遍历代码

package com.algorithms.graph; import com.algorithms.queue.Queue; import com.algorithms.stack.Stack; / * 顶点的深度优先顺序 * @author yjw * @date 2019/5/21/021 */ public class DepthFirstOrder { 
    private boolean[] marked; / * 所有顶点的前序序列 */ private Queue<Integer> pre; / * 所有顶点的后序序列 */ private Queue<Integer> post; / * 所有顶点的逆后序序列 */ private Stack<Integer> reversePost; public DepthFirstOrder(DiGraph g) { 
    pre = new Queue<>(); post = new Queue<>(); reversePost = new Stack<>(); marked = new boolean[g.vertexNum()]; for (int v = 0; v < g.vertexNum(); v++) { 
    if (!marked[v]) { 
    dfs(g,v); } } } private void dfs(DiGraph g, int v) { 
    marked[v] = true; //前序:递归调用dfs之前将顶点加入队列 pre.enqueue(v); for (int w : g.adj(v)) { 
    if (!marked[w]) { 
    //递归调用dfs dfs(g, w); } } //遍历完所有的邻接点后(递归调用dfs之后) //后序:递归调用dfs之后将顶点加入队列 post.enqueue(v); //逆后序:在递归调用dfs之后将顶点压入栈 reversePost.push(v); } public Iterable<Integer> pre() { 
    return pre; } public Iterable<Integer> post() { 
    return post; } public Iterable<Integer> reversePost() { 
    return reversePost; } public static void main(String[] args) { 
    DiGraph g = new DiGraph(6); g.addEdge(0,2,2,0,2,1,2,3,3,2,3,4,3,5); DepthFirstOrder dfo = new DepthFirstOrder(g); for (Integer v: dfo.pre()){ 
    System.out.print(v + " "); } System.out.println(); for (Integer v: dfo.post()){ 
    System.out.print(v + " "); } System.out.println(); for (Integer v: dfo.reversePost()){ 
    System.out.print(v + " "); } System.out.println(); / * 输出: * 0 2 1 3 4 5 * 1 4 5 3 2 0 * 0 2 3 5 4 1 */ } } 

拓扑排序

回顾一下拓扑排序的描述

在这里插入图片描述

拓扑排序:给定一副有向图,将所有的顶点排序,使得所有的有向边均从排在前面的顶点指向排在后面的顶点。

在这里插入图片描述
上图为示例的拓扑排序,所有的边都是向下的。按照这个顺序,可以在满足先导课程限制的条件下选修完所有课程。

一副有向无环图的拓扑排序即为所有顶点的逆后序排列

package com.algorithms.graph; / * 有向无环图的拓扑排序即为所有顶点的逆后序排列 * * @author yjw * @date 2019/5/21/021 */ public class Topological { 
    //顶点的拓扑顺序 private Iterable<Integer> order; public Topological(DiGraph g) { 
    //首先检测是否有环 DirectedCycle cycle = new DirectedCycle(g); if (!cycle.hasCycle()) { 
    DepthFirstOrder dfs = new DepthFirstOrder(g); order = dfs.reversePost(); } } public Iterable<Integer> order() { 
    return order; } / * 是否为有向无环图 * * @return */ public boolean isDAG() { 
    return order != null; } public static void main(String[] args) { 
    DiGraph g = new DiGraph(13); g.addDiEdges(0, 1, 5, 6); g.addDiEdges(2, 0, 3); g.addDiEdges(3, 5); g.addDiEdges(5, 4); g.addDiEdges(6, 4, 9); g.addDiEdges(7, 6); g.addDiEdges(8, 7); g.addDiEdges(9, 10, 11,12); g.addDiEdges(11, 12); Topological tp = new Topological(g); if (tp.isDAG()) { 
    for (Integer v : tp.order) { 
    System.out.print(v + " "); } } } } 
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/199189.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月26日 下午1:38
下一篇 2026年3月26日 下午1:39


相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号