图的应用
一.几个重要的概念
生成树和连通分量
-
生成树是包含图中全部顶点的一个极小连通子图。
极小连通子图是指在一个图中,如果我们从该子图中移除任何一个顶点或边,那么这个子图就不再是连通的。
-
连通分量是无向图的极大连通子图,包含尽可能多的顶点和边。
例如:以下图示左边是原图,因为它本身就是连通的,所以它的连通分量就是它本身(因为此情况他保留了最多的顶点和边),而他的生成树则要求他把所有的顶点保留,然后一直去边, 直到无法通过去掉任何一个顶点或边来减小它的连通性。
二.最小生成树
1.概念
最小生成树是在一副连通加权无向图中,权值最小的生成树。
具体来说:给定一个无向图,其中的边带有权值,我们要找到一个生成树,使得这棵树包含了原图中的所有顶点,并且树上的边的权值之和最小。
2.现实意义
在城市规划,网络设计,电路布局方面,能够给我们提供好的方案,以达到节约成本的效果。
3.求最小生成树
求最小生成树的方法包括Kurskal算法和prim算法
Kruskal算法基本思想:从小到大加入边(不要导致出现闭环哦),是一种贪心算法。
Prim 算法基本思想:从一个结点开始,不断加点,以及用新的边更新其他结点的距离。
4.注意:
1)最小生成树可能有多个,只要保证其保留了所有顶点,且边的权值之和是唯一,最小的即可。
2)只有连通图才有生成树。
二.关键路径
1.AOE图
在带权有向图中,以顶点表示时间,以有向边表示活动,以边上的权值表示该活动的开销。
从源点(开始顶点)到汇点(结束顶点)的有向路径可能有多条。所有路径中,具有最大路径长度的路径成为关键路径,而把关键路径上的活动成为关键活动。
2.相关概念
1)事件:例如v1,v2这些
活动:例如a1,a2这些
2)事件的最早发生事件 事件的最晚发生事件
活动的最早开始时间 活动的最晚开始时间
活动ai的时间余量d(i)=l(i)-e(i),即活动的最晚开始时间-活动的最早开始时间
若一个活动的事件余量为0,则说明该活动必须要如期完成。
d(i)=0即l(i)=e(i)的活动a(i)即为关键活动
由关键活动组成的路径就是关键路径
3.特性
1)若关键活动耗时增加,则整个工程的工期将增长
2)缩短关键活动的时间,可以缩短整个工程的工期
3)当缩短到一定程度时,关键活动可能会变成非关键活动
4)可能有多条关键路径,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。
三.有向无环图
首先,将表达式中的操作数不重复地排成一排。然后,标出各个运算符的生效顺序(先后顺序有点出入无所谓)。按顺序在DAG图中加入运算符,一层层的叠上去。
注意:
-
在DAG图中,每一层表示一个运算符的生效顺序。
-
利用下一层的结果作为上一层的操作数,逐步构建DAG图。
-
用DAG图存储,得到的图是不唯一的,因为我们表达式生效的次序不同。
四.最短路径问题
1.BFS求无权图的单源最短路径问题
算法思想
1)首先,让我们明确一下问题:我们要找到从一个起点到图中其他顶点的最短路径。这里的图是无权图,也就是说,图中的边没有权重或者权重都相等。
2)BFS(广度优先搜索)是一种层序遍历算法,它总是按照距离由近到远来遍历图中的每个顶点。
3)以下面图示为例,我们要计算从2出发到其余各个顶点的最短路径,我们使用BFS来实现,这里要还要借助如图4-2所示的三个数组:
distance数组记录每个顶点到起点的距离
front数组记录从起点到该顶点的路径上,该顶点的前一个顶点。
(我写这个数组只是为了方便手写计算距离,但其实这个数组在我们的代码实现上并没有用到,因为我们使用了一个队列来进行元素的弹出与弹入,利用队列的天然特性加上无向图,即可天然记录某一个顶点的前一个顶点是谁,以对其到起点的距离进行正确标识,例如2的下一层即为1,6,则1,6的距离就为2到起点的距离+1,即0+1=1)
visited记录该顶点是否被访问过
经过一顿操作之后,这3个数组会变成如图 4-3所示。
图4-1
图4-2
图4-3
代码实现
-
首先,将起点加入队列,并将起点到起点的距离设为 0。
-
在循环中,每次从队列中弹出一个顶点,记录其距离,并将其邻接点加入队列(如果邻接点未被访问过)。
循环结束后,
distance
中存储了每个顶点到起点的最短距离。
可以看到,在代码中,我们利用队列将顶点访问进行了分层
第一层为2
第二层为2的领接顶点(即1,6)
第三层为1,6的领接顶点(即5,3,7)
第四层为5,3,7的领接顶点(即4,8)
public static HashMap<Character, Integer> bfs(HashMap<Character, List<Character>> graph, char start) { // 用于维护遍历顺序 Queue<Character> queue = new LinkedList<>(); // 用于维护遍历顺序 Set<Character> visited = new HashSet<>(); //用于记录每个点到起点的距离 HashMap<Character, Integer> distance = new HashMap<>(); // 1.将起点加入队列 queue.add(start); int levelDistance = 0; while (!queue.isEmpty()) { // 遍历当前层的顶点数 int levelSize = queue.size(); for (int i = 0; i < levelSize; i++) { //弹出队首的元素,设置其到起点的位置,并将该顶点设置为已标记 char vertex = queue.poll(); distance.put(vertex, levelDistance); visited.add(vertex); //遍历该元素的领接点,将其中未被访问的顶点加入队列queue中。 for (char nextVertex : graph.get(vertex)) { if (!visited.contains(nextVertex)) { queue.add(nextVertex); } } } levelDistance++; // 距离增加一层 } return distance; // 返回每个顶点到起点的距离 }
2.Dijkstra算法
问题描述:
-
给定一个有向或无向图,每条边都有一个非负的权重(距离)。
-
我们要找到从一个源节点到其他所有节点的最短路径。
基本思想:
-
Dijkstra 算法采用贪心策略,逐步找到从源节点到其他所有节点的最短路径。
-
算法的核心思想是不断扩展当前已确定最短路径的节点集合,逐步逼近最短路径。
具体步骤:
我们需要借助三个数组,
success数组用来标记各顶点是否已找到最短路径
length数组用来标记最短路径长度
path数组用来标记路径上的前驱
如图4-4所示,以v0为起点,则将success[0]标记为true,
与v0相连的点有V0,V1,V4,可以更新length数组和front数组中对应的值。
图4-3
算法的过程如下:
循环遍历所有结点,找到还没确定最短路径,且length最⼩的顶点Vi,令successl[i]=ture
检查所有邻接⾃ Vi 的顶点,若其 success 值为false, 则更新 length和 front 信息。
依照这个过程去更新这三个数组,最终我们能够在length数组中找到每一个顶点到起点的最短距离。
图4-4
五.图的存储结构
十字链表(Orthogonal List)
介绍
1)十字链表主要用于有向图,它解决了无法快速找到一个顶点的入边的问题。
2)十字链表将有向图的顶点和边分别存储在两个链表中,从而有效地表示图的结构。
3)十字链表的定义如下:
代码实现
class ArcNode { int tailvex; // 弧尾在顶点数组中的位置下标 int headvex; // 弧头在顶点数组中的位置下标 ArcNode hlink; // 指向弧头相同的下一条弧 ArcNode tlink; // 指向弧尾相同的下一条弧 int weight; // 权值 } class VexNode { char data; // 顶点数据 ArcNode firstin; // 指向以该顶点为弧头的第一个弧 ArcNode firstout; // 指向以该顶点为弧尾的第一个弧 } class OLGraph { VexNode[] vexs; // 存储顶点的一维数组 int vexnum; // 图的顶点数 int arcnum; // 图的弧数 }
邻接多重表:
定义
1)邻接多重表主要用于无向图,它解决了重复存储同一条边两次的问题。
-
邻接多重表可以看作是邻接表和十字链表的结合。
-
邻接多重表的定义类似于十字链表,但是只用一个结点来表示无向图中的每条边。
-
邻接多重表的基本存储结构如下:
-
顶点表节点:
-
data
:顶点数据域 -
firstedge
:边表节点的头指针
-
-
边表节点:
-
ivex
:与结点i相关联的边 -
jvex
:与结点j相关联的边 -
ilink
:与结点i相邻的下一个边表节点的指针 -
jlink
:与结点j相邻的下一个边表节点的指针 -
info
:权值(如果有的话)
-
-
例如
边b的两个顶点是D和C,则在边b的数据结构中,我们能找到顶点D,顶点C,还能找到顶点D的另一条边a,C的另一条边c。
代码实现
class EdgeNode { int ivex; // 边的起点在顶点数组中的下标 int jvex; // 边的终点在顶点数组中的下标 EdgeNode ilink; // 指向下一个起点相同的边 EdgeNode jlink; // 指向下一个终点相同的边 int weight; // 权值 } class VertexNode { char data; // 顶点数据 EdgeNode firstedge; // 指向第一条与该顶点有关系的边 } class AMLGraph { VertexNode[] adjmulist; // 存储顶点的一维数组 int vexnum; // 图的顶点数 int arcnum; // 图的边数 }
应用:
-
十字链表有效地解决了有向图中计算结点入度的问题。通过
firstin
链表中的结点数,我们可以得到顶点的入度。对于有向图中某结点的出度,我们可以通过
firstout
链表中的结点数来计算。 -
邻接多重表适用于存储无向图,特别是在图中存在平行边(即连接同一对顶点的多条边)的情况下。
它有效地解决了邻接表中边存储冗余的问题,因为每条边只在邻接多重表中存储一次。
邻接多重表还可以用于计算无向图中顶点的度数(即与该顶点相连的边数)。