【恋上数据结构与算法二】(三)图(Graph)

数据结构回顾

图(Graph)

◼图由顶点(vertex)和边(edge)组成,通常表示为 G = (V, E)
G表示一个图,V是顶点集,E是边集
顶点集V有穷且非空
任意两个顶点之间都可以用边来表示它们之间的关系,边集E可以是空的

图的应用举例

◼ 图结构的应用极其广泛
社交网络
地图导航
游戏开发
......

有向图(Directed Graph)

◼ 有向图的边是有明确方向的

◼有向无环图(Directed Acyclic Graph,简称 DAG)
如果一个有向图,从任意顶点出发无法经过若干条边回到该顶点,那么它就是一个有向无环图

出度、入度

◼ 出度、入度适用于有向图

◼ 出度(Out-degree)
一个顶点的出度为 x,是指有 x 条边以该顶点为起点
顶点11的出度是3

◼ 入度(In-degree)
一个顶点的入度为 x,是指有 x 条边以该顶点为终点
顶点11的入度是2

无向图(Undirected Graph)

◼ 无向图的边是无方向的

◼ 效果类似于下面的有向图

混合图(Mixed Graph)

◼ 混合图的边可能是无向的,也可能是有向的

简单图、多重图

◼ 平行边
在无向图中,关联一对顶点的无向边如果多于1条,则称这些边为平行边
在有向图中,关联一对顶点的有向边如果多于1条,并且它们的的方向相同,则称这些边为平行边

◼ 多重图(Multigraph)
有平行边或者有自环的图

◼简单图(Simple Graph)
既没有平行边也不没有自环的图
课程中讨论的基本都是简单图

无向完全图(Undirected Complete Graph)

◼ 无向完全图的任意两个顶点之间都存在边
n 个顶点的无向完全图有 n(n − 1)/2 条边
n−1 + n−2 + n−3 +⋯+3+2+1 = n(n − 1)/2

有向完全图(Directed Complete Graph)

◼ 有向完全图的任意两个顶点之间都存在方向相反的两条边
n 个顶点的有向完全图有 n(n − 1) 条边

◼稠密图(Dense Graph):边数接近于或等于完全图
◼稀疏图(Sparse Graph):边数远远少于完全图

有权图(Weighted Graph)

◼ 有权图的边可以拥有权值(Weight)

连通图(Connected Graph)

◼如果顶点 x 和 y 之间存在可相互抵达的路径(直接或间接的路径),则称 x 和 y 是连通的
◼如果无向图 G 中任意2个顶点都是连通的,则称G为连通图

连通分量(Connected Component)

◼ 连通分量:无向图的极大连通子图
连通图只有一个连通分量,即其自身;非连通的无向图有多个连通分量
◼ 下面的无向图有3个连通分量

强连通图(Strongly Connected Graph)

◼如果有向图 G 中任意2个顶点都是连通的,则称G为强连通图

强连通分量(Strongly Connected Component)

◼ 强连通分量:有向图的极大强连通子图
强连通图只有一个强连通分量,即其自身;非强连通的有向图有多个强连通分量

图的实现方案

◼ 图有2种常见的实现方案
邻接矩阵(Adjacency Matrix)
邻接表(Adjacency List)

邻接矩阵(Adjacency Matrix)

◼ 邻接矩阵的存储方式
一维数组存放顶点信息
二维数组存放边信息

◼ 邻接矩阵比较适合稠密图
不然会比较浪费内存

邻接矩阵 – 有权图

邻接表(Adjacency List)

邻接表 – 有权图

图的基础接口

int verticesSize();
int edgesSize();

void addVertex(V v);
void removeVertex(V v);

void addEdge(V from, V to);
void addEdge(V from, V to, E weight);
void removeEdge(V from, V to);

顶点的定义

private static class Vertex<V, E> {
    V value;
    Set<Edge<V, E>> inEdges = new HashSet<>();
    Set<Edge<V, E>> outEdges = new HashSet<>();
    Vertex(V value) {
        this.value = value;
    }
    @Override
    public boolean equals(Object obj) {
        return Objects.equals(value, ((Vertex<V, E>)obj).value);
    }
    @Override
    public int hashCode() {
        return value == null ? 0 : value.hashCode();
    }
}

边的定义

private static class Edge<V, E> {
    Vertex<V, E> from;
    Vertex<V, E> to;
    E weight;
    
    @Override
    public boolean equals(Object obj) {
        Edge<V, E> edge = (Edge<V, E>) obj;
        return Objects.equals(from, edge.from) && Objects.equals(to, edge.to);
    }
    @Override
    public int hashCode() {
        return from.hashCode() * 31 + to.hashCode();
    }
}

遍历

◼ 图的遍历
从图中某一顶点出发访问图中其余顶点,且每一个顶点仅被访问一次

◼ 图有2种常见的遍历方式(有向图、无向图都适用)
广度优先搜索(Breadth First Search,BFS),又称为宽度优先搜索、横向优先搜索
深度优先搜索(Depth First Search,DFS)
发明“深度优先搜索”算法的2位科学家在1986年共同获得计算机领域的最高奖:图灵奖

广度优先搜索(Breadth First Search)

◼ 之前所学的二叉树层序遍历就是一种广度优先搜索

广度优先搜索 – 思路

广度优先搜索 – 实现

public void bfs(V begin, VertexVisitor<V> visitor) {
    if (visitor == null) return;
    Vertex<V, E> beginVertex = vertices.get(begin);
    if (beginVertex == null) return;
    
    Set<Vertex<V, E>> visitedVertices = new HashSet<>();
    Queue<Vertex<V, E>> queue = new LinkedList<>();
    queue.offer(beginVertex);
    visitedVertices.add(beginVertex);
    
    while (!queue.isEmpty()) {
        Vertex<V, E> vertex = queue.poll();
        if (visitor.visit(vertex.value)) return;
        
        for (Edge<V, E> edge : vertex.outEdges) {
            if (visitedVertices.contains(edge.to)) continue;
            queue.offer(edge.to);
            visitedVertices.add(edge.to);
        }
    }
}

深度优先搜索(Depth First Search)

◼ 之前所学的二叉树前序遍历就是一种深度优先搜索

深度优先搜索 – 递归实现

public void dfs(V begin) {
    Vertex<V, E> beginVertex = vertices.get(begin);
    if (beginVertex == null) return;
    dfs2(beginVertex, new HashSet<>());
}
private void dfs(Vertex<V, E> vertex, Set<Vertex<V, E>> visitedVertices) {
    System.out.println(vertex.value);
    visitedVertices.add(vertex);

    for (Edge<V, E> edge : vertex.outEdges) {
        if (visitedVertices.contains(edge.to)) continue;
        dfs2(edge.to, visitedVertices);
    }
}

深度优先搜索 – 非递归思路

1.从otuEdges中选择一条边
2.将选择边的from、to按顺序入栈
3.打印选择边的to
4.将to加到已经访问的范围中
5.break

深度优先搜索 – 非递归实现

public void dfs(V begin, VertexVisitor<V> visitor) {
    if (visitor == null) return;
    Vertex<V, E> beginVertex = vertices.get(begin);
    if (beginVertex == null) return;

    Set<Vertex<V, E>> visitedVertices = new HashSet<>();
    Stack<Vertex<V, E>> stack = new Stack<>();
    
    // 先访问起点
    stack.push(beginVertex);
    visitedVertices.add(beginVertex);
    if (visitor.visit(begin)) return;
    
    while (!stack.isEmpty()) {
        Vertex<V, E> vertex = stack.pop();
        
        for (Edge<V, E> edge : vertex.outEdges) {
            if (visitedVertices.contains(edge.to)) continue;
            
            stack.push(edge.from);
            stack.push(edge.to);
            visitedVertices.add(edge.to);
            if (visitor.visit(edge.to.value)) return;
            
            break;
        }
    }
}

AOV网(Activity On Vertex Network)

◼ 一项大的工程常被分为多个小的子工程
✓ 子工程之间可能存在一定的先后顺序,即某些子工程必须在其他的一些子工程完成后才能开始

◼ 在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,子工程被称为活动(Activity)
✓以顶点表示活动、有向边表示活动之间的先后关系,这样的图简称为 AOV 网

◼标准的AOV网必须是一个有向无环图(Directed Acyclic Graph,简称 DAG)

◼ B依赖于A
◼ C依赖于B
◼ D依赖于B
◼ E依赖于B、C、D
◼ F依赖于E

拓扑排序(Topological Sort)

◼ 前驱活动:有向边起点的活动称为终点的前驱活动
只有当一个活动的前驱全部都完成后,这个活动才能进行

◼ 后继活动:有向边终点的活动称为起点的后继活动

◼ 什么是拓扑排序?
将 AOV 网中所有活动排成一个序列,使得每个活动的前驱活动都排在该活动的前面
比如上图的拓扑排序结果是:A、B、C、D、E、F 或者 A、B、D、C、E、F (结果并不一定是唯一的)

拓扑排序 – 思路

◼ 可以使用卡恩算法(Kahn于1962年提出)完成拓扑排序
假设 L 是存放拓扑排序结果的列表
1 把所有入度为 0 的顶点放入 L 中,然后把这些顶点从图中去掉
2 重复操作 1,直到找不到入度为 0 的顶点
如果此时 L 中的元素个数和顶点总数相同,说明拓扑排序完成
如果此时 L 中的元素个数少于顶点总数,说明原图中存在环,无法进行拓扑排序

拓扑排序 – 实现

// 拓扑排序(有向无环图才可以)
@Override
public List<V> topologicalSort() {
    List<V> list = new ArrayList<>();
    Queue<Vertex<V, E>> queue = new LinkedList<>();
    Map<Vertex<V, E>, Integer> ins = new HashMap<>();
    // 初始化(将度为0的节点都放入队列)
    vertices.forEach((V v, Vertex<V, E> vertex) -> {
        int in = vertex.inEdges.size();
        if (in == 0) {
            queue.offer(vertex);
        } else {
            ins.put(vertex, in);
        }
    });
    
    while (!queue.isEmpty()) {
        Vertex<V, E> vertex = queue.poll();
        // 放入返回结果中
        list.add(vertex.value);
        
        for (Edge<V, E> edge : vertex.outEdges) {
            int toIn = ins.get(edge.to) - 1;
            if (toIn == 0) {
                queue.offer(edge.to);
            } else {
                ins.put(edge.to, toIn);
            }
        }
    }
    
    return list;
}

作业

◼ 自学《AOE网络》
◼课程表 II
https://leetcode-cn.com/problems/course-schedule-ii/

生成树(Spanning Tree)

◼生成树(Spanning Tree),也称为支撑树
连通图的极小连通子图,它含有图中全部的 n 个顶点,恰好只有 n – 1 条边

最小生成树(Minimum Spanning Tree)

◼最小生成树(Minimum Spanning Tree,简称MST)
也称为最小权重生成树(Minimum Weight Spanning Tree)、最小支撑树
是所有生成树中,总权值最小的那棵
适用于有权的连通图(无向)

◼ 最小生成树在许多领域都有重要的作用,例如
要在 n 个城市之间铺设光缆,使它们都可以通信
铺设光缆的费用很高,且各个城市之间因为距离不同等因素,铺设光缆的费用也不同
如何使铺设光缆的总费用最低?

◼ 如果图的每一条边的权值都互不相同,那么最小生成树将只有一个,否则可能会有多个最小生成树

◼ 求最小生成树的2个经典算法
Prim(普里姆算法)
Kruskal(克鲁斯克尔算法)

切分定理

◼ 切分(Cut):把图中的节点分为两部分,称为一个切分
◼ 下图有个切分 C = (S, T),S = {A, B, D},T = {C, E}

◼横切边(Crossing Edge):如果一个边的两个顶点,分别属于切分的两部分,这个边称为横切边
比如上图的边 BC、BE、DE 就是横切边

◼ 切分定理:给定任意切分,横切边中权值最小的边必然属于最小生成树

Prim算法 – 执行过程

◼假设 G = (V,E) 是有权的连通图(无向),A 是 G 中最小生成树的边集
算法从S={u0 }(u0 ∈V),A={}开始,重复执行下述操作,直到S=V为止
✓找到切分 C = (S,V – S) 的最小横切边 (u0,v0) 并入集合 A,同时将 v0 并入集合 S

Prim算法 – 实现

private Set<EdgeInfo<V, E>> prim() {
    Iterator<Vertex<V, E>> it = vertices.values().iterator();
    if (!it.hasNext()) return null;
    Vertex<V, E> vertex = it.next();
    Set<EdgeInfo<V, E>> edgeInfos = new HashSet<>();
    Set<Vertex<V, E>> addedVertices = new HashSet<>();
    addedVertices.add(vertex);
    MinHeap<Edge<V, E>> heap = new MinHeap<>(vertex.outEdges, edgeComparator);
    int verticesSize = vertices.size();
    while (!heap.isEmpty() && addedVertices.size() < verticesSize) {
        Edge<V, E> edge = heap.remove();
        if (addedVertices.contains(edge.to)) continue;
        edgeInfos.add(edge.info());
        addedVertices.add(edge.to);
        heap.addAll(edge.to.outEdges);
    }
    return edgeInfos;
}

Kruskal算法 – 执行过程

◼ 按照边的权重顺序(从小到大)将边加入生成树中,直到生成树中含有 V – 1 条边为止( V 是顶点数量)
若加入该边会与生成树形成环,则不加入该边
从第3条边开始,可能会与生成树形成环

Kruskal算法 – 实现

private Set<EdgeInfo<V, E>> kruskal() {
    int edgeSize = vertices.size() - 1;
    if (edgeSize == -1) return null;
    Set<EdgeInfo<V, E>> edgeInfos = new HashSet<>();
    MinHeap<Edge<V, E>> heap = new MinHeap<>(edges, edgeComparator);
    UnionFind<Vertex<V, E>> uf = new UnionFind<>();
    vertices.forEach((V v, Vertex<V, E> vertex) -> {
        uf.makeSet(vertex);
    });
    while (!heap.isEmpty() && edgeInfos.size() < edgeSize) {
        Edge<V, E> edge = heap.remove();
        if (uf.isSame(edge.from, edge.to)) continue;
        edgeInfos.add(edge.info());
        uf.union(edge.from, edge.to);
    }
    return edgeInfos;
}

◼时间复杂度:O ElogE

最短路径(Shortest Path)

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

最短路径 – 无权图
◼ 无权图相当于是全部边权值为1的有权图

最短路径 – 负权边

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

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

最短路径 – 负权环

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

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

最短路径

◼ 最短路径的典型应用之一:路径规划问题

◼ 求解最短路径的3个经典算法
1.单源最短路径算法
✓ Dijkstra(迪杰斯特拉算法)
✓ Bellman-Ford(贝尔曼-福特算法)
2.多源最短路径算法
✓ Floyd(弗洛伊德算法)

Dijkstra

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

◼由荷兰的科学家 Edsger Wybe Dijkstra 发明,曾在1972年获得图灵奖

Dijkstra – 等价思考

◼Dijkstra 的原理其实跟生活中的一些自然现象完全一样
把每1个顶点想象成是1块小石头
每1条边想象成是1条绳子,每一条绳子都连接着2块小石头,边的权值就是绳子的长度
将小石头和绳子平放在一张桌子上(下图是一张俯视图,图中黄颜色的是桌子)

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

◼ 有一个很关键的信息
后离开桌面的小石头
✓ 都是被先离开桌面的小石头拉起来的

Dijkstra – 执行过程

◼绿色
已经“离开桌面”
已经确定了最终的最短路径

◼ 红色:更新了最短路径信息

◼ 松弛操作(Relaxation):更新2个顶点之间的最短路径
这里一般是指:更新源点到另一个点的最短路径
松弛操作的意义:尝试找出更短的最短路径

◼ 确定A到D的最短路径后,对DC、DE边进行松弛操作,更新了A到C、A到E的最短路径

// 求最短路径
@Override
public Map<V, PathInfo<V, E>> shortestPath(V begin) {
    return dijkstra(begin);
}

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.put(beginVertex, new PathInfo<>(weightManager.zero()));

    while (!paths.isEmpty()) {
        Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = getMinPath(paths);
        // minVertex离开桌面
        Vertex<V, E> minVertex = minEntry.getKey();
        PathInfo<V, E> minPath = minEntry.getValue();
        selectedPaths.put(minVertex.value, minPath);
        paths.remove(minVertex);
        // 对它的minVertex的outEdges进行松弛操作
        for (Edge<V, E> edge : minVertex.outEdges) {
            // 如果edge.to已经离开桌面,就没必要进行松弛操作
            if (selectedPaths.containsKey(edge.to.value)) continue;
            relaxForDijkstra(edge, minPath, paths);
        }
    }
    
    selectedPaths.remove(begin);
    return selectedPaths;
}

/**
 * 从paths中挑一个最小的路径出来
 * @param paths
 * @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;
}

/**
 * 松弛
 * @param edge 需要进行松弛的边
 * @param fromPath edge的from的最短路径信息
 * @param paths 存放着其他点(对于dijkstra来说,就是还没有离开桌面的点)的最短路径信息
 */
private void relaxForDijkstra(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());
}
static void test() {
    Graph<Object, Double> graph = directedGraph(Data.SP);
    Map<Object, PathInfo<Object, Double>> sp = graph.shortestPath("A");
    if (sp == null) return;
    sp.forEach((Object v, PathInfo<Object, Double> path) -> {
        System.out.println(v + " - " + path);
    });
}

Bellman-Ford

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

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

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

Bellman-Ford – 实例

◼ 一共8条边
◼ 假设每次松弛操作的顺序是:DC、DF、BC、ED、EF、BE、AE、AB

◼ 不难分析出,经过4次松弛操作之后,已经计算出了A到其他所有顶点的最短路径

@SuppressWarnings("unused")
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++) { // v - 1 次
        for (Edge<V, E> edge : edges) {
            PathInfo<V, E> fromPath = selectedPaths.get(edge.from.value);
            if (fromPath == null) continue;
            relax(edge, fromPath, selectedPaths);
        }
    }
    
    for (Edge<V, E> edge : edges) {
        PathInfo<V, E> fromPath = selectedPaths.get(edge.from.value);
        if (fromPath == null) continue;
        if (relax(edge, fromPath, selectedPaths)) {
            System.out.println("有负权环");
            return null;
        }
    }
    
    selectedPaths.remove(begin);
    return selectedPaths;
}
/**
 * 松弛
 * @param edge 需要进行松弛的边
 * @param fromPath edge的from的最短路径信息
 * @param paths 存放着其他点(对于dijkstra来说,就是还没有离开桌面的点)的最短路径信息
 */
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;
}
static void test() {
    Graph<Object, Double> graph2 = directedGraph(Data.SP);
    Map<Object, PathInfo<Object, Double>> sp2 = graph2.shortestPath2("A");
    if (sp2 == null) return;
    sp2.forEach((Object v, PathInfo<Object, Double> path) -> {
        System.out.println(v + " - " + path);
    });
}

Floyd

◼Floyd 属于多源最短路径算法,能够求出任意2个顶点之间的最短路径,支持负权边
时间复杂度:O(V3),效率比执行 V 次 Dijkstra 算法要好( V 是顶点数量)

◼ 算法原理
从任意顶点 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 的最短路径的距离

// Floyd - 多源最短路径算法
@Override
public Map<V, Map<V, PathInfo<V, E>>> shortestPathFloyd() {
    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<V, E>();
                    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;
}
static void test() {
    Graph<Object, Double> graph = directedGraph(Data.NEGATIVE_WEIGHT1);
    Map<Object, Map<Object, PathInfo<Object, Double>>> sp = graph.shortestPathFloyd();
    sp.forEach((Object from, Map<Object, PathInfo<Object, Double>> paths) -> {
        System.out.println(from + "---------------------");
        paths.forEach((Object to, PathInfo<Object, Double> path) -> {
            System.out.println(to + " - " + path);
        });
    });
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容