作者: 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