图论总结-拓扑排序以及最短路径问题(无权最短路径、Dijkstra算法、具有负边值的图)

图的定义

一个图(graph)G = (V,E)顶点(vertex) 集 V 和 边(edge) 集 E 组成。
每一条边就是一个点对(v,w),其中 v,w∈ V,有时也被称为弧(arc)
如果点对是有序的,那么图就叫做是有向的(directed),有向的图有时也叫做有向图(digraph)
顶点 v 和 w 邻接(adjacent) 当且仅当(v,w)∈ E。
有时边还有第三种成分,称作 权(weight)值(cost)
对于一个顶点 v,边(u,v)的条数称为入度(indegree)

图中的一条路径(path) 是一个顶点序列 w1,w2,w3,...wN,使得(wi,wi+1)∈ E,1<= i < N。
这样的一条路径的长(length) 是该条路径上的边数,它等于 N - 1。
一条简单路径是这样的一条路径:其上的所有顶点都是互异的但第一个顶点和最后一个顶点可能相同

有向图中的圈(cycle) 是满足 w1 = wN 且长度至少为 1 的一条路径,如果该条路径是简单路径,那么这个圈就是简单圈
如果一个有向图没有圈,则称其为无圈的(acyclic)。一个有向无圈图有时也简称为 DAG

如果一个无向图中从每一个顶点到每一个其他顶点都存在一条路径,则称该无向图是连通的(connected)
具有这样性质的有向图称为是强连通(strongly connected) 的。
如果边上去掉方向所形成的图是连通的,那么称为基础图(underlying graph)
如果一个有向图不是连通的,但是它是基础图,那么该有向图称为弱连通(weakly connected) 的。
完全图(complete graph) 是其每一对顶点间都存在一条边的图。
稠密图(dense) :图中 E 的条数接近 V*V 也就是,接近任意两点之间相连。
稀疏图(sparse) :图中 E 的条数远小于 V*V。

图的表示

关于图的具体 Java 实现可以点此查看,或点击下面的链接:
https://github.com/0xZhangKe/Algorithms/tree/master/src/com/zhangke/java/graph/adt
我这里是使用邻接表的方式实现的。

邻接矩阵

表示图的一种简单方法是使用一个二维数组,称为邻接矩阵(adjacency matrix)表示法。
适合稠密的图。

邻接表

如果图是稀疏的,则更好的表示方式是使用邻接表(adjacency list)表示。

拓扑排序

拓扑排序是对有向无圈图的顶点的一种排序,他使得如果存在一条从 vi 到 vj 的路径,那么在排序中 vj 出现在 vi 后面。
一个简单的求拓扑排序的算法是先找出任意一个没有入度的顶点。然后我们显示出该顶点,并将它和它的边一起从图中删除。然后,我们对图的其余部分应用同样的方法处理。

public static <T> void sort(ListDGraph<T> graph) {
    Queue<Vertex<T>> queue = new ArrayDeque<>();
    Queue<Vertex<T>> resultQueue = new ArrayDeque<>();
    int size = graph.size();
    int[] indegree = new int[size];//入度数组
    for (int i = 0; i < size; i++) {
        List<Edge<Vertex<T>>> edges = graph.get(i).getEdgeList();
        for (Edge<Vertex<T>> item : edges) {
            indegree[graph.get(item.getDest())]++;
        }
    }
    for (int i = 0; i < size; i++) {
        if (indegree[i] == 0) {
            queue.offer(graph.get(i));
        }
    }
    while (!queue.isEmpty()) {
        Vertex<T> vertex = queue.poll();
        resultQueue.offer(vertex);
        List<Edge<Vertex<T>>> edges = vertex.getEdgeList();
        if (edges != null) {
            for (Edge<Vertex<T>> edge : edges) {
                int index = graph.get(edge.getDest());
                indegree[index]--;
                if (indegree[index] <= 0) {
                    queue.offer(edge.getDest());
                }
            }
        }
    }
    while (!resultQueue.isEmpty()) {
        Vertex<T> item = resultQueue.poll();
        System.out.print(item.getValue());
        if (!resultQueue.isEmpty()) {
            System.out.print(" -> ");
        }
    }
}

最短路径算法

输入一个赋权图:与每条边(vi, vj)相联系的是穿越该弧的代价(或称为值)ci, j。一条路径 v1 v2 ... vN 的值是 ∑i=1 N-1 ,i+ 1 叫做赋权路经长(weight path length)。而无权路径长(unweight path length)只是路径上的边数,及 N - 1。
为了解决最短路径问题,这里引入一个配置表的概念:


配置表

对于每个顶点,我们跟踪三个信息。
Know:表示该顶点是否是已知的;
dv:从起点 s 到该点的距离;
pv:簿记变量,它使我们能够显示出实际的路径。

那么再来定义一个用于创建配置表的方法:

/**
 * 用于寻找最短路径的辅助配置表
 *
 * Created by ZhangKe on 2019/3/31.
 */
public class TableEntity<T> {
    static final int INFINITY = Integer.MAX_VALUE;
    boolean know = false;
    int dist = INFINITY;
    T path = null;
    static <T> Map<Vertex<T>, TableEntity<Vertex<T>>> getTable(DGraph<T> graph, Vertex<T> vertex){
        Map<Vertex<T>, TableEntity<Vertex<T>>> table = new HashMap<>();
        for (int i = 0; i < graph.size(); i++) {
            Vertex<T> v = graph.get(i);
            TableEntity<Vertex<T>> entity = new TableEntity<>();
            if (v.equals(vertex)) {
                entity.dist = 0;
            }
            table.put(v, entity);
        }
        return table;
    }
    static <T> void printTable(Map<Vertex<T>, TableEntity<Vertex<T>>> table){
        String divider = "        ";
        System.out.print(String.format("v%sKnown%sDv%sPv", divider, divider, divider));
        System.out.println();
        for (Vertex<T> key : table.keySet()) {
            TableEntity<Vertex<T>> itemTable = table.get(key);
            System.out.print(key.getValue() +
                    divider +
                    itemTable.know +
                    divider +
                    itemTable.dist +
                    divider +
                    (itemTable.path == null ? "null" : itemTable.path.getValue()));
            System.out.println();
        }
    }
}

需要注意的是,这里的 DGraph 以及其他图相关的实现类可以点击这里查看,或者点击下面的链接,此处不做详细说明:
https://github.com/0xZhangKe/Algorithms/tree/master/src/com/zhangke/java/graph/adt

无权最短路径

使用某个顶点 s 作为输入参数,我们想找出从 s 到所有其他顶点的最短路径,我们只对包含在路径中的边数感兴趣,因此在边上不存在权。
显然,这是赋权最短路径问题的特殊情形,因为我们可以为所有的边都赋以权 1。
具体的代码实现如下:

public <T> void find(DGraph<T> graph, Vertex<T> s) {
    //创建初始配置表
    Map<Vertex<T>, TableEntity<Vertex<T>>> table = TableEntity.getTable(graph, s);
    Queue<Vertex<T>> queue = new ArrayDeque<>();
    queue.offer(s);
    while (!queue.isEmpty()) {
        Vertex<T> v = queue.poll();
        TableEntity<Vertex<T>> itemTable = table.get(v);
        itemTable.know = true;
        if (v.getEdgeList() != null) {
            for (Edge<Vertex<T>> edge : v.getEdgeList()) {
                if (edge.getDest() != null) {
                    TableEntity<Vertex<T>> destTable = table.get(edge.getDest());
                    if (destTable.dist == TableEntity.INFINITY) {
                        destTable.dist = itemTable.dist + 1;
                        destTable.path = v;
                        queue.offer(edge.getDest());
                    }
                }
            }
        }
    }
    TableEntity.printTable(table);
}

上面这种搜索方式成为广度优先搜索(breadth-first search)。该方法按层处理顶点:距开始点最近的那些顶点首先被赋值,而最远的那些顶点最后被赋值。这很像对树的层序遍历(level-order traversal)

Dijkstra 算法

解决单源最短路径问题的一般方法叫做 Dijkstra 算法(Dijkstra`s algorithm)。这个有 30 年历史的解法是贪婪算法(greedy algorithm) 最好的例子。
Dijkstra 算法像无权最短路径算法一样,按阶段进行,在每个阶段, Dijkstra 算法选择一个顶端 v,它在所有未知顶点中具有最小的 dv,同时算法声明从 s 到 v 的最短路径是已知的。

/**
     * 1.获取未标志过的顶点列表中值最小的顶点(因为默认都是 MAX_VALUE ,所以只可能邻接节点,本质上是寻找邻接节点中的最小值)
     * 2.遍历该顶点的邻接点,如果邻接点未标记,且值小于当前路径权重值,则用当前路径权重值更新
     * 3.重复步骤1/2
     */
private static <T> void find(DGraph<T> graph, Vertex<T> vertex) {
    Map<Vertex<T>, TableEntity<Vertex<T>>> table = TableEntity.getTable(graph, vertex);
    for (int i = 1; i < graph.size(); i++) {
        Vertex<T> minVertex = findUnknownMin(table);
        if (minVertex == null) {
            break;
        }
        TableEntity<Vertex<T>> minTable = table.get(minVertex);
        int minDist = minTable.dist;
        minTable.know = true;
        if (minVertex.getEdgeList() != null) {
            for (Edge<Vertex<T>> edge : minVertex.getEdgeList()) {
                if (edge.getDest() != null) {
                    TableEntity<Vertex<T>> edgeTable = table.get(edge.getDest());
                    if (!edgeTable.know &&
                        (minDist + edge.getWeight() < edgeTable.dist)) {
                        edgeTable.dist = minDist + edge.getWeight();
                        edgeTable.path = minVertex;
                    }
                }
            }
        }
    }
    TableEntity.printTable(table);
}
/**
     * 从未知表中读取一个 dist 最小的顶点
     */
private static <T> Vertex<T> findUnknownMin(Map<Vertex<T>, TableEntity<Vertex<T>>> table) {
    int min = TableEntity.INFINITY;
    Vertex<T> vertex = null;
    for (Vertex<T> key : table.keySet()) {
        TableEntity<Vertex<T>> item = table.get(key);
        if (!item.know && min >= item.dist) {
            min = item.dist;
            vertex = key;
        }
    }
    return vertex;
}

具有负边值的图

把赋权和无权的算法结合起来将会解决这个问题。

private static <T> void find(DGraph<T> graph, Vertex<T> vertex) {
    Map<Vertex<T>, TableEntity<Vertex<T>>> table = TableEntity.getTable(graph, vertex);
    Queue<Vertex<T>> queue = new ArrayDeque<>();
    queue.offer(vertex);
    while (!queue.isEmpty()) {
        Vertex<T> itemVertex = queue.poll();
        TableEntity<Vertex<T>> itemTable = table.get(itemVertex);
        itemTable.know = true;
        if (itemVertex.getEdgeList() != null) {
            for (Edge<Vertex<T>> edge : itemVertex.getEdgeList()) {
                if (edge.getDest() != null) {
                    TableEntity<Vertex<T>> destTable = table.get(edge.getDest());
                    if (itemTable.dist + edge.getWeight() < destTable.dist) {
                        destTable.dist = itemTable.dist + edge.getWeight();
                        destTable.path = itemVertex;
                        if (!queue.contains(edge.getDest())) {
                            queue.offer(edge.getDest());
                        }
                    }
                }
            }
        }
    }
    TableEntity.printTable(table);
}

注意:上述内容是对【数据结构与算法——C语言描述】.mark Allen Weiss 一书第九章:图论算法的学习总结。

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

推荐阅读更多精彩内容