1
0
mirror of https://github.com/Didnelpsun/CS408.git synced 2026-02-09 13:45:48 +08:00
Files
CS408/Data-Structrue/8-graph.md
Didnelpsun 1795de9caa 数据结构完结
对原有的文件添加上序号
2021-05-06 15:42:57 +08:00

21 KiB
Raw Blame History

基本概念

图的定义

图是顶点集和边集构成的二元组即图G由顶点集V和边集E组成记为G=(V,E)其中V(G)表示图G中顶点的有限非空集E(G)表示图G中顶点之间的关系集合。

若V=${v_1,v_2\cdots,v_n}$,则用|V|表示图G中顶点的个数也称图G的阶E=${(u,v)|u\in V,v\in V}$,用|E|表示图G中边的条数。

图一定是非空的即V一定是非空集。

图的类别

  • 无向图若E是无向边简称边的有限集合时则图G为无向图。边是顶点的无序对记为(v, w)或(w,v),因为(v, w)=(w,v),其中v、w是顶点。可以说顶点w和顶点v互为邻接点。边(v, w)依附于顶点w和v或者说边(v, w)和顶点v、w相关联。
  • 有向图若E是有向边也称弧的有限集合时则图G为有向图。弧是顶点的有序对记为<v,w>其中v、w是顶点v称为弧尾w称为弧头<v,w>称为从顶点v到顶点w的弧也称v邻接到w或w邻接自v。<v,w>≠<w,v>。
  • 简单图:不存在重复边,且不存在顶点到自身的边。一般的图默认是简单图。
  • 多重图图G中某两个结点之间的边数多于一条又允许顶点通过同一条边与自己关联。
  • 无向完全图:无向图中任意两个顶点之间都存在边。
  • 有向完全图:有向图中任意两个顶点之间都存在方向相反的两条弧。
  • 稀疏图:一般$\vert E\vert<\vert V\vert\log\vert V\vert$的图。
  • 稠密图:一般$\vert E\vert>\vert V\vert\log\vert V\vert$的图。
  • 树:不存在回路,且连通的无向图。
  • 有向树一个顶点的入度为0其余顶点入度均为1的有向图。

顶点的度

对于无向图顶点v的度是指依附于该顶点的边的条数记为TD(v)。$\sum_{i=1}^nTD(v_i)=2\vert E\vert$。

对于有向图入度是指以顶点v为终点的有向边的条数记为ID(v)出度指以顶点v为起点的有向边的条数记为OD(v)顶点v的度就是其入度和出度之和即TD(v)=ID(v)+OD(v)。$\sum_{i=1}^nID(v_i)=\sum_{i=1}^nOD(v_i)=\vert E\vert$。

顶点的关系

  • 路径:从一个点到另一个点所经过的顶点序列。
  • 回路(环):第一个顶点与最后一个顶点相同的路径。
  • 长度(无权图):沿路径所经过的边数成为该路径的长度。
  • 简单路径:路径中的顶点不重复出现。
  • 简单回路:由简单路径组成的回路。
  • 点到点的距离从顶点u到顶点v的最短路径若存在则此路径的长度就是从u到v的路径若不存在路径则记该路径为无穷。
  • 连通在无向图中若从顶点v到顶点u有路径存在则称uv是连通的。
  • 强连通在有向图中若从顶点v到顶点u和从顶点u到顶点v之间都有路径则称强连通。

图的连通

  • 连通图无向图中任意两个顶点之间都是连通的。对于n个顶点的无向图若其是连通图则最少需要n-1条边若其是非连通图则最多有$C_{n-1}^2$条边。
  • 强连通图有向图中任意两个顶点之间都是强连通的。对于n个顶点的有相同若其是强连通图则最少需要n条边来形成环路。
  • 子图设有两个图G=(V,E)和G'=(V',E')若V'是V的子集E'是E的子集则G'是G的子图。
  • 生成子图若有满足V(G')=V(G)的子图G'则G'是G的生成子图。
  • 连通分量无向图G中的极大连通子图称为G的连通分量对任何连通图而言连通分量就是其自身。
  • 强连通分量有向图G中的极大连通子图称为G的强连通分量对任何强连通图而言强连通分量就是其自身。
  • 生成树包含连通图中全部顶点的一个极小连通子图。若图的顶点为n则其生成树包含n-1条边若去掉生成树的一条边则会变成非连通图若加上一条边则会形成一个回路。
  • 生成森林:在非连通图中,连通分量的生成树构成了非连通图的生成森林。

图的权

  • 边的权:在一个图中,每条边都可以表上具有某种含义的数值,这就是该边的权值。
  • 网络(网):若图中的每条边都有权,这个带权图被称为网。
  • 带权路径长度:取沿路径各边的权之和作为此路径的长度。

无权图

若v[i][j]=0表示$v_{i+1}$到$v_{j+1}$是不连通的若v[i][j]=1表示$v_{i+1}$到$v_{j+1}$是连通的。

若v[i][j]=∞,表示$v_{i+1}$到$v_{j+1}$是不连通的若v[i][j]=某权值,表示$v_{i+1}$到$v_{j+1}$是连通的。

图的存储结构

邻接矩阵

使用一个长宽皆为$\vert v\vert$的二维矩阵v从左上角到右上角从左上角到左下角分别标识表示$v_1,v_2\cdots,v_n$。假设矩阵的索引从0开始而结点编号从1开始。

  • 对于无向图第i个结点的度=第i行或第i列的非零元素个数。
  • 对于有向图第i个结点的入度=第i行的非零元素个数第i个结点的出度=第i列的非零元素个数第i个结点的度=第i行的非零元素个数+第i列的非零元素个数。

空间复杂度是$O(\vert V\vert^2)$。

适用于存储稠密图。

对于无向图,因为没有方向,所以只有两点连接就是连通的,从而无向图的邻接矩阵都是主对角线对称的。因为对称,所以可以压缩存储。

设图G的邻接矩阵为$A$矩阵元素为0或1则$A^n$的元素$A^n[i][j]$表示由顶点$v_{i+1}$到结点$v_{j+1}$的长度为n的路径的数目。

邻接表

邻接表存储方式是顺序存储与链式存储的结合,存储方式和树的孩子表示法类似。

使用一个数组保存图的每一个结点,每一个结点元素包含一个指向后一条边的指针。

对于无向图,因为同一条边两端的点会重复存储,所以空间复杂度为$O(\vert V\vert+2\vert E\vert)$,而对于有向图空间复杂度为$O(\vert V\vert+\vert E\vert)$。

  • 对于无向图,每个结点的边链表的结点数就是该结点的度。
  • 对于有向图,每个结点的边链表的结点数就是该结点的出度,而对于入度就只能遍历所有结点的结点链表。

邻接表的表示方式是不唯一的。

十字链表

十字链表置用于存储有向图。可以解决邻接矩阵空间复杂度高和邻接表计算入度入边不方便的问题。

十字链表定义了两种结点:

  • 顶点结点:用于表示顶点,被一个数组包裹。
    • 数据域。
    • 该顶点作为弧头的第一条弧。
    • 该顶点作为弧尾的第一条弧。
  • 弧结点:被顶点结点指向的结点。
    • 弧尾顶点编号。
    • 弧头顶点编号。
    • 权值。
    • 弧头相同的下一条弧。
    • 弧尾相同的下一条弧。

空间复杂度为$O(\vert V\vert+\vert E\vert)$。

邻接多重表

邻接多重表用于存储无向图,可以解决邻接矩阵空间复杂度高和邻接表删除插入结点不方便的问题。

邻接多重表定义了两种结点:

  • 顶点结点:用于表示顶点,被一个数组包裹。
    • 数据域。
    • 该顶点相连的第一条边。
  • 边结点:被顶点结点指向的结点。
    • 边一端编号i。
    • 边另一端编号j。
    • 权值。
    • 依附于i的下一条边。
    • 依附于j的下一条边。

空间复杂度为$O(\vert V\vert+\vert E\vert)$。

图的基本操作

图查找

查找边

使用邻接矩阵只用根据对应行列的元素是否为1或某值就可以了如果是0或无穷就代表没有该邻边。时间复杂度为$O(1)$。

而使用邻接矩阵需要从一端点出发遍历对应的结点链表,如果能在链表中找到另一端点的索引,就代表有边。时间复杂度为$O(1)$到$O(\vert V\vert)$。

查找点邻边

对于无向图邻接矩阵需要遍历对应结点的那一行所有数值为1或某数值的列就是对应的有边的另一个端点。时间复杂度为$O(\vert V\vert)$。

对于有向图邻接矩阵需要遍历对应结点的那一行得到出边以及那一列代表入边所有数值为1或某数值的列就是对应的有边的另一个端点。时间复杂度为$O(\vert V\vert)$。

对于无向图,邻接表只用遍历对应结点的结点链表就可以。时间复杂度为$O(1)$到$O(\vert V\vert)$。

对于有向图,邻接表用遍历对应结点的结点链表得到出边,而对于入边需要遍历所有邻接表的边结点。出边时间复杂度为$O(1)$到$O(\vert V\vert)$,入边时间复杂度为$O(\vert E\vert)$。

查找头邻接点

邻接矩阵只用扫描对应的行,找到结点就可以了。时间复杂度为$O(1)$到$O(\vert V\vert)$。

对于无向图,邻接表只用找到结点的边结点的第一个结点。时间复杂度为$O(1)$。

对于有向图,出边邻接表只用找到结点的边结点的第一个结点。时间复杂度为$O(1)$。而对于入边需要遍历所有的结点的第一个链表结点。时间复杂度为$O(1)$到$O(\vert E\vert)$。

查找下一个邻接点

邻接矩阵只用扫描对应的行,找到结点就可以了。时间复杂度为$O(1)$到$O(\vert V\vert)$。

邻接表只用找到当前结点的下一个结点。时间复杂度为$O(1)$。

图插入

邻接矩阵只用在最后增加一行一列。时间复杂度是$O(1)$。

邻接表只用在存储结点的数组的末尾添加一个结点指针设置为NULL。时间复杂度是$O(1)$。

图删除

邻接矩阵的删除元素分为两种方式,如果是直接删除对应元素行与列上的所有元素并移动其他元素,那么时间复杂度就是$O(\vert V\vert^2)$如果删除对应元素行与列上的所有元素但是不移动其他元素而是将保存结点数据的数组中对应结点的数据变为NULL则时间复杂度就是$O(\vert V\vert)$。

对于无向图,邻接表的删除需要删除该结点并删除结点后连接的所有结点链表元素,时间复杂度为$O(1)$到$O(\vert V\vert)$。

对于有向图,邻接表的删除需要删除该结点并删除结点后连接的所有结点链表元素且还要遍历所有的边并删除,删除出边时间复杂度为$O(1)$到$O(\vert V\vert)$,删除入边时间复杂度为$O(\vert E\vert)$。

图遍历

分为广度优先BFS与深度优先DFS。

广度优先遍历

  • 找到一个与顶点相邻的所有结点。
  • 标记哪些结点被访问过。
  • 需要一个辅助队列保存结点是否被访问的数据。

广度优先遍历过程:

  1. 访问顶点v。
  2. 访问v的所有未被访问的邻接点。
  3. 依次从这些邻接点(在步骤二中访问的顶点)出发,访问它们的所有未被访问的邻接点; 依此类推,直到图中所有访问过的顶点的邻接点都被访问。

邻接矩阵实现时的时间复杂度为$O(\vert V\vert^2)$,邻接表实现时的时间复杂度为$O(\vert V\vert+\vert E\vert)$;空间复杂度为$O(\vert V\vert)$。

广度优先生成树根据广度优先遍历可以将所有第一次访问结点时的路径组合生成一个广度优先生成树若图结点为n个则生成树边一共有n-1条。因为保存图的数据结构若是不唯一则其广度优先生成树也是不唯一的。 广度优先生成森林:若图是不连通的,那会生成连通分量个广度优先生成树,就构成了广度优先生成森林。

深度优先遍历

类似于树的先根遍历。

深度优先遍历过程:

  1. 访问顶点v。
  2. 依次从v的未被访问的邻接点出发对图进行深度优先遍历。
  3. 直至图中和v有路径相通的顶点都被访问。
  4. 若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

邻接矩阵实现时的时间复杂度为$O(\vert V\vert^2)$,邻接表实现时的时间复杂度为$O(\vert V\vert+\vert E\vert)$;空间复杂度为$O(\vert V\vert)$。

邻接表的深度优先序列会优先选择每个结点的第一个相邻结点,即结点链中的第一个元素。

邻接矩阵方式唯一所以深度优先序列唯一,而邻接表方式不唯一,所以深度优先序列不唯一。

深度优先生成树根据深度优先遍历可以将所有第一次访问结点时的路径组合生成一个深度优先生成树若图结点为n个则生成树边一共有n-1条。因为保存图的数据结构若是不唯一则其深度优先生成树也是不唯一的。 深度优先生成森林:若图是不连通的,那会生成连通分量个深度优先生成树,就构成了深度优先生成森林。

图遍历与图连通性

若起始顶点到其他各顶点都有路径,那么只需调用一次深度优先或广度优先遍历函数。

对强连通图,从任意一结点出发都只用调用一次深度优先或广度优先遍历函数。

图的应用

最小生成树

最小生成树定义

最小生成树MST也是最小代价树。已知生成树就是最小边的能到任意结点的树这种树只关心边数所以有多个不同的生成树。

而最小生成树就是带权生成树的最小权值和的情况。

设R为图G的所有生成树的集合若T为R中边的权值之和最小的生成树则T称G的最小生成树。

  • 最小生成树可能有多个,但是边的权值总是唯一且最小的。
  • 最小生成树的边数=顶点数-1。减去一条则不连通增加一条则会出现回路。
  • 若一个连通图本身就是一棵树,则其最小生成树就是其本身。
  • 只用连通图才有生成树,非连通图只有生成森林。

Prim算法

普里姆算法:从某个顶点开始构建生成树,每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。

时间复杂度为$O(\vert V\vert^2)$,适用于边稠密图。

Kruskal算法

克鲁斯卡尔算法:每次选择一条权值最小的边,使这条边的两头连通,若本就连通的就不选,直到所有的结点都连通。

时间复杂度为$O(\vert E\vert\log_2\vert E\vert)$,适用于边稀疏图。

最短路径

  • 单源最短路径:单个结点到图的其他结点的最短路径。
    • BFS算法无权图
    • Dijkstra算法带权图、无权图
  • 每对顶点间最短路径:每个结点之间的最短路径。
    • Floyd算法带权图、无权图

BFS算法

广度优先算法可以计算无权图的单源最短路径。

实际上无权图可以视为一种特殊的带权图只是每条边的权值全部为1。

广度优先算法基本上就是对广度优先遍历的改进。定义两个数组,索引号就代表元素的序号,一个数组表示从起点开始到该点的最短路径长度,另一个数组表示从起点开始到该点的最短路径的上一个结点的索引值。

Dijkstra算法

  1. 从$v_0$开始,初始化三个数组:标记各顶点是否已找到最短路径;最短路径长度;最短路径上的前驱。
  2. 遍历所有结点找到还没确定最短路径且最短路径长度值最小的的一个顶点这就确定了下一个最短路径的结点令其各顶点是否已找到最短路径的值为true。
  3. 检查所有邻接这个结点的其他结点,若其点还没有找到最短路径,则更新最短路径长度值与最短路径上前驱的值。
  4. 重复步骤二再次循环遍历所有结点并找到没确定最短路径则最短路径长度最小的顶点。

Dijkstra算法与Prim算法类似都是优先与最短的路径结合。

时间复杂度为$O(\vert V\vert^2)$。

当权值中含有负权值的时候可能Dijkstra算法会失效。

Floyd算法

是一种动态规划算法,将问题的求解分为多个阶段。

对于n个结点的图G求任意一对顶点$v_i$到$v_j$之间的最短路径可分为如下阶段:

  1. 初始化:不允许在其他顶点中转,求最短路径。
  2. 若允许在$v_0$中转,求最短路径。
  3. 若允许在$v_0$、$v_1$中转,求最短路径。
  4. ...
  5. 若允许在$v_0$、$v_1\cdots v_{n-1}$中转,求最短路径。

算法需要遍历n次每次遍历都需要查看n×n的矩阵中是否有更优的中转点。

时间复杂度为$O(\vert V\vert^3)$,空间复杂度为$O(\vert V\vert^2)$。

Floyd算法复杂度高所以基本上都是四个结点以下的图能解决带负权值的问题但是不能解决带有负权回路的图即有负权值的边组成回路这种图可能没有最短路径。

有向无环图

若一个有向图中不存在环则是有向无环图简称DAG图。

有向无环图可以运用到表达式的表达上,将操作数共同的结点部分删除并将边合并到一起,这就形成了图。

顶点中不可能出现重复的操作数。

  1. 把各个单个的操作数不重复的排成一排。
  2. 标出各个运算符的生效顺序。
  3. 按顺序加入运算符,并注意对运算符的优先级进行分层。
  4. 当构建完成后从底向上逐层检查同层的运算符是否可以合并。

拓扑排序

AOV网用DAG图表示一个工程顶点表示活动有向边$<v_i,v_j>$表示活动$v_i$必须先于活动$v_j$进行。

拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条;件时,称为该图的一个拓扑排序:

  1. 每个顶点出现且只出现一次。
  2. 若顶点A在序列中排在顶点B的前面则在图中不存在从顶点B到顶点A的路径。

或定义为拓扑排序是对有向无环图的顶点的一种排序它使得若存在一条从顶点A到顶点B的路径则在排序中顶点B出现在顶点A的后面。每个AOV网都有一个或多个拓扑排序序列。

拓扑排序的实现:

  1. 从AOV网中选择一个没有前驱的入度为0的顶点并输出。
  2. 从网中删除该顶点和所有以它为起点的有向边。
  3. 重复步骤一和二直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。

逆拓扑排序的实现:

  1. 从AOV网中选择一个没有后继的出度为0的顶点并输出。
  2. 从网中删除该顶点和所有以它为起点的有向边。
  3. 重复步骤一和二直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。

关键路径

在带权有向图中以顶点表示时间以有向边表示活动以边上的权值表示完成该活动的开销称之为用边表示活动的网络简称AOE网。

  • 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始。
  • 只有在进入某顶点的各有向边所代表的活动都已经结束时,该顶点所代表的事件才能发生,活动也可以并行进行。
  • 只有一个入度为0的顶点即开始顶点源点表示整个工程的开始。
  • 只有一个出度为0的顶点称为结束顶点汇点表示整个工程的结束。
  • 从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径(即决定完成整个工程所需的最小时间),而关键路径上的活动称为关键活动。
  • 事件的最早发生时间:决定了所有从该事件开始的活动能够开工的最早时间。
  • 活动的最早开始时间:指该活动弧的起点所表示的事件最早发生时间。
  • 事件的最迟发生时间:在不推迟整个工程完成的前提下,该事件最迟必须发生的时间。
  • 活动的最迟开始时间:指该活动弧的终点所表示的事件的最迟发生时间与该活动所需时间之差。
  • 活动的时间余量:在不增加完成整个工程所需总时间的情况下,活动可以拖延的时间。

求关键路径的步骤:

  1. 求所有事件的最早发生时间ve。根据拓扑排序序列依次按照所有路径的最大值求出各个顶点的最早发生时间。
  2. 求所有事件的最迟发生时间vl。根据逆拓扑排序序列回退依次将每个结点按第一步计算的整个工程的时间减去本结点需要处理的时间得到每个活动都最晚应该发生的时间交叉的结点取最小值。
  3. 求所有活动的最早发生时间e。根据第二步可以得到每个活动发生的最早时间。
  4. 求所有活动的最迟发生时间l。根据vl求每个结点不影响整个工程的情况下最晚可以什么时候开始。
  5. 求所有活动的时间余量d。将l-e得到d余量为0的活动就是关键活动表示如果该活动拖延就会影响整个工程的进度。
  • 关键活动时间增加,整个工程工期延长。
  • 关键活动时间减少,整个工程工期缩短。
  • 关键活动时间减少,可能变为非关键活动。
  • 若有多条关键路径,则必须提高所有关键路径关键活动才能缩短工期。