数据结构&算法拾遗(5)-- 有向无环图(DAG)与拓扑排序(调度)

在有向图中,边是单向的;每条边所连接的两个顶点都是一个有序对,它们的邻接性是单向的。有向图的应用方向很多,如网络、任务调度条件或者是电话关系等都是天然的有向图。今天主要介绍有向无环图(DAG)与拓扑排序之间的关系。

1. 术语

拓扑排序:对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序

简单的说,将调度任务想象为DAG图,每个顶点之间有先后次序,如何使用一个线性结构来执行这些调度任务,并且不违背它们之间的先后次序,解决这个问题,就可以使用有向无环图的拓扑排序算法。

2.有向图的数据类型

首先老规矩,先介绍有向图的数据类型及其代码实现,有向图的API如下图所示:


有向图的api
返回值类型Digraph类方法说明
 Digraph(int V)创建一幅含有V个顶点但没有边的有向图
 Digraph(In in)从输入流in 中读取一幅有向图
intV()顶点总数
intE()边的总数
voidaddEdge(int v, int w)向有向图中添加一条边v-->w
Iterable<Integer>adj(int v)由 v指出的边所连接的所有顶点
Digraphreverse()该图的反向图
StringtoString()对象的字符串表示

实现代码如下:

package algorithms.digraph;

import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;


/**
 * Created by xudong on 2019/8/16.
 */
public class Digraph {
    private final int V;
    private int E;
    private Bag<Integer>[] adj;
    public Digraph(int V){
        this.V = V;
        this.E = 0;
        adj = (Bag<Integer>[]) new Bag[V];
        for (int v = 0; v < V; v++){
            adj[v] = new Bag<Integer>();
        }
    }

    public Digraph(In in){
        this(in.readInt());     //读取V并将图初始化
        int E = in.readInt();   //读取E
        for( int i =0; i < E; i++){
            //添加一条边
            int v = in.readInt(); //读取一个顶点
            int w = in.readInt(); //读取另一个顶点
            addEdge(v, w); // 添加一条连接他们的边
        }
    }

    public int V(){
        return V;
    }

    public int E(){
        return E;
    }

    public void addEdge(int v, int w){
        adj[v].add(w);
        E++;
    }

    public Iterable<Integer> adj(int v){
        return adj[v];
    }

    // 返回一个反向的 有向图
    public Digraph reverse(){
        Digraph R = new Digraph(V);
        for(int v = 0; v < V; v++ ){
            for(int w : adj(v))
                R.addEdge(w, v);
        }
        return R;
    }

    //图的邻接表的字符串表示
    public String toString(){
        StringBuffer s = new StringBuffer();
        s.append(V + "vertices, " + E + " edges\n");
        for( int v = 0; v < V; v++){
            s.append(v + ": ");
            for( int w : this.adj(v) ){
                s.append(w + " ");
            }
            s.append("\n");
        }
        return s.toString();
    }



}

 输入格式如下图所示:

13
22
4 2
2 3
3 2
6 0
0 1
2 0
11 12
12 9
9 10
9 11
8 9
10 12
11 4
4 3
3 5
7 8
8 7
5 4
0 5
6 4
6 9
7 6

3. 有向图解决的问题

3.1 可达性问题

对于有向图中的可达性问题(“是否存在一条从S到达给定顶点V的有向图”  或者  “是否存在一条从集合中的任意顶点到达给定顶点V的有向路径”)可以使用深度优先搜索来解决这些问题(它对每个顶点调用递归方法dfs(), 以标记遇到的任意顶点)代码如下所示:

package algorithms.digraph;

import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.StdOut;

/**
 * 有向图的可达性API
 * 多点可达性的一个重要应用是 典型的内存管理系统中,在java的内存实现中,一个顶点表示一个对象,一条边表示一个对象对另一
 * 个的引用,标记-清除 的垃圾回收策略会为每一个对象保留一个标志位作垃圾回收使用,周期性的运行一个类似DirectedDFS的有向图
 * 可达性算法来标记所有可以被访问到的对象,然后清理所有未被标记的对象
 * DepthFirstPaths 和 BreadthFirstPaths 也是有向图的重要算法,他们主要解决以下问题:
 * 单点有向路径:“从s到给定目的定点v是否有一条有向路径”
 * 单点最短有向路径: “从s到给定目的顶点是否有一条有向路径, 如果有,找出最短路径”
 * Created by xudong on 2019/8/17.
 */
public class DirectedDFS {
    private boolean[] marked;

    // 在 有向图 G 中, 端点s 开始 的可达路径有哪些
    public DirectedDFS( Digraph G, int s){
        marked = new boolean[G.V()];
        dfs(G, s);
    }

    // 多个起始点的情况
    public DirectedDFS(Digraph G, Iterable<Integer> sources){
        marked = new boolean[G.V()];
        for(int s: sources){
            if(!marked[s])
                dfs(G, s);
        }
    }

    private void dfs(Digraph G, int v){
        marked[v] = true;
        for(int w: G.adj(v)){
            if(!marked[w])
                dfs(G, w);
        }
    }

    public boolean marked(int v){
        return marked[v];
    }

    public static void main(String[] args) {
        Digraph G = new Digraph(new In("tinyDG.txt"));
        Bag<Integer> sources = new Bag<Integer>();
        for(int i = 0; i < args.length; i++){
            sources.add(Integer.parseInt(args[i]));
        }

        DirectedDFS reachable = new DirectedDFS(G, sources);
        for(int v = 0; v < G.V(); v++){
            if(reachable.marked(v))
                StdOut.print(v + " ");
        }
        System.out.println();
    }

}

3.2 环、有向无环图与调度问题

有向环:在与有向图相关的应用中,有向环特别的重要,在实际应用中,我们可能会关注其是否在图中存在。

调度问题:一种应用广泛的模型是指定一组任务并且安排它们的执行顺序,限制条件是这些任务的执行方法和起始时间,可能还包括任务的时耗或其他资源,其中最重要的一种限制条件叫做优先级限制,它指明了哪些任务必须在哪些任务之前完成。对于这类问题,我们都可以立马画一张有向图,其中顶点对应任务,有向边对应优先级顺序,简明起见优先级限制下的调度问题等价于下面这个基本问题(拓扑排序):

拓扑排序:给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素。在解决这个问题前,首先要确定图是无环的。

检测有向图是否有环的代码如下:

package algorithms.digraph;

import edu.princeton.cs.algs4.Stack;

/**
 * Created by xudong on 2019/8/17.
 * 检测有向图中是否有 环的存在, 保证该图 为有向无环图(DAG), 因为环的存在在有向图中常常指代死锁问题,
 * 所以建模需要有向无环图
 */
public class DirectedCycle {
    private boolean[] marked;
    private int[] edgeTo;
    private Stack<Integer> cycle; //有向环中所有顶点(如果存在)
    private boolean[] onStack; //递归调用的栈上的所有顶点

    public DirectedCycle(Digraph G){
        onStack = new boolean[G.V()];
        edgeTo = new int[G.V()];
        marked = new boolean[G.V()];
        for(int v = 0; v < G.V(); v++){
            if(!marked[v]){
                dfs(G, v);
            }
        }
    }

    private void dfs(Digraph G, int v){
        onStack[v] = true;
        marked[v] = true;
        for(int w: G.adj(v)){
            if(this.hasCycle()) return;
            else if(!marked[w]){
                edgeTo[w] = v;  // 顶点的上一个顶点
                dfs(G, w);
            }
            else if(onStack[w]){
                cycle = new Stack<Integer>();
                for(int x = v; x != w; x = edgeTo[x]){
                    cycle.push(x);
                }
                cycle.push(w);
                cycle.push(v);   // w的上一个是 v
            }
        }

        onStack[v] = false;   //深度优先结束后将 v 出栈
    }

    public Iterable<Integer> cycle(){
        return cycle;
    }

    public boolean hasCycle(){
        return cycle != null;
    }

}

以上代码基于这样一个事实:在深度优先搜索中,由系统维护的递归调用的栈表示的正式当前正在遍历的有向路径。一旦我们找到了一条有向边v-->w且w已经存在于栈中,就找到了一个环,因为栈表示的是一条由w到v的有向路径,而v-->w正好补全了这个环。

 

顶点的深度优先次序排序与拓扑排序:

优先级限制下的调度问题等价于计算有向无环图中的所有顶点的拓扑顺序。

package algorithms.digraph;


import edu.princeton.cs.algs4.Graph;
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.Stack;

/**
 *  计算有向图中顶点的深度优先次序(前序、后序和逆后序)
 *  前序: 在递归调用之前将顶点加入队列
 *  后序:在递归调用之后将顶点加入队列
 *  逆后序:在递归调用之后将顶点加入栈
 */
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<Integer>();
        post = new Queue<Integer>();
        reversePost = new Stack<Integer>();
        marked = new boolean[G.V()];

        for(int v = 0; v < G.V(); v++){
            if(!marked[v]) dfs(G, v);
        }
    }


    private void dfs(Digraph G, int v){
        pre.enqueue(v);
        marked[v] = true;
        for(int w : G.adj(v)){
            if(!marked[v]){
                dfs(G, w);
            }
        }
        post.enqueue(v);
        reversePost.push(v);
    }

    public Iterable<Integer> pre(){
        return pre;
    }

    public Iterable<Integer> post(){
        return post;
    }

    public Iterable<Integer> reversePost(){
        return reversePost;
    }
}

 

 

package algorithms.digraph;

import edu.princeton.cs.algs4.SymbolDigraph;

public class Topological {
    private Iterable<Integer> order;   //拓扑的顺序

    public Topological(Digraph G){
        DirectedCycle cycleFinder = new DirectedCycle(G);
        if(!cycleFinder.hasCycle()){
            DepthFirstOrder dfs = new DepthFirstOrder(G);
            order = dfs.reversePost();
        }
    }
    //拓扑有序的所有顶点
    public Iterable<Integer> order(){
        return order;
    }

    //G是有向无环图吗
    public boolean isDAG(){
        return order != null;
    }

    public static void main(String[] args) {

    }
}

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值