2

Algorithms Fourth Edition
Written By Robert Sedgewick & Kevin Wayne
Translated By 谢路云
Chapter 4 Section 2 有向图


有向图的建立

有向图API

有向图API

  • 修改了方法void addEdge(v, w) 添加的边为单向的, 从v到w

  • 修改了方法adj(v) 返回的是从v指出去的边连接的顶点

  • 增加了方法Digraph reverse() 创建了一个反向边的图副本,因为有时候我们需要的是指向特定顶点的其他顶点

Digraph 代码

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 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;
    }
}

可达性(深度优先搜索)

可达性API

可达性API

  • 增加了构造函数DirectedDFS(G,sources) 一个source生成一棵树,n个sources生成好n棵树;也有可能是一棵树,只是先找到了孙子,没法通过孙子找爸爸和爷爷,后来输入了爷爷,找到了爸爸,连到了孙子,形成了一棵大树。

DirectedDFS 代码

基本和有向图没区别

public class DirectedDFS {
    private boolean[] marked;

    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];
    }
}

多点可达性应用

  • 标记-清除的垃圾箱

  • 寻路

环和有向无环图

  • 调度问题

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

有向无环图 Directed Acyclic Graph (DAG)

有向环API

DirectedCycle

  • 方法boolean hasCycle() 有向环检测

DirectedCycle 代码

public class DirectedCycle {
    private boolean[] marked;
    private int[] edgeTo;
    private Stack<Integer> cycle; // vertices on a cycle (if one exists)
    private boolean[] onStack; // vertices on recursive call stack

    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; //这个变量是神来之笔。因为有好几棵树,但我只要查我所在的这棵树。所以在递归的时候一路把这棵树标成true,在返回之前再标回false。为下一棵树可以循环再利用做准备。
        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);//v压了两次,第一次作为箭头终点,第二次作为箭头起点
            }
        onStack[v] = false;
    }

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

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

拓扑排序API

拓扑排序API

  • 前提

    • 当且仅当一幅图是有向无环图时,才能进行拓扑排序

  • 一种拓扑排序的实现方式

    • 深度优先搜索DFS

  • 顶点排列顺序

    • 前序 递归调用之前将顶点加入队列

    • 后续 递归调用之后将顶点加入队列

    • 逆后续 递归调用之后将顶点压入栈

DepthFirstOrder 代码

顶点排列顺序

就是记录顶点的位置和方式不一样

public class DepthFirstOrder {
    private boolean[] marked;
    private Queue<Integer> pre; // vertices in preorder
    private Queue<Integer> post; // vertices in postorder
    private Stack<Integer> reversePost; // vertices in reverse postorder

    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[w]) 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;
    }
}

Topological 代码

  • 复杂度:

    • 时间: V+E

(为什么我觉得Topological这个类没干什么实事,DirectedCycle检测是否是有向无环图,DepthFirstOrder进行了排序。。。Topological感觉就封装了一下)

public class Topological {
    private Iterable<Integer> order; // topological 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;
    }

    public boolean isDAG() {
        return order == null;
    }
}

强联通性

  • 定义

    • w和v是相互可达的,则称它们为强连通的(Strongly Connected)
      (v到w有一条路径,则w是从v可达的)

    • 如果有向图G的每两个顶点都强连通,称G是一个强连通图。

    • 有向图的极大强连通子图,称为强连通分量(Strongly Connected Components/Strong Components)

  • 两个顶点是强连通的,当且仅当它们在同一个有向环中

  • 性质

    • 自反性

    • 对称性

    • 传递性

强连通分量API

Strongly Connected Components
强连通分量API

Kosaraju算法

算法步骤很简单,但是原理很玄幻。。。只好特地拎出来记录+证明一下

算法步骤

第一步:在逆图GR上运行DFS,将顶点按照逆后序(reversePost)方式压入栈Stack s中
(显然,这个过程作用在有向无环图DAG上得到的就是一个拓扑排序;作用在非DAG上得到的是一个伪拓扑排序)

第二步:在原图G上按第一步s.pop()的编号顺序进行DFS。
(栈的特点是FILO,先进后出)

  • 算法基于CC(ConnectedComponents)

    • CC按顺序0~G.V()

    • SCC按顺序s.pop()

  • SCC顺序怎么得到?

    • 类DepthFirstOrder

  • 两次DFS

图示两次DFS
直观理解2

KosarajuSCC 代码

  • 复杂度:所需时间与空间与V+E成正比,连通性查询为常数时间

    • 时间: 处理有向图的反向图,+ 2次DFS 这三步所需的时间都与V+E成正比

    • 空间: 反向复制一副有向图的空间与V+E成正比

    • 连通性查询: 数组id[]查询,常数时间

public class KosarajuSCC {
    private boolean[] marked; // reached vertices
    private int[] id; // component identifiers
    private int count; // number of strong components

    public KosarajuSCC(Digraph G) {
        marked = new boolean[G.V()];
        id = new int[G.V()];
        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) {
        return id[v] == id[w];
    }

    public int id(int v) {
        return id[v];
    }

    public int count() { //复制书上的,感觉返回的不是强连通的个数N,因为从零开始计数,所以返回的是N-1
        return count;
    }
}

模糊猜想

原图
直观理解

pop顺序   →→→→     →→→→→→→→→
7 → 8 6 → 9 → 11 10 → 12 0 → 5 → 4 → 3 → 2 1
 ←     ←←←←←←   ←←←←←←←      

由于排版问题只画了部分的有向边

可见是否是环,应该从1开始排除,因为1是图中最深的结点,最有可能只有指向1的,而没有从1指出去的。通过开始一个一个排查,应该是一种比较好的想法。

算法正确性证明

证明的目标,就是最后一步 --- 每一颗搜索树代表的就是一个强连通分量

首先 最后一步是在原图G中通过s找到其他顶点v的,即从s→v是可达的。
那么我们需要证明,原图G中v→s也是可达的。

等价于已知:在逆图GR中存在有向路径v→s
那么要证明:逆图GR中从s→v是可达的

而之所以DFS(s)能够在DFS(v)之前被调用,是因为在对G获取ReversePost-Order序列时,s出现在v之前,这也就意味着,v是在s之前加入该序列的(因为该序列使用栈作为数据结构,先加入的反而会在序列的后面)。

因此根据DFS调用的递归性质,DFS(v)应该在DFS(s)之前返回,而有当时在逆图GR中两种情形满足该条件:
DFS(v) START -> DFS(v) END -> DFS(s) START -> DFS(s) END
DFS(s) START -> DFS(v) START -> DFS(v) END -> DFS(s) END

第一种情形下,调用DFS(v)却没能在它返回前递归调用DFS(s),与在逆图GR中存在有向路径v→s相矛盾的,因此不可取。
故情形二为唯一符合逻辑的调用过程。而根据DFS(s) START -> DFS(v) START可以推导出在逆图GR中存在有向路径s→v。

所以从s到v以及v到s都有路径可达,证明完毕。

顶点对的可达性

顶点对的四种情况

w⇆v 强连通
w→v
w←v
w⇎v

问题:如何写一个算法判断从w到v是可达的吗?
有向图的可达性问题 和 连通性 有很大区别。 因为连通性是双向的,可达性是单向的。

目前的解决办法:传递闭包(其实就是一个类似于邻接矩阵的矩阵,用来记录是否连通)
传递闭包

传递闭包API

传递闭包2

TransitiveClosure 代码

给每个顶点创立了一棵树,在每棵树里有数组marked[V],标记是否连通。

  • 复杂度

    • 空间:V*V

    • 时间:V*(V+E)

public class TransitiveClosure {
    private DirectedDFS[] all;

    TransitiveClosure(Digraph G) {
        all = new DirectedDFS[G.V()];
        for (int v = 0; v < G.V(); v++)
            all[v] = new DirectedDFS(G, v);
    }

    boolean reachable(int v, int w) {
        return all[v].marked(w);
    }
}

lindsay_bubble
26 声望11 粉丝