作者: iOS小洁 | 来源:发表于2023-02-05 21:15 被阅读0次

    图G由顶点集V(vertex)和边集E(edge)组成,通常表示为G= (V, E)

    V 非空

    E 可以为空

    图的应用:社交网络,地图导航。。。

    有向图:边有明确方向的图

    有向无环图:从任意顶点出发,无法经过若干条边回到该顶点,就是有向无环图

    出度:一个顶点的出度为x,指的是有x条边以该顶点为起点

    入度:一个顶点的出度为x,指的是有x条边以该顶点为终点

    无向图:边无方向的图

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

    平行边

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

    多重图:有平行边或者有自环的图

    简单图:既没有平行边也没有自环的图。

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

    无向完全图

    无向完全图的任意两个顶点之间都存在边

    n 个顶点的无向完全图有 n ( n − 1 )/ 2 条边

    有向完全图

    有向完全图的任意两个顶点之间都存在方向相反的两条边

    n 个顶点的有向完全图有 n ( n − 1 ) 条边

    稠密图(Dense Graph):边数接近于或等于完全图

    稀疏图(Sparse Graph):边数远远少于完全图

    连通图

    如果顶点 x 和 y 之间存在可相互抵达的路径(直接或间接的路径),则称 x 和 y 是连通的

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

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

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

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

    图的实现

    实现方案有两种:1、邻接矩阵(Adjacency Matrix),2、邻接表(Adjacency List)

    邻接矩阵

    邻接矩阵的存储方式 :

    • 一维数组存放顶点信息
    • 二维数组存放边信息
    顶点数组
    𝜈0 𝜈1 𝜈2 𝜈3 𝜈4
    边数组
    𝜈0 𝜈1 𝜈2 𝜈3 𝜈4
    𝜈0 6
    𝜈1 9 3
    𝜈2 2 5
    𝜈3 1
    𝜈4

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

    接口设计

    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);
    

    顶点的定义

    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();
        }
        @Override
        public String toString() {
            return value == null ? "null" : value.toString();
        }
    }
    

    边的定义

    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();
            }
            @Override
            public String toString() {
                return value == null ? "null" : value.toString();
            }
        }   
    

    图的遍历

    广度优先搜索(Breadth First Search,BFS),又称为宽度优先搜索、横向优先搜索。二叉树的层序遍历

    深度优先搜索(Depth First Search,DFS) 。二叉树的前序遍历

    广度优先:

    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);
            }
        }
    }
    

    深度优先:

    // 递归实现
    private void dfs2(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);
        }
    }
    
    // 非递归实现
    public void dfs(V begin) {
        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);
        System.out.println(beginVertex.value);
    
        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);
                System.out.println(edge.to.value);
    
                break;
            }
        }
    }
    

    AOV网

    一项大的工程常被分为多个小的子工程

    子工程之间可能存在一定的先后顺序,即某些子工程必须在其他的一些子工程完成后才能开始

    在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,子工程被称为活动(Activity)

    以顶点表示活动、有向边表示活动之间的先后关系,这样的图简称为 AOV 网

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

    拓扑排序

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

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

    什么是拓扑排序? 将 AOV 网中所有活动排成一个序列,使得每个活动的前驱活动都排在该活动的前面(结果并不一定是唯一的)

    拓扑排序 – 思路:

    可以使用卡恩算法(Kahn于1962年提出)完成拓扑排序

    • 假设 L 是存放拓扑排序结果的列表
      • ① 把所有入度为 0 的顶点放入 L 中,然后把这些顶点从图中去掉
      • ② 重复操作 ①,直到找不到入度为 0 的顶点
    • 如果此时 L 中的元素个数和顶点总数相同,说明拓扑排序完成
    • 如果此时 L 中的元素个数少于顶点总数,说明原图中存在环,无法进行拓扑排序
    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;
    }
    

    生成树(Spanning Tree)

    生成树(Spanning Tree),也称为支撑树

    连通图的极小连通子图,它含有图中全部的 n 个顶点,恰好只有 n – 1 条边

    最小生成树

    (Minimum Spanning Tree,简称MST) 也称为最小权重生成树(Minimum Weight Spanning Tree)、最小支撑树

    是所有生成树中,总权值最小的那棵 。适用于有权的连通图(无向)

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

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

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

    切分定理

    切分(Cut):把图中的节点分为两部分,称为一个切分

    横切边(Crossing Edge):如果一个边的两个顶点,分别属于切分的两部分,这个边称为横切边

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

    Prim算法

    假设 G = (V,E) 是有权的连通图(无向),A 是 G 中最小生成树的边集

    算法从 S = { u 0 }( u 0 ∈ V),A = { } 开始,重复执行下述操作,直到 S = V 为止

    • 找到切分 C = (S,V – S) 的最小横切边 ( u 0 , v 0 ) 并入集合 A,同时将 v 0 并入集合 S
    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条边开始,可能会与生成树形成环

    时间复杂度: O (Elo g E)

    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;
    }
    

    最短路径(Shortest Path)

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

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

    有负权边,但没有负权环时,存在最短路径。有负权环时,不存在最短路径。因为转环可以无限小

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

    求解最短路径的3个经典算法

    • 单源最短路径算法
      • Dijkstra(迪杰斯特拉算法)
      • Bellman-Ford(贝尔曼-福特算法)
    • 多源最短路径算法
      • Floyd(弗洛伊德算法)

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

    Dijkstra

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

    • 把每1个顶点想象成是1块小石头 每1条边想象成是1条绳子,每一条绳子都连接着2块小石头,边的权值就是绳子的长度 将小石头和绳子平放在一张桌子上
    • 接下来想象一下,手拽着小石头,慢慢地向上提起来,远离桌面,其他石头会依次被提起来
    • 后离开桌面的小石头都是被先离开桌面的小石头拉起来的

    Bellman-Ford

    Bellman-Ford 也属于单源最短路径算法,支持负权边,还能检测出是否有负权环

    算法原理:对所有的边进行 V – 1 次松弛操作( V 是节点数量),得到所有可能的最短路径

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

    最坏情况是恰好每次都从右到左的顺序对边进行松弛操作

    对所有边需进行 V – 1 次松弛操作才能计算出A到达其他所有顶点的最短路径

    Floyd

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

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

    算法原理

    • 从任意顶点 i 到任意顶点 j 的最短路径不外乎两种可能
      • ① 直接从 i 到 j
      • ② 从 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 的最短路径的距离

    相关文章

      网友评论

        本文标题:

        本文链接:https://www.haomeiwen.com/subject/oeezhdtx.html