4.2 有向图

4.2.1介绍

在有向图中,边是单向的.实际上组合性的结构对算法有深远影响,使得有向图和无向图之间的处理大有不同.
顶点出度: 由该顶点指出边的总数.
顶点入度:指向该顶点的边的总数.
一条有向边的第一个顶点叫头,第二个顶点称为尾.(v → w)
一幅有向图的顶点可能存在4种关系:
1.没有边相连接
2.存在v → w
3.存在w → v
4.存在 双向 v ↔ w

4.2.2有向图的数据类型Digraph
package algorithm.algorithmbook.graph;
/**
 * description: 有向图的数据接口
 *
 * @author leon
 * @date 18-2-23.
 */
public interface IDigraph {
    /**
     * @return 顶点数
     */
    int V();
    /**
     * @return 边数
     */
    int E();
    /**
     * 添加 v-> w 的边
     *
     * @param v 起点
     * @param w 终点
     */
    void addEage(int v, int w);
    /**
     * 由v指出所连接的所有顶点
     *
     * @param v 顶点
     * @return 可迭代的顶点列表
     */
    Iterable<Integer> adj(int v);
    /**
     * 这个方法有时很有用,返回的是图的一个副本,但将其中所有的边的方向反向.
     * 因为可以找出"指向"每个顶点的边,而adj()为指出每个顶点的边.
     * @return 该图的反向图
     */
    IDigraph reverse();
    /**
     *
     * @return 字符串描述
     */
    String toString();
}
package algorithm.algorithmbook.graph;
import java.io.DataInputStream;
import algorithm.algorithmbook.graph.IDigraph;
/**
 * description:有向图
 *
 * @author leon
 * @date 18-2-23.
 */
public class Digraph implements IDigraph {
    //顶点数
    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[i] = new Bag<Integer>();
        }
    }
    public Digraph(DataInputStream input) {
        v = 0;
    }
    @Override
    public int V() {
        return v;
    }
    @Override
    public int E() {
        return e;
    }
    @Override
    public void addEage(int v, int w) {
        //因为这里是有向图,所以只需要连接 v->w 的边
        adj[v].add(w);
        e++;
    }
    @Override
    public Iterable<Integer> adj(int v) {
        return adj[v];
    }
    @Override
    public IDigraph reverse() {
        Digraph revDigraph = new Digraph(v);
        for (int i = 0; i < v; i++) {
            for (int to : revDigraph.adj(i)) {
                addEage(to, v);
            }
        }
        return revDigraph;
    }
}
4.2.3 有向图的可到达性.

单点可到达性:给定起点s ,是否有到指定顶点v的有向路径.算法和DeepFirstSearch类似.
多点可到达性:给定一个有向图和顶点集合,回答”是否存在一条从集合中任意的顶点到达给定顶点v的有向路径?”等类似问题.

4.2.3-1 标记-清除 的垃圾回收器

多点可到达性的重要实际应用在与 内存管理系统中.对于java 内存,在一个有向图中,对象表示一个顶点,引用表示一条边.这个模型很好的解决了java内存中.定期的进行对有向图的多点可到达性检测,发现有不可到达的顶点进行回收的处理.

4.2.3-2 有向图的寻路

DepthFirstPaths和breadthFirstPaths同样适合有向图.
单点有向图 给定一个有向图和起始顶点:回答”从s到给定目的的顶点v是否存在一条有向路径,如果有找出来”
单点最短有向路径.”从s到给定目的地v是否存在一条路径,如果存在找出其中最短的一条(所含的边最少)”

4.2.4 环和有向无环图

从原则上说,一副有向图可能包含多个有向环,但是实际上我们一般重点关注一小部分,或只想知道他们是否存在.


image.png
4.2.4.1 调度问题

优先级限制条件是调度问题的核心,他指明了哪些任务必须在哪些任务之前完成. 这时候采用有向图的方法来求解调度的顺序就是很不错的.例如下图

image.png

优先级限制下的调度问题: 解决这个问题可以用有向图来模型来表示.等价于 拓扑排序

image.png

拓扑排序: 给定一个有向图,将所有的顶点排序,使得所有的边都是从排在前面的边指向排在后面的边.例如下面下边示意图:


image.png

拓扑排序的典型应用:

应用 顶点
任务调度 任务 优先级限制
课程安排 课程 先导课程限制
继承 java类 Extends 关系
符号链接 文件名 链接
4.2.4.2 有向图中的环

在某些特定的有向图依赖关系中是不允许出现环结构的.例如java 类依赖关系.
有向环检测: 如果有环,在所有顶点中按照顺序寻找,返回自己的顶点.即找到了一个有向环.
一幅有向图的环的个数有可能是图大小的指数级别,所以一般情况下只需要找到一个有向环即可判断,而不需要找出所有的环.因此有向无环图就表现的尤为突出.

定义:

有向无环图[DAG (Directed acyclic graph)]就是一副不含环的有向图.

检测是否是有向无环图.采用深度优先搜索.维护一个递归调用栈来记录当前在遍历的有向路径,一旦我们找到了一条边v→ w ,并且w已经存在当前递归栈中,则表示找到了一个有向环,同时如果找不到这样一条边,则说明图是有向无环图.

package algorithm.algorithmbook.graph;
import java.util.Stack;
/**
 * description:寻找有向环
 *
 * @author leon
 * @date 18-2-26.
 */
public class DirectCycle {
    private int[] edgeTo;
    private boolean[] marked;
    //用于存放环的所有顶点,如果存在环
    private Stack<Integer> cycles;
    //递归调用的栈上的所有顶点
    private boolean[] onStack;
    public DirectCycle(Digraph digraph) {
        onStack = new boolean[digraph.V()];
        marked = new boolean[digraph.V()];
        edgeTo = new int[digraph.V()];
        for (int v = 0; v < digraph.V(); v++) {
            if (!marked[v]) {
                dfs(digraph, v);
            }
        }
    }
    private void dfs(Digraph digraph, int v) {
        onStack[v] = true;
        marked[v] = true;
        for (int w : digraph.adj(v)) {
            //特别的处理环的,如果已经找到了环则跳出循环
            if (this.hashCycle()) {
                return;
            }
            //进行深度优先遍历
            if (!marked[w]) {
                edgeTo[w] = v;
                dfs(digraph, w);
            }
            //如果已经marked了,说明已经查找过顶点,也就是出现了环
            else if (onStack[w]) {
                cycles = new Stack<>();
                for (int x = v; x != w; x = edgeTo[x]) {
                    cycles.push(x);
                }
                cycles.push(w);
                cycles.push(v);
            }
            onStack[v] = false;
        }
    }
    public boolean hashCycle() {
        return !cycles.empty();
    }
    public Iterable<Integer> cycle() {
        return cycles;
    }
}
4.2.3.4 顶点的深度优先次序和拓扑排序
命题E:

只有有向无环图才能进行拓扑排序.

实际上我们已经见过一种拓扑排序的算法,只需要添加一行代码就可以由标准的深度优先搜索完成这项任务!引入一种搜索排序,”有向图中基于深度优先的顶点搜索排序”.他的思想是深度优先搜索只会访问所有的顶点一次.遍历这个数据结构实际上能访问图中的所有顶点,而遍历的顺序取决与这个数据结构是递归前还是之后进行保存.一般情况人们关注3中情况:

1.前序遍历 (pre) :在递归前将顶点加入队列

2.后续遍历(post):在递归后将顶点加入队列

3.逆后续(reversePost): 把后续遍历的顺序反转采用栈保存,刚好可以反过来).在递归后将顶点加入栈.

待排序的有向图
image.png

执行深度优先搜索的 3中排序过程:


image.png
package algorithm.algorithmbook.graph;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.Stack;
/**
 * description:有向图深度优先排序的几种方式
 * <p>
 * 这里根据在递归前,后 进行顶点保存的顺序分成前序,后序,逆后序
 * 1.前序遍历 (pre) :在递归前将顶点加入队列
 * 2.后续遍历(post):在递归后将顶点加入队列
 * 3.逆后续(reversePost): 把后续遍历的顺序反转采用栈保存,刚好可以反过来).在递归后将顶点加入栈.
 *
 * @author leon
 * @date 18-2-27.
 */
public class DirectDeepFirstOrder {
    private boolean[] marked;
    private Queue<Integer> preQ;
    private Queue<Integer> postQ;
    private Stack<Integer> reversePost;
    public DirectDeepFirstOrder(Digraph g) {
        marked = new boolean[g.V()];
        preQ = new ArrayDeque<>();
        postQ = new ArrayDeque<>();
        reversePost = new Stack<>();
        for (int v = 0; v < g.V(); v++) {
            if (!marked[v]) {
                dfs(g, v);
            }
        }
    }
    /**
     * 深度优先遍历递归
     *
     * @param g 图
     * @param v 需要递归的顶点
     */
    private void dfs(Digraph g, int v) {
        preQ.offer(v);
        marked[v] = true;
        for (int w : g.adj(v)) {
            if (!marked[w]) {
                dfs(g, w);
            }
        }
        postQ.offer(v);
        reversePost.push(v);
    }
    /**
     * 前序
     *
     * @return 排序列表
     */
    public Iterable<Integer> pre() {
        return preQ;
    }
    /**
     * 后序
     *
     * @return 排序列表
     */
    public Iterable<Integer> post() {
        return postQ;
    }
    /**
     * 逆后序
     *
     * @return 排序列表
     */
    public Iterable<Integer> reversePost() {
        return reversePost;
    }
}
命题:

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

证明:

对于任意边v→ w,在调用dfs(v)时以下三种情况之一会满足:
1.dfs(w)已经调用过并且已经返回了(w已经被标记了)
2.dfs(w)还没有调用,此时会继续接下来调用dfs(w),并且在 dfs(v)之前调用,并且标记.
3.dfs(w)已经调用并且没有返回.证明的关键在于,需要证明这种情况在有向无环图中是不可能出现的,则可以确定v→ w 的dfs(x) 是有顺序的.因为 如果 dfs(w)已经调用,但是没有返回,则说明有边w→ v存在,然而本身有v→ w ,这样就形成了一个环.与有向无环图有矛盾,故情况3是不存在的.
综上所述:任意一条边的dfs(w) 都会在dfs(v)之前标记,也就是说 在排名靠后w的dfs(w) 比排名靠前v的dfs(v)先标记,把标记列表倒序就得到排名靠前的v→ w的顶点.

package algorithm.algorithmbook.graph;
/**
 * description:拓扑排序
 *
 * @author leon
 * @date 18-2-26.
 */
public class Topological {
    private Iterable<Integer> orderList;
    public Topological(Digraph digraph) {
        DirectCycle directCycle = new DirectCycle(digraph);
        if (!directCycle.hashCycle()) {
            DirectDeepFirstOrder directDeepFirstOrder = new DirectDeepFirstOrder(digraph);
            orderList = directDeepFirstOrder.reversePost();
        }
    }
    /**
     * 是否是有向无环图
     *
     * @return
     */
    public boolean isDAG() {
        return orderList != null;
    }
    /**
     * 拓扑排序
     *
     * @return
     */
    Iterable<Integer> order() {
        return orderList;
    }
    /**
     * test
     *
     * @param args
     */
    public static void main(String[] args) {
        String fileName = args[0];
        String sepatetor = args[1];
        SymbolDirectGraph symbolGraph = new SymbolDirectGraph(fileName, sepatetor);
        Topological topological = new Topological(symbolGraph.graph());
        if (!topological.isDAG()) {
            return;
        }
        for (int v : topological.orderList) {
            //通过索引表来获取图中对应的 名字
            System.out.println(symbolGraph.name(v));
        }
    }
}
命题G:

使用深度优先搜索对无向图进行拓扑排序的所需时间与V+E成正比.

证明:

由代码可知,拓扑排序需要进行两次,1.进行是否有环判断,2 进行逆后序排序. 每次操作都访问了顶点和边,即是V+E 的倍数关系.

4.2.5 有向图中的强连通性
定义:

如果有向图中 w和v 双向可到达即存在v到w的有向路径,也存在w到v的有向路径,称为强连通.如果一副有向图总任意两点之间都是强连通,则称这幅有向图也是强连通的.

两个顶点是强连通.当且仅当他们都是同在一个有向环中.
强连通分量特点:
  • 自反性:任意顶点v 和自己都是强连通的.
  • 对称性: v和w 是强连通,那么w和v也是强连通
  • 传递性: v和w 是强连通,w和x 是强连通.那么v 和x 是强连通.

强连通性将所有顶点分成一些平等的部分,每个部分是由所组成强连通的顶点最大子集组成.把这些最大子集称为强连通分量.包含V个顶点的有向图包含 1-V个强连通分量;一个强连通图只有一个强连通分量(也就是整个图);而一个无环图则包含V个强连通分量(每个顶点都是自己的强连通分量,所以强连通分量就是每个顶点).因为强连通是针对顶点而不是边,所以会出现有些边连接的两个顶点出现在同一个强连通分量中,而有些边连接的两个顶点则出现在不同的强连通分量中.


image.png
4.2.5.2 应用举例

在理解有向图的结构时,强连通性是一个重要的特点.他突出了相互关联的几组顶点(强连通分量).例如强连通分量能够帮组教科书的作者决定那些话题应该归为一类,帮助程序员组织程序的模块;表示哪些食物链中的物种应该在一个生态圈中.
强连通分量API

SCC(digraph G) 构造函数
Boolean stronglyConnetied(int v,int w) V ,w 是否是强连通
Int count() 强连通分量数
Int id(int v) V属于哪个强连通分量(0 ~ count-1)
kosaraju算法:

1.在给定有向图G,使用DeepFirstOrder计算他的反向图GR的逆后序排序.

2.在G图中进行标准的深度优先搜索,但是需要按照刚才计算出的顺序访问所有未标记过的顶点(而非标准的顺序访问).

3.在构造函数中,所有同一个递归dfs()调用中访问到的顶点都在同一个强连通分量中,将他们按照CC的方式识别出来.

强连通性: 回答”给定的两点是强连通的吗? 这幅图中含有多少个强连通分量 ?”

因为 Kosaraju 方法借助了有向图求反向图,然后再计算 逆序排序.

能不能用和 无向图相同的效率解决有向图图的强连通问题.Tarjan提出了一个解决方案.而且这个简单的解决方案令人惊讶.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容