最短路径: Dijkstra算法、Bellman-Ford算法、Floyd算法

目录

最短路径(Shortest Path)

最短路径 – 无权图

最短路径 – 负权边

最短路径 – 负权环

最短路径算法

Dijkstra (迪杰斯特拉算法)

Dijkstra – 思路

Dijkstra – 执行过程

Dijkstra – 代码实现

Bellman-Ford (贝尔曼-福特算法)

Bellman-Ford – 思路

Bellman-Ford – 思考

Bellman-Ford – 执行过程

Bellman-Ford – 代码实现

Floyd (弗洛伊德算法)

Floyd – 算法原理

Floyd – 代码实现


最短路径(Shortest Path)

最短路径是指两顶点之间权值之和最小的路径(有向图、无向图均适用,不能有负权环)

最短路 无权图

无权图相当于是全部边权值1的有权

最短路 负权边

有负权边,但没有负权环时,存在最短路径

A到E的最短路径是:A → B → E

最短路 负权环

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

通过负权环, A到E的路径可以无限短
A → E → D → F → E → D → F → E → D → F → E → D → F → E → ...

最短路径算法

Dijkstra (迪杰斯特拉算法)

Dijkstra 属于单源最短路径算法, 用于计算一个顶点到其他所有顶点的最短路径
使用前提: 不能有负权边
时间复杂度: 可优化至 O(ElogV), E是边数量, V是节点数量

Dijkstra  思路

Dijkstra的原理其实跟生活中的一些自然现象完全一样

1. 把每1个顶点想象成是1块小石头, 每1条边想象成是1条绳子, 每一条绳子都连接着2块小石头, 边的权值就是绳子的长度;   将小石头和绳子平放在一张桌子上(下图是一张俯视图,图中黄颜色的是桌子)

2. 接下来想象一下, 手拽着小石头A, 慢慢地向上提起来,远离桌面
B、D、C、E会依次离开桌面, 最后绷直的绳子就是A到其他小石头的最短路径

Dijkstra 执行过程

0. 初始化

1. 找到A出发权重最小的边, 该边的to顶点将被第一个提起来, 那么AB的最短路径将被确认; 对已确认最短路劲的B顶点的所有出边进行松弛操作, 尝试更新B顶点所有出边的to顶点的最短路径

2. 在第1步中尝试更新了所有顶点的最短路径, 那么此时再次从所有路径中查询出最小的路径(A-D), 该路径的to顶点将被提起, to顶点的最短路径将被确认; 然后, 对已确认最短路劲的D顶点的所有出边进行松弛操作, 尝试更新D顶点所有出边的to顶点的最短路径

松弛操作 (Relaxation): 更新2个顶点之间的最短路径
这里一般是指: 更新源点到另一个点的最短路径
松弛操作的意义: 尝试找出更短的最短路径
确定A到D的最短路径后, 对DC、DE边进行松弛操作, 更新了A到C、A到E的最短路径

3. 2 中尝试更新了所有顶点的最短路径, 那么此时再次从所有路径中查询出最小的路径(A-D-C), 该路径的to顶点将被提起, to顶点的最短路径将被确认; 然后, 对已确认最短路劲C顶点的所有出边进行松弛操作, 尝试更新C顶点所有出边的to顶点的最短路径

4. 3 中尝试更新了所有顶点的最短路径, 那么此时再次从所有路径中查询出最小的路径(A-D-C-E), 该路径的to顶点将被提起, to顶点的最短路径将被确认; 然后, 对已确认最短路劲E顶点的所有出边进行松弛操作, 尝试更新E顶点所有出边的to顶点的最短路径, 至此, 所有顶点的最短路径均已确认, 原始路径集合为空, 退出循环

Dijkstra  代码实现

@Override
public 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();

    //将起始顶点提前放入selectedPaths中, 避免对fromVertex顶点进行松弛操作,在后面需要再次移除
    selectedPaths.put(begin,null);

    //初始化起始顶点所有出边的权重
    Iterator<Edge<V, E>> it = beginVertex.outEdges.iterator();
    while(it.hasNext()){
        Edge<V, E> edge = it.next();
        PathInfo<V, E> pathInfo = new PathInfo<>();
        pathInfo.weight = edge.weight;
        pathInfo.edgeInfos.add(edge.info());
        paths.put(edge.to,pathInfo);
    }

    while(!paths.isEmpty()) {
        //将最小权重的顶点放入selectedPaths中
        Map.Entry<Vertex<V, E>, PathInfo<V,E>> minPath = findMinPath(paths);
        selectedPaths.put(minPath.getKey().value, minPath.getValue());
        paths.remove(minPath.getKey());

        Vertex<V, E> minVertex = minPath.getKey();
        Iterator<Edge<V, E>> outEdges = minVertex.outEdges.iterator();
        //对minVertex顶点的所有出边的to顶点进行松弛操作
        while (outEdges.hasNext()) {
            Edge<V, E> edge = outEdges.next();
            if (selectedPaths.containsKey(edge.to.value)) continue;
            relaxForDijkstra(edge,selectedPaths.get(edge.from.value),paths);
        }
    }
    //移除起始顶点
    selectedPaths.remove(begin);
    return selectedPaths;
}

/**
 *  松弛操作
 * @param edge  进行松弛操作的边
 * @param fromPath  起始顶点到edge的from顶点的最短路径
 * @param paths 顶点与路径(非最短路径)的映射
 */
private void relaxForDijkstra(Edge<V, E> edge,PathInfo<V,E> fromPath, Map<Vertex<V,E>,PathInfo<V,E>> paths){
    //begin顶点到to顶点的路径
    PathInfo<V,E> oldPath = paths.get(edge.to);
    //begin顶点到from顶点的权重 + 获取from顶点到to顶点的权重
    E newWeight = weightManager.add(fromPath.weight, edge.weight);
    //比较权重, 进行松弛操作
    if (oldPath == null || weightManager.compare(newWeight,oldPath.weight) < 0) {
        if(oldPath == null){
            oldPath = new PathInfo<>();
            paths.put(edge.to, oldPath);
        }
        /**
         * 将[begin--from]路径与[from--to]路径进行拼接, [begin--from]路径为begin顶点到from顶点最短路径
         * 将[begin--from]路径在paths中更新, 后面通过获取最小路径后,再放入selectedPaths
         */
        oldPath.weight = newWeight;
        List<EdgeInfo<V, E>> edgeInfos = oldPath.edgeInfos;
        edgeInfos.clear();
        edgeInfos.addAll(fromPath.edgeInfos); //将[begin--from]路径集合加入到edgeInfos集合中
        edgeInfos.add(edge.info()); //将[from--to]路径加入到edgeInfos集合中
    }
}

Bellman-Ford (贝尔-福特算法)

Bellman-Ford  思路

Bellman-Ford 也属于单源最短路径算法,支持负权边,还能检测出是否有负权环
算法原理:
对所有的边进行 V – 1 次松弛操作(V是节点数量), 得到所有可能的最短路径
时间复杂度: O(EV), E是边数量, V是节点数量

最好情况是恰好从左到右的顺序对边进行松弛操作
对所有边仅需进行1次松弛操作就能计算出A到达其他所有顶点的最短路径

最坏情况是恰好每次都从右到左的顺序对边进行松弛操作
对所有边需进行 V – 1 次松弛操作才能计算出A到达其他所有顶点的最短路径

Bellman-Ford  思考

1. 如何判断图中是否存在负权环?
         在进行 V-1 次松弛操作后, 再进行一轮松弛操作, 在这轮松弛操作中某个顶点再次计算出了更短的路径, 那么说明图中存在负权环

2. 为什么最多需要 V-1 次松弛操作就可以确认所有顶点的最短路径?
         最坏情况下,  某一个顶点E的最短路径需要经过所有的顶点也就是顶点E的最短路径会存在V-1条边, 那么顶点E最短路径确认的前提条件是其他顶点的最短路径全部确认; 那么在每轮松弛操作中, 至少会确认其中的一条边的为顶点E最短路径中的一个边, 那么总共需要V-1轮松弛操作 (每一个轮的松弛操作都是在上一轮松弛操作更新路径的基础上)

3. 某一个顶点E的最短路径需要经过所有的顶点为什么会是最坏情况?
      如果一个顶点顶点E的最短路径并不需要经过所有的顶点,  那么一定存在以下情况: 至少存在两个顶点依赖一个顶点的最短路径, 也就是在一轮松弛操作后, 某一个顶点确认之后, 下一轮松弛操作将至少确认两个顶点的最短路径;  如下, 当顶点B的路径更新后, 下一轮松弛操作中顶点E和顶点F的路径可以同时更新, 将少一轮松弛操作, 也就是低于V-1轮松弛操作就可以确认所有顶点的最短路径

以上为个人理解, 如果有不对的地方, 欢迎指出

Bellman-Ford  执行过程

不难分析出,经4次松弛操作之后,已经计算出了A到其他所有顶点的最短路径, 在第五次松弛操作时没有更新任何一个顶点的最短路径

Bellman-Ford  代码实现

@Override
public Map<V, PathInfo<V, E>> bellman_ford(V begin) {
    Vertex<V, E> beginVertex = vertices.get(begin);
    if(beginVertex == null) return null;

    Map<V,PathInfo<V,E>> paths = new HashMap();
    PathInfo<V, E> beginPathInfo = new PathInfo<>();
    beginPathInfo.weight = weightManager.zero();
    paths.put(begin,beginPathInfo);

    int count = vertices.size() - 1;
    for (int i = 0; i < count - 1; i++) {
        boolean relaxSucc = false;
        for (Edge<V, E> edge : edges) {
            if (relaxSucc){
                relaxForBellman_Ford(edge,paths.get(edge.from.value),paths);
            }else{
                relaxSucc = relaxForBellman_Ford(edge,paths.get(edge.from.value),paths);
            }
        }
        /**
         * 如果在 V-1次以内的某一轮循环中, 没有执行有效的松弛操作, 那么说明, 所有顶点的最短路径均已确认, 直接退出;
         */
        if (!relaxSucc) break;
    }
    /**
     * 负权环检测
     */
    for (Edge<V, E> edge : edges) {
        if (relaxForBellman_Ford(edge,paths.get(edge.from.value),paths)){
            System.out.println("存在负权环!");
            paths.clear();
            return paths;
        }
    }
    /**
     * 如果在V-1次以内的某一轮循环中, 没有执行有效的松弛操作, 那么说明, 所有顶点的最短路径均已确认, 直接退出;
     */
    paths.remove(begin);
    return paths;
}
/**
 * 松弛操作
 * @param edge  进行松弛操作的边
 * @param fromPath  起始顶点到edge的from顶点的路径
 * @param paths 顶点与路径(非最短路径)的映射
 * @return 是否进行了松弛操作
 */
private boolean relaxForBellman_Ford(Edge<V, E> edge,PathInfo<V,E> fromPath, Map<V,PathInfo<V,E>> paths){
    //begin顶点到to顶点的路径
    PathInfo<V,E> oldPath = paths.get(edge.to.value);
    //begin顶点到from顶点的权重 + 获取from顶点到to顶点的权重
    E newWeight = weightManager.add(fromPath.weight, edge.weight);
    //比较权重, 进行松弛操作
    if (oldPath == null || weightManager.compare(newWeight,oldPath.weight) < 0) {
        if(oldPath == null){
            oldPath = new PathInfo<>();
            paths.put(edge.to.value, oldPath);
        }
        /**
         * 将[begin--from]路径与[from--to]路径进行拼接, [begin--from]路径为begin顶点到from顶点最短路径
         * 将[begin--from]路径在paths中更新, 后面通过获取最小路径后,再放入selectedPaths
         */
        oldPath.weight = newWeight;
        List<EdgeInfo<V, E>> edgeInfos = oldPath.edgeInfos;
        edgeInfos.clear();
        edgeInfos.addAll(fromPath.edgeInfos); //将[begin--from]路径集合加入到edgeInfos集合中
        edgeInfos.add(edge.info()); //将[from--to]路径加入到edgeInfos集合中
        return true;
    }else{
        return false;
    }
}

Floyd (弗洛伊德算法)

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

时间复杂度:O(V3), 效率比执行 V 次 Dijkstra 算法要好( V 是顶点数量)

Floyd – 算法原理

从任意顶点 i 到任意顶点 j 的最短路径不外乎两种可能
1.直接从 i 到 j
2.从i经过若干个顶点到 j
    (1) 假设 dist(i, j) 为顶点 i 到顶点 j 的最短路径的距离
    (2) 对于每一个顶点 k, 检查 dist(i, k) + dist(k, j) < dist(i,j) 是否成立
       a. 如果成立, 证明从 i 到 k 再到 j 的路径比 i 直接到 j 的路径短, 设置 dist(i, j) = dist(i, k) + dist(k, j)
       b. 当我们遍历完所有结点 k, dist(i, j) 中记录的便是 i 到 j 的最短路径的距离

Floyd – 代码实现

@Override
public Map<V, Map<V, PathInfo<V, E>>> floyd() {
    Map<V, Map<V, PathInfo<V, E>>> paths = new HashMap<>();
    //初始化paths, 确保每个顶点都在paths中作为from顶点, 即paths.get(v) != null
    edges.forEach((Edge<V, E> edge) -> {
        Map<V, PathInfo<V, E>> fromPathMap = paths.get(edge.from.value);
        if(fromPathMap == null){
            fromPathMap = new HashMap();
            paths.put(edge.from.value,fromPathMap);
        }
        PathInfo<V, E> toPathInfo = new PathInfo<>();
        toPathInfo.edgeInfos.add(edge.info());
        toPathInfo.weight = edge.weight;
        fromPathMap.put(edge.to.value,toPathInfo);
    });
    vertices.forEach((V v2, Vertex<V, E> vertiex2) -> {   //中间节点
        vertices.forEach((V v1, Vertex<V, E> vertiex1) -> { //from节点
            vertices.forEach((V v3, Vertex<V, E> vertiex3) -> {  //to节点
                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);
                //比较[v1--v2]权重 + [v2--v3]权重 与 [v1--v3]权重的大小, 判断是否需要更新v1到v2的路径
                if(path13 == null || weightManager.compare(newWeight, path13.weight) < 0){
                    if(path13 == null){  //如果原v1->v3路径为空,则初始化, 并加入到v1的路径信息中
                        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;
}

/**
 * 获取指定from顶点和to顶点的路径信息
 * @param from
 * @param to
 * @param paths
 * @return
 */
private PathInfo<V, E> getPathInfo(V from,V to,Map<V, Map<V, PathInfo<V, E>>> paths){
    Map<V, PathInfo<V, E>> pathInfo = paths.get(from);
    return pathInfo == null ? null : pathInfo.get(to);
}

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值