图的最短路径

最短路径是指两个顶点之间权值之和最小的路径, 但是不能有负权环

有权有向图和无向图最短路径
无权有向图无向图路径

有负权边, A 到E 最短路径, A -> B -> E

有负权路径

有负权环, 不存在最短路径

有环的负权路径

最短路径典型应用之一, 路径规划问题

3 个经典算法

单源最短路径

  • Dijkstra
  • Bellman-Ford

多源路径

  • Floyd

Dijkstra

用于计算一个顶点到其他所有顶点的最短路径, 但是不能有负权边

时间复杂度, 可以优化至O(ElogV), E 是边数量, V 是节点数量

假想所有顶点是石头, 边为连着两块石头的绳子, 平放在桌子上

将石头A, 提起, 远离桌面, B, D, C 会依次离开桌面

最后绷直的绳子就是A 到其他小石头的最短路径, 后离开桌面的小石头, 都是被先离开桌面的小石头拉起的

dijkstra想象成石头
石头A提起

执行过程

绿色代表已经离开桌面, 确定了最短路径

红色更新最短路径信息

dijkstra执行过程1
dijkstra执行过程2
dijkstra执行过程3

松弛操作(Relaxation): 更新两个顶点之间最短路径, 找出更短的路径

抽象类

public abstract class Graph<V, E> {
    
    protected WeightManager<E> weightManager;
    
    public Graph() {}
    
    public Graph(WeightManager<E> weightManager) {
        this.weightManager = weightManager;
    }
    
    public abstract int edgesSize();
    public abstract int verticesSize();
    
    public abstract void addVertex(V v);
    public abstract void addEdge(V from, V to);
    public abstract void addEdge(V from, V to, E weight);
    
    public abstract void removeVertex(V v);
    public abstract void removeEdge(V from, V to);
    
    public abstract void bfs(V begin, VertexVisitor<V> visitor);
    public abstract void dfs(V begin, VertexVisitor<V> visitor);
    
    public abstract Set<EdgeInfo<V, E>> mst();
    
    public abstract List<V> topologicalSort();
    
//  public abstract Map<V, E> shortestPath(V begin);
    public abstract Map<V, PathInfo<V, E>> shortestPath(V begin);
    public abstract Map<V, Map<V, PathInfo<V, E>>> shortestPath();
    
    public interface WeightManager<E> {
        int compare(E e1, E e2);
        E add(E e1, E e2);
        E zero();
    }
    
    public interface VertexVisitor<V> {
        boolean visit(V v);
    }
    
    public static class PathInfo<V, E> {
        protected E weight;
        protected List<EdgeInfo<V, E>> edgeInfos = new LinkedList<>();
        
        public PathInfo() {}
        public PathInfo(E weight) {
            this.weight = weight;
        }

        public E getWeight() {
            return weight;
        }
        
        public void setWeight(E weight) {
            this.weight = weight;
        }
        
        public List<EdgeInfo<V, E>> getEdgeInfos() {
            return edgeInfos;
        }
        
        public void setEdgeInfos(List<EdgeInfo<V, E>> edgeInfos) {
            this.edgeInfos = edgeInfos;
        }

        @Override
        public String toString() {
            return "PathInfo [weight=" + weight + ", edgeInfos=" + edgeInfos + "]";
        }
        
    }
    
    public static class EdgeInfo<V, E> {
        private V from;
        private V to;
        private E weight;
        
        public EdgeInfo(V from, V to, E weight) {
            this.from = from;
            this.to = to;
            this.weight = weight;
        }

        
        public V getFrom() {
            return from;
        }

        
        public void setFrom(V from) {
            this.from = from;
        }

        
        public V getTo() {
            return to;
        }

        
        public void setTo(V to) {
            this.to = to;
        }

        
        public E getWeight() {
            return weight;
        }
        
        public void setWeight(E weight) {
            this.weight = weight;
        }

        @Override
        public String toString() {
            return "EdgeInfo [from=" + from + ", to=" + to + ", weight=" + weight + "]";
        }
        
    }
    
}
    private Map<V, PathInfo<V, E>> dijkstra(V begin) {
        Vertex<V, E> beginVertex = vertices.get(begin);
        if (beginVertex == null) return null;
        
        Map<V, PathInfo<V, E>> selectedPaths = new HashMap<>();
        Map<Vertex<V, E>, PathInfo<V, E>> paths = new HashMap<>();
        
        // 初始化paths
        for (Edge<V, E> edge : beginVertex.outEdges) {
            PathInfo<V, E> path = new PathInfo<>();
            path.weight = edge.weight;
            path.edgeInfos.add(edge.info());
            paths.put(edge.to, path);
        }
        
        while (!paths.isEmpty()) {
            Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = getMinPath(paths);
            Vertex<V, E> minVertex = minEntry.getKey();
            PathInfo<V, E> minPath = minEntry.getValue();
            selectedPaths.put(minVertex.value, minEntry.getValue());
            paths.remove(minVertex);
            for (Edge<V, E> edge : minVertex.outEdges) {
                // 如果edge.to 已经离开桌面, 就没必要进行松弛操作
                if (selectedPaths.containsKey(edge.to.value)) continue;
                relaxDijkstra(edge, minPath, paths);
//               新的最短路径, beginVertex 到edge.from 的最短路径 + edge.weight
//              E newWeight = weightManager.add(minEntry.getValue().weight, edge.weight);
//              // 以前的最短路径, beginVertex 到edge.to 的最短路径
//              PathInfo<V, E> oldPath = paths.get(edge.to);
//              if (oldPath != null && weightManager.compare(newWeight, oldPath.weight) >= 0) continue;
//              if (oldPath == null) {
//                  oldPath = new PathInfo<>();
//                  paths.put(edge.to, oldPath);
//              } else {
//                  oldPath.edgeInfos.clear();
//              }
//
//              oldPath.weight = newWeight;
//              oldPath.edgeInfos.addAll(minEntry.getValue().edgeInfos);
//              oldPath.edgeInfos.add(edge.info());
            }
        }
        selectedPaths.remove(begin);
        return selectedPaths;
    }
    
    /**
     * 松弛
     * @param edge 需要进行松弛的边
     * @param fromPath edge的from的最短路径信息
     * @param paths
     */
    private void relaxDijkstra(Edge<V, E> edge, PathInfo<V, E> fromPath, Map<Vertex<V, E>, PathInfo<V, E>> paths) {
        // 新的最短路径, beginVertex 到edge.from 的最短路径 + edge.weight
        E newWeight = weightManager.add(fromPath.weight, edge.weight);
        // 以前的最短路径, beginVertex 到edge.to 的最短路径
        PathInfo<V, E> oldPath = paths.get(edge.to);
        if (oldPath != null && weightManager.compare(newWeight, oldPath.weight) >= 0) return;
        if (oldPath == null) {
            oldPath = new PathInfo<>();
            paths.put(edge.to, oldPath);
        } else {
            oldPath.edgeInfos.clear();
        }

        oldPath.weight = newWeight;
        oldPath.edgeInfos.addAll(fromPath.edgeInfos);
        oldPath.edgeInfos.add(edge.info());
        
    }
    
    /**
     * 返回paths 中的最小路径
     * @param path
     * @return
     */
    private Entry<Vertex<V, E>, PathInfo<V, E>> getMinPath(Map<Vertex<V, E>, PathInfo<V, E>> paths) {
        Iterator<Entry<Vertex<V, E>, PathInfo<V, E>>> it = paths.entrySet().iterator();
        Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = it.next();
        while (it.hasNext()) {
            Entry<Vertex<V, E>, PathInfo<V, E>> entry = it.next();
            if (weightManager.compare(entry.getValue().weight, minEntry.getValue().weight) < 0) {
                minEntry = entry;
            }
        }
        return minEntry;
    }

Bellman-Ford

支持负权边, 可以检测出是否有负权环

原理: 对所有的边进行V - 1 次松弛操作, 得到所有可能的最短路径

时间复杂度O(EV), E 为边数量, V 是节点数量

例如, 对下图, 所有边尽一次松弛, 找出A 到所有顶点最短路径, 顺序从左到右

A到所有顶点的松弛操作

最坏情况, 恰好每次从右到左对边进行松弛操作, 所有边进行V - 1 次松弛操作, 才能计算出A 到其他所有顶点最短路径

最坏情况松弛

松弛操作

松弛操作

每次松弛顺序DC, DF, BC, ED, EF, BE, AE, AB

执行过程1
执行过程2
正常过程3
    private Map<V, PathInfo<V, E>> bellmanFord(V begin) {
        Vertex<V, E> beginVertex = vertices.get(begin);
        if (beginVertex == null) return null;
        
        Map<V, PathInfo<V, E>> selectedPaths = new HashMap<>();
        selectedPaths.put(begin, new PathInfo<>(weightManager.zero()));
        
        int count = vertices.size() - 1;
        for (int i = 0; i < count; i++) {
            for (Edge<V, E> edge : edges) {
                PathInfo<V, E> pathFrom = selectedPaths.get(edge.from.value);
//              if (pathFrom == null) continue;
                relax(edge, pathFrom, selectedPaths);
            }
        }
        
        for (Edge<V, E> edge : edges) {
            PathInfo<V, E> pathFrom = selectedPaths.get(edge.from.value);
//          if (pathFrom == null) continue;
            if (relax(edge, pathFrom, selectedPaths)) {
                System.out.println("有负权环");
                return null;
            }
        }
        
        selectedPaths.remove(begin);
        return selectedPaths;
    }
    
    /**
     * 松弛
     * @param edge 需要进行松弛的边
     * @param fromPath edge的from的最短路径信息
     * @param paths
     */
    private boolean relax(Edge<V, E> edge, PathInfo<V, E> fromPath, Map<V, PathInfo<V, E>> paths) {
        // 新的最短路径, beginVertex 到edge.from 的最短路径 + edge.weight
        E newWeight = weightManager.add(fromPath.weight, edge.weight);
        // 以前的最短路径, beginVertex 到edge.to 的最短路径
        PathInfo<V, E> oldPath = paths.get(edge.to.value);
        if (oldPath != null && weightManager.compare(newWeight, oldPath.weight) >= 0) return false;
        if (oldPath == null) {
            oldPath = new PathInfo<>();
            paths.put(edge.to.value, oldPath);
        } else {
            oldPath.edgeInfos.clear();
        }

        oldPath.weight = newWeight;
        oldPath.edgeInfos.addAll(fromPath.edgeInfos);
        oldPath.edgeInfos.add(edge.info());
        return true;
    }

Floyd

多源路径算法, 能够求出任意2 个顶点之间的最短路径, 支持负权边

时间复杂度O(V^3), 效率比执行V 次Dijkstra 算法要好

原理:

从任意顶点i 到任意顶点 j 的最短路径包括

  1. 直接i 到j
  2. 从i 经过若干顶点到j

假设dist(i, j)为顶点i 到j 的最短路径距离

对于每一个顶点k, 检查dist(i, k) + dist(k, j) < dist(i, j) 是否成立

如果成立, 证明从i 到k 再到j 的路径比i 直接到j 的路径, 设置dist(i, j) = dist(i, k) + dist(k, j)

遍历所有节点k, dist(i, j) 中记录的便是i 到j 的最短路径的距离

    public Map<V, Map<V, PathInfo<V, E>>> shortestPath() {
        Map<V, Map<V, PathInfo<V, E>>> paths = new HashMap<>();
        // 初始化
        for (Edge<V, E> edge : edges) {
            Map<V, PathInfo<V, E>> map = paths.get(edge.from.value);
            if (map == null) {
                map = new HashMap<>();
                paths.put(edge.from.value, map);
            }
            PathInfo<V, E> pathInfo = new PathInfo<>(edge.weight);
            pathInfo.edgeInfos.add(edge.info());
            map.put(edge.to.value, pathInfo);
        }
        
        vertices.forEach((V v2, Vertex<V, E> vertex2) -> {
            vertices.forEach((V v1, Vertex<V, E> vertex1) -> {
                vertices.forEach((V v3, Vertex<V, E> vertex3) -> {
                    if (v1.equals(v2) || v2.equals(v3) ||v1.equals(v3)) return;
                    
                    // v1 -> v2
                    PathInfo<V, E> path12 = getPathInfo(v1, v2, paths);
                    if (path12 == null) return;
                    
                    // v2 -> v3
                    PathInfo<V, E> path23 = getPathInfo(v2, v3, paths);
                    if (path23 == null) return;
                    
                    // v1 -> v3
                    PathInfo<V, E> path13 = getPathInfo(v1, v3, paths);
                    E newWeight = weightManager.add(path12.weight, path23.weight);
                    if (path13 != null && weightManager.compare(newWeight, path13.weight) >= 0) return;
                    
                    if (path13 == null) {
                        path13 = new PathInfo<>();
                        paths.get(v1).put(v3, path13);
                    } else {
                        path13.edgeInfos.clear();
                    }
                    
                    path13.weight = newWeight;
                    path13.edgeInfos.addAll(path12.edgeInfos);
                    path13.edgeInfos.addAll(path23.edgeInfos);
                    
                });
            });
        });
        
        return paths;
    }
    
    private PathInfo<V, E> getPathInfo(V from, V to, Map<V, Map<V, PathInfo<V, E>>> paths) {
        Map<V, PathInfo<V, E>> map = paths.get(from);
        return map == null ? null : map.get(to);
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容

  • 摘要 最短路径问题是一个在图论研究中很经典的问题,已经被应用到GIS、GPS等信息管理系统中,为人们生活带来了很大...
    偏偏孤倨引山洪阅读 302评论 0 1
  • 摘要 最短路径问题是一个在图论研究中很经典的问题,已经被应用到GIS、GPS等信息管理系统中,为人们生活带来了很大...
    你本无意穿堂风_a69c阅读 615评论 2 3
  • 图论最短路径问题 最最原始的问题——两点间的最短路这类背景一般是类似:已知各城市之间距离,请给出从城市A到城市B的...
    yuq329阅读 592评论 0 0
  • 求图的最短路径(详谈Floyd和Dijkstra) (注:在这一部分起点、源点意思相近;点的距离、边的长度、权值意...
    _黑色吊椅阅读 2,775评论 0 9
  • 前言 本专题旨在快速了解常见的数据结构和算法。 在需要使用到相应算法时,能够帮助你回忆出常用的实现方案并且知晓其优...
    蛮三刀酱阅读 3,381评论 0 0