有向图:学习总结

有向图

相较于无向图,有向图的边是带有方向性的。v→w,那么v的邻接点链表中会有w,但是w的邻接点链表不存在v。因此就邻接表来说,体现有向性是通过链表中的节点有无来实现的。

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 i = 0; i < V; i++) {
            adj[V] = new Bag<>();
        }
    }

    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 i = 0; i < V; i++) {
            for (int w : adj(i)) {
                // 通过相反的方式添加边
                R.addEdge(w, i);
            }
        }
        return R;
    }

}

有向图的可达性

public class DirectedDFS {
    private boolean[] marked;

    public DirectedDFS(Digraph G, int s) {
        marked = new boolean[G.V()];
        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 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<>();
                // 构建这个环,依次获取DFS中顶点的上级顶点
                for (int x = v; x != w; x = edgeTo[x]) {
                    cycle.push(x);
                }
                cycle.push(w);
                cycle.push(v);
            }
        }
        onStack[v] = false;
    }

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

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

拓扑排序要点

  • 在进行DFS时,将参数顶点保存在一个数据结构中,遍历这个数据结构实际上就能访问图中的所有顶点。
  • 排序顺序(注意数据结构的使用):
    • 前序:在递归调用之前将顶点加入队列
    • 后序:在递归调用之后将顶点加入队列
    • 逆后序:在递归调用之后将顶点压入

三种排序方式的数据结构 – 可类比树的遍历

public class DepthFirstOrder {
    private boolean[] marked;
    private Queue<Integer> pre;
    private Queue<Integer> post;
    private Stack<Integer> reversePost;

    public DepthFirstOrder(Digraph G) {
        // 用原生JDK实现
        pre = new LinkedList<>();
        post = new LinkedList<>();
        reversePost = new Stack<>();
        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.offer(v);
        
        // DFS 过程
        marked[v] = true;
        for (int w : G.adj(v)) 
            if (!marked[w]) dfs(G, w);
        
        // 逆序
        post.offer(v);
        // 逆后序
        reversePost.push(v);
    }

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

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

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

拓扑算法实现:

public class Topological {

    // 顶点的拓扑排序
    private  Iterable<Integer> order;

    public Topological(Digraph G){
        // 检测有无环
        DirectedCycle cycleDetector = new DirectedCycle(G);
        if(!cycleDetector.hasCycle()){
            // 无环则返回节点序列
            DepthFirstOrder dfs = new DepthFirstOrder(G);
            // 拓扑顺序为逆后序 -- 入度小的在前面
            order = dfs.reversePost();
        }
    }

    public Iterable<Integer> order() {
        return order;
    }
    public boolean isDAG(){
        return order != null;
    }

}

有向图的强连通性

  • 定义:有向图的两个顶点v和w,如果是相互可达的,那么称他们强连通

  • 推论:顶点强连通 当且仅当 它们都在一个普通的有向环

  • 满足离散数学推论中的:自反性、对称性、传递性

  • 定义是基于点而不是边

强连通分量的计算算法 – Kosaraju算法:

这个算法的理解比较恶心,首先需要理解这个算法的目的:

  • 保证在遍历强连通分量时,有一个其他连通分量的顶点,在这个强连通分量的前面
  • 这样检验完一个强连通分量后,可以进入下一个强连通分量
  • 强连通分量之间是可达,但不构成回路的
  • 综上所述,要进行统计之前,首先要构建一个序列,将各个连通分量隔离开(内部顺序不定)

接下来是证明通过求反向图的逆后序,可以保证两个连通分量之间是隔离开的

可以看到,通过构建反向图的逆后序排列,成功将各个强连通分量隔离开,并保证可以从一个强连通分量进入下一个强连通分量

image-20200616172859240

public class KosarajuSCC {
    private boolean[] marked;
    private int[] id;
    private int count;

    public KosarajuSCC(Digraph G) {
        marked = new boolean[G.V()];
        id = new int[G.V()];

        // 核心部分 -- 其实完成了两次DFS,在创建order的时候还进行了依次dfs
        DepthFirstOrder order = new DepthFirstOrder(G.reverse());

        for (int s : order.reversePost()) {
            if (!marked[s]) {
                dfs(G, s);
                count++;
            }
        }
    }

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

    public boolean stronglyConnected(int v, int w){
        // 如果所属的连通分量id相同,则证明v和w相互连通
        return id[v] == id[w];
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章