13. 图(Graph)

本文详细介绍了图的概念,包括有向图、无向图、混合图、简单图和多重图。接着探讨了无向完全图和有向完全图,以及有权图的概念。还讲解了连通图、强连通图及其连通分量和强连通分量。重点讨论了图的实现方案,如邻接矩阵和邻接表,并介绍了图的遍历方法,包括广度优先搜索(BFS)和深度优先搜索(DFS)。此外,文章涵盖了生成树、最小生成树的Prim和Kruskal算法,以及最短路径问题,如Dijkstra、Bellman-Ford和Floyd算法。最后,简要提及了图的代码实现。
摘要由CSDN通过智能技术生成
1 图(Graph)
  1. 图由顶点(vertex)和边(edge)组成,通常表示为 G = (V, E)
    1. G表示一个图,V是顶点集,E是边集
    2. 顶点集V有穷且非空
    3. 任意两个顶点之间都可以用边来表示它们之间的关系,边集E可以是空的
2 有向图(Directed Graph)
  1. 有向图的边是有明确方向的
  2. 有向无环图(Directed Acyclic Graph,简称 DAG):如果一个有向图,从任意顶点出发无法经过若干条边回到该顶点,那么它就是一个有向无环图
    在这里插入图片描述
  3. 出度、入度
    1. 出度、入度适用于有向图
    2. 出度(Out-degree)
      1. 一个顶点的出度为 x,是指有 x 条边以该顶点为起点。
      2. 顶点11的出度是3
    3. 入度(In-degree)
      1. 一个顶点的入度为 x,是指有 x 条边以该顶点为终点
      2. 顶点11的入度是2
        在这里插入图片描述
3 无向图(Undirected Graph)
  1. 无向图的边是无方向的
  2. 效果类似于下面的有向图
    在这里插入图片描述
4 混合图(Mixed Graph)
  1. 混合图的边可能是无向的,也可能是有向的
    在这里插入图片描述
5 简单图、多重图
  1. 平行边
    1. 在无向图中,关联一对顶点的无向边如果多于1条,则称这些边为平行边
    2. 在有向图中,关联一对顶点的有向边如果多于1条,并且它们的的方向相同,则称这些边为平行边
  2. 多重图(Multigraph)
    1. 有平行边或者有自环的图
  3. 简单图(Simple Graph)
    1. 既没有平行边也不没有自环的图
    2. 课程中讨论的基本都是简单图
6 无向完全图(Undirected Complete Graph)
  1. 无向完全图的任意两个顶点之间都存在边
  2. n 个顶点的无向完全图有 n(n − 1)/2 条边
    在这里插入图片描述
7 有向完全图(Directed Complete Graph)
  1. 有向完全图的任意两个顶点之间都存在方向相反的两条边
  2. n 个顶点的有向完全图有 n(n − 1) 条边
  3. 稠密图(Dense Graph):边数接近于或等于完全图
  4. 稀疏图(Sparse Graph):边数远远少于完全图
8 有权图(Weighted Graph)
  1. 有权图的边可以拥有权值(Weight)
    在这里插入图片描述
9 连通图(Connected Graph)
  1. 如果顶点 x 和 y 之间存在可相互抵达的路径(直接或间接的路径),则称 x 和 y 是连通的
  2. 连通图是无向图的一种
  3. 如果无向图 G 中任意2个顶点都是连通的,则称G为连通图
  4. 连通分量(Connected Component)
    1. 连通分量指无向图的极大连通子图
    2. 连通图只有一个连通分量,即其自身;非连通的无向图有多个连通分量
    3. 下面的无向图有3个连通分量
      在这里插入图片描述
10 强连通图(Strongly Connected Graph)
  1. 强连通图是有向图的一种
  2. 如果有向图 G 中任意2个顶点都是连通的,则称G为强连通图
    在这里插入图片描述
  3. 强连通分量(Strongly Connected Component)指有向图的极大强连通子图
  4. 强连通图只有一个强连通分量,即其自身;非强连通的有向图有多个强连通分量
    在这里插入图片描述
11 图的实现方案
11.1 邻接矩阵(Adjacency Matrix)
  1. 邻接矩阵的存储方式
    1. 一维数组存放顶点信息
    2. 二维数组存放边信息
  2. 邻接矩阵比较适合稠密图
    1. 不然会比较浪费内存
    2. 可以观察同一条边,例如v2到v1和v1到v2会存储两遍,同时不存在的边,例如v0到v0也会分配内存
      在这里插入图片描述
  3. 邻接矩阵 – 有权图
    在这里插入图片描述
11.2 邻接表(Adjacency List)
  1. 一维数组存放顶点信息
  2. 使用链表存储以该顶点为起点的边的信息,或以该顶点为终点的边的信息(逆邻接表)
    在这里插入图片描述
  3. 邻接表 – 有权图
    在这里插入图片描述
  4. 自己的实现类似邻接表,存放顶点、以该顶点为起点的边、以该顶点为终点的边
12 图的遍历
  1. 从图中某一顶点出发访问图中其余顶点,且每一个顶点仅被访问一次
12.1 广度优先搜索(Breadth First Search,BFS)
  1. 之前所学的二叉树层序遍历就是一种广度优先搜索,所以bfs方法和层序遍历一样,也可以使用队列实现
  2. 先找到距起点一步的所有节点,再找距离第一步节点一步的所有节点,直到所有节点遍历完毕
  3. 程序流程
    1. 将起点入队
    2. 将起点出队并访问,同时将以A为起点的所有边的终点入队
    3. 循环直到队列为空
    4. 期间注意,放入队列过(不管是否还在队列中)的顶点,就不要再次放入队列了
  4. 有向图使用广度优先搜索遍历时,要遵守方向
    在这里插入图片描述
12.2 深度优先搜索(Depth First Search,DFS)
  1. 之前所学的二叉树前序遍历就是一种深度优先搜索,所以也可以通过递归或栈的方式实现
  2. 从起点开始,有多深走多深,到头之后往回退,看是否还有其他分支,有分支走分支,没分支继续回退
  3. 递归程序流程
    1. 先访问该顶点
    2. 将该顶点加入到访问过的顶点集合中
    3. 取出所有以该顶点为起点的边
      1. 如果这些边的终点已经访问过,那么跳过对该条边的处理
      2. 否则对该边的终点,使用第1、2、3步递归进行处理
  4. 使用栈程序流程
    1. 将起点入栈、访问、加入到已访问过的起点集合中
    2. 只要栈不为空,循环执行以下操作
      1. 出栈一个顶点,拿到该顶点为起点的所有边进行循环处理
        1. 如果该边终点已经访问过,跳过对该边的处理
        2. 否则将该边的起点入栈、终点入栈、访问终点,将终点加入到访问过的集合中
        3. 找到一条本次可以处理的边就退出当前循环,这样相当于可以对栈顶的那个更深处的顶点,继续往更深处处理
          在这里插入图片描述
13 AOV网(Activity On Vertex Network)
  1. 一项大的工程常被分为多个小的子工程
  2. 子工程之间可能存在一定的先后顺序,即某些子工程必须在其他的一些子工程完成后才能开始
  3. 在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,子工程被称为活动(Activity)
  4. 以顶点表示活动、有向边表示活动之间的先后关系,这样的图简称为 AOV 网
  5. 标准的AOV网必须是一个有向无环图(Directed Acyclic Graph,简称 DAG)
  6. 前驱活动:有向边起点的活动称为终点的前驱活动。只有当一个活动的前驱全部都完成后,这个活动才能进行
  7. 后继活动:有向边终点的活动称为起点的后继活动
  8. 拓扑排序(Topological Sort):将 AOV 网中所有活动排成一个序列,使得每个活动的前驱活动都排在该活动的前面
  9. 比如上图的拓扑排序结果是:A、B、C、D、E、F 或者 A、B、D、C、E、F (结果并不一定是唯一的)
    在这里插入图片描述
  10. 可以使用卡恩算法(Kahn于1962年提出)完成拓扑排序
  11. 假设 L 是存放拓扑排序结果的列表
  12. 把所有入度为 0 的顶点放入 L 中,然后把这些顶点从图中去掉
  13. 重复操作1,直到找不到入度为 0 的顶点
  14. 如果此时 L 中的元素个数和顶点总数相同,说明拓扑排序完成
  15. 如果此时 L 中的元素个数少于顶点总数,说明原图中存在环,无法进行拓扑排序
    在这里插入图片描述
  16. 程序执行过程
    1. 为了不真正删除图中顶点,先建立一个顶点-入度对照表ins,存放入度不为0的所以顶点和入度对照关系
    2. 将初始入度为0的顶点,放入queue队列中
    3. 只要队列中元素不为空,执行如下循环
      1. 将队列中元素出队,并放入最终的拓扑排序结果list中
      2. 假想该顶点已经从整个图中去除,那么以改顶点为起点的所有边的终点的入度都应该减少1
        1. 如果减少1后,入度变为0,那么应该将该顶点放入queue队列中,保证循环继续
        2. 如果减少1后,入度仍不为0,那么应该更新顶点-入度对照表中的该顶点入度信息
14 生成树(Spanning Tree)
  1. 生成树(Spanning Tree):也称为支撑树(用最少的边,支撑了所有的顶点),是连通图的一种(所以是无向的)
  2. 极小连通子图:用最少的边构成的连通图
  3. 生成树是连通图的极小连通子图,如果连通图有n个顶点,那么生成树就恰好有n-1条边
  4. 最小生成树(Minimum Spanning Tree)
    1. 也称为最小权重生成树(Minimum Weight Spanning Tree)、最小支撑树
    2. 是所有生成树中,总权值最小的那棵
    3. 如果图的每一条边的权值都互不相同,那么最小生成树将只有一个,否则可能会有多个最小生成树
14.1 Prim(普里姆算法)
  1. 切分定理
    1. 给定任意切分,横切边中权值最小的边必然属于最小生成树
    2. 切分(Cut):把图中的节点分为两部分,称为一个切分
    3. 下图有个切分 C = (S, T),S = {A, B, D},T = {C, E}
    4. 横切边(Crossing Edge):如果一个边的两个顶点,分别属于切分的两部分,这个边称为横切边,比如上图的边 BC、BE、DE 就是横切边
    5. 由于横切边会将整个树分成两部分,也就是说,如果想让这两部分树连通,一定要选择其中一个横切边,对他们进行连接,横切边中权重最小的那条边,一定属于该图的最小生成树
    6. 以所有选好的边的所有顶点为起点的,不属于选好的边的,所有边,就是下一次切分的横切边
      在这里插入图片描述
  2. Prim算法执行过程
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  3. 程序执行过程
    1. 先找到任意一个顶点,将该顶点加入到已添加顶点集合addedVertices中
    2. 将以该顶点为起点的所有边放入一个小顶堆中
    3. 只要已添加的顶点数,小于整个图的顶点数,说明还没有将所有点连接起来,因此应该执行如下循环
      1. 从堆中找到这些边中权值最小的一条边,并从堆中删除
      2. 将该边加入到最后返回的,表示最小生成树所有边的edgeInfos集合中
      3. 将这条边的终点,加入到已添加顶点集合addedVertices中
      4. 将所有以该终点为起点的所有边,加入到堆中
    4. 循环结束后,得到的就是最后包含最小生成树的所有边信息的集合
14.2 Kruskal(克鲁斯克尔算法)
  1. Kruskal算法执行过程
    1. 按照边的权重顺序(从小到大)将边加入生成树中,直到生成树中含有 V – 1 条边为止( V 是顶点数量)
      1. 若加入该边会与生成树形成环,则不加入该边
      2. 从第3条边开始,可能会与生成树形成环
        在这里插入图片描述
        在这里插入图片描述
        在这里插入图片描述
  2. 程序执行过程
    1. 将所有边加入到最小堆中
    2. 将所有顶点加入到并查集中
    3. 只要选出的边,还未达到顶点数-1,也就是还未选出最小生成树,就执行以下循环
      1. 从堆中找到权值最小的边,并从堆中删除该边
      2. 判断该边的起点和终点,在并查集中,是否属于同一个子集合,如果属于,说明这条边会导致形成环,因此不能选用该边
      3. 如果不属于同一个子集合,该边就是最小生成树的边,放入最终需要返回的最小生成树边集合edgeInfos中
      4. 在并查集中,对该边的终点和起点进行合并,让他们归属于同一个子集合
15 最短路径(Shortest Path)
  1. 最短路径是指两顶点之间权值之和最小的路径(有向图、无向图均适用,不能有负权环)
    1. 负权环:构成环的整个权重加起来是负的。如果有负权环,是根本无法算出最小路径的
    2. 负权边:边的权重为负数。Dijkstra算法,不能计算图中含有负权边的最短路径
15.1 Dijkstra算法
  1. Dijkstra 属于单源最短路径算法,用于计算一个顶点到其他所有顶点的最短路径
  2. 使用前提:不能有负权边
  3. 时间复杂度:可优化至 O (ElogV) ,E 是边数量,V 是节点数量
  4. Dijkstra的等价思考
    1. Dijkstra 的原理其实跟生活中的一些自然现象完全一样
    2. 把每1个顶点想象成是1块小石头
    3. 每1条边想象成是1条绳子,每一条绳子都连接着2块小石头,边的权值就是绳子的长度
    4. 将小石头和绳子平放在一张桌子上(下图是一张俯视图,图中黄颜色的是桌子)
    5. 接下来想象一下,手拽着小石头A,慢慢地向上提起来,远离桌面
    6. B、D、C、E会依次离开桌面
    7. 最后绷直的绳子就是A到其他小石头的最短路径
    8. 注:后离开桌面的小石头,都是被先离开桌面的小石头拉起来的,所以下图中,路径长度最短的那个终点一定是下一个离开地面的"石头"
      在这里插入图片描述
      在这里插入图片描述
  5. Dijkstra算法执行过程
    1. 找到以A为起点的所有边,其中路径长度最短的,就是到该终点的最短路径
    2. 对以该终点为起点的所有边进行松弛操作
      1. 对边进行松弛操作,其实就是指用源点到该边的起点的最短路径+该边的权值,重新计算源点到该边终点的距离
      2. 目的是看是否能得到比原来统计出的源点到该边终点的权值,更小的权值
    3. 重新找到路径长度中最短的,循环执行上述操作
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
  6. 程序执行过程
    1. 先只将A这个终点,以及A到A的最短路径,权值0,放入用于存放红色和蓝色记录的paths中
    2. 只要paths中不为空,也就是未找到所有最短路径,执行如下循环
      1. 获取paths中当前路径长度最短的那条路径
      2. 将这条路径信息放入到最终需要返回的存放所有最短路径的selectedPaths集合中
      3. 从paths中删除这条路径
      4. 将以这条路径的终点为起点的所有边,放入到paths中,循环执行如下操作
        1. 如果该边的终点的最短路径已经找到,那么不做处理
        2. 如果未被找到,对该边进行松弛操作
          1. 也就是使用已经选好的、该边起点的最小路径+该边权值,与之前该边起点的最小路径进行比较
          2. 如果前者更小,那么更新paths中最短路径、路径长度等信息
          3. 如果后者更小,不做处理
      5. 将selectedPaths中,作为辅助用的,以A为起点、A为终点、A-A为最短路径,0为路径长度的那条信息删除
15.2 Bellman-Ford算法
  1. Bellman-Ford 也属于单源最短路径算法,支持负权边,还能检测出是否有负权环
  2. 时间复杂度:O(EV) ,E 是边数量,V 是节点数量
  3. 最好情况对所有边仅需进行 1 次松弛操作就能计算出A到达其他所有顶点的最短路径
  4. 注:如果一个路径起点的最短路径都没有得到,那么改路径一定无法松弛成功
    在这里插入图片描述
  5. Bellman-Ford算法执行过程
    1. 对所有的边进行 V – 1 次松弛操作( V 是节点数量),就能得到所有可能的最短路径
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
  6. 程序执行过程
    1. 先将A为终点、A->A为最短路径,0为路径长度这些信息放入最终要返回的记录着终点、最短路径、路径长度信息的selectedPaths集合中
    2. 循环执行下面过程顶点个数-1次
      1. 循环所有的边,对它们进行松弛操作
    3. 此时可以尝试对所有边再进行一次松弛操作,如果有一次松弛成功,就说明该图中有负权环
    4. 将selectedPaths中,作为辅助用的,以A为起点、A为终点、A-A为最短路径,0为路径长度的那条信息删除
15.3 Floyd算法
  1. Floyd 属于多源最短路径算法,能够求出任意2个顶点之间的最短路径,支持负权边
  2. 时间复杂度:O(V 3 ),效率比执行 V 次 Dijkstra 算法要好( V 是顶点数量)
  3. 算法原理:
    1. 从任意顶点 i 到任意顶点 j 的最短路径不外乎两种可能
      1. 直接从 i 到 j
      2. 从 i 经过若干个顶点到 j
    2. 假设 dist(i,j) 为顶点 i 到顶点 j 的最短路径的距离
    3. 对于每一个顶点 k,检查 dist(i,k) + dist(k,j)<dist(i,j) 是否成立
      1. 如果成立,证明从 i 到 k 再到 j 的路径比 i 直接到 j 的路径短,设置 dist(i,j) = dist(i,k) + dist(k,j)
      2. 当我们遍历完所有结点 k,dist(i,j) 中记录的便是 i 到 j 的最短路径的距离
  4. 程序执行过程
    1. 先准备一个最终需要返回的,存放从任意顶点到任意顶点最短路径信息的paths
    2. 对paths进行初始化,将当前所有边的起点、终点、路径信息,放入其中
    3. 对所有顶点,展开三层循环,最外层表示顶点k,变量设为v2,中间一层表示i,变量设为v1,最后一层表示j,变量设为v3,最内层循环执行如下步骤
      1. 判断如果三层循环中的顶点,有任意两层是相同的,直接退出本次循环
      2. 如果v1到v2或v2到v3目前为止还未被找到一条路径,就退出本次循环
      3. 如果上面都符合,且v1到v2+v2到v3,小于v1到v3,那么更新paths中v1到v3的路径
    4. 循环结束后,paths就是最终的结果
16 相关代码
  1. Data:用于构建指定的图
package com.mj;

public class Data {
   
	
	public static final Object[][] BFS_01 = {
   
			{
   "A", "B"}, {
   "A", "F"},
			{
   "B", "C"}, {
   "B", "I"}, {
   "B", "G"}, 
			{
   "C", "I"}, {
   "C", "D"},
			{
   "D", "I"}, {
   "D", "G"}, {
   "D", "E"}, {
   "D", "H"},   	
			{
   "E", "H"}, {
   "E", "F"},
			{
   "F", "G"},
			{
   "G", "H"},
	};
	
	public static final Object[][] BFS_02 = {
   
			{
   0, 1}, {
   0, 4},
			{
   1, 2},
			{
   2, 0}, {
   2, 4}, {
   2, 5},
			{
   3, 1}, 
			{
   4, 6}, {
   4, 7},
			{
   5, 3}, {
   5, 7},
			{
   6, 2}, {
   6, 7}
	};
	
	public static final Object[][] BFS_03 = {
   
			{
   0, 2}, {
   0, 3},
			{
   1, 2}, {
   1, 3}, {
   1, 6},
			{
   2, 4},
			{
   3, 7}, 
			{
   4, 6},
			{
   5, 6},
			{
   6, 7}
	};
	
	public static final Object[][] BFS_04 = {
   
			{
   1, 2}, {
   1, 3}, {
   1, 5},
			{
   2, 0},
			{
   3, 5}, 
			{
   5, 6}, {
   5, 7},
			{
   6, 2},
			{
   7, 6}
	};
	
	public static final Object[][] DFS_01 = {
   
			{
   0, 1},
			{
   1, 3}, {
   1, 5}, {
   1, 6}, {
   1, 2},
			{
   2, 4},
			{
   3, 7}
	};
	
	public static final Object[][] DFS_02 = {
   
			{
   "a", "b"}, {
   "a", "e"},
			{
   "b", "e"},
			{
   "c", "b"},
			{
   "d", "a"},
			{
   "e", "c"}, {
   "e", "f"},
			{
   "f", "c"}
	};
	
	public static final Object[][] TOPO = {
   
			{
   0, 2},
			{
   1, 0},
			{
   2, 5}, {
   2, 6},
			{
   3, 1}, {
   3, 5}, {
   3, 7},
			{
   5, 7},
			{
   6, 4},
			{
   7, 6}
	};
	
	public static final Object[][] NO_WEIGHT2 = {
   
			{
   0, 3}, 
			{
   1, 3}, {
   1, 6},
			{
   2, 1}, 
			{
   3, 5}, 
			{
   6, 2}, {
   6, 5},
			{
   4, 7}
	};
	
	public static final Object[][] NO_WEIGHT3 = {
   
			{
   0, 1}, {
   0, 2},
			{
   1, 2}, {
   1, 5},
			{
   2, 4}, {
   2, 5},
			{
   5, 6}, {
   7, 6},
			{
   3}
	};
	
	public static final Object[][] MST_01 = {
   
			{
   0, 2, 2}, {
   0, 4, 7},
			{
   1, 2, 3}, {
   1, 5, 1}, {
   1, 6, 7},
			{
   2, 4, 4}, {
   2, 5, 3}, {
   2, 6, 6},
			{
   3, 7, 9},
			{
   4, 6, 8},
			{
   5, 6, 4}, {
   5, 7, 5}
	};
	
	public static final Object[][] MST_02 = {
   
			{
   "A", "B", 17}, {
   "A", "F", 1}, {
   "A", "E", 16},
			{
   "B", "C", 6}, {
   "B", "D", 5}, {
   "B", "F", 11},
			{
   "C", "D", 10}, 
			{
   "D", "E", 4}, {
   "D", "F", 14},
			{
   "E", "F", 33}
	};
	
	public static final Object[][] WEIGHT3 = {
   
			{
   "广州", "佛山", 100}, {
   "广州", "珠海", 200}, {
   "广州", "肇庆", 200},
			{
   "佛山", "珠海", 50}, {
   "佛山", "深圳", 150}, 
			{
   "肇庆", 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值