第8章 图
第8章 图
数据结构与算法_师大完整教程目录(更有python、go、pytorch、tensorflow、爬虫、人工智能教学等着你):https://www.cnblogs.com/nickchen121/p/13768298.html
一、图的基本概念
-
图 \(G\) 的顶点集:记作 \(V(G)\)
-
图 \(G\) 的边集:记作 \(E(G)\)
-
无向图的边:顶点 \(v\) 到 顶点 \(u\) 的边记作 \((u,v)\)
-
有向图的边:顶点 \(v\) 到 顶点 \(u\) 的边记作 \(<u,v>\)
-
邻接点:若 \((v,u)\) 是一条无向边,则称 \(u\) 和 \(v\) 互为邻接点
-
顶点和边数的关系:
- \(n\) 个顶点的无向图,其边数 \(e\) 小于等于 \(n(n-1)/2\)。边数恰好为 \(n(n-1)/2\) 的无向图称为无向完全图
- \(n\) 个顶点的有向图,其边数 \(e\) 小于等于 \(n(n-1)\)。边数恰好为 \(n(n-1\) 的有向图称为有向完全图
-
无向图顶点的度:顶点相关联的边的数目,顶点 \(v\) 的度记为 \(D(v)\)
-
有向图顶点的度:
- 入度:以顶点 \(v\) 作为终点的边的数目,记为 \(ID(v)\)
- 出度:以顶点 \(v\) 作为始点的边的数目,记为 \(OD(v)\)
- 注:有向图顶点的度等于顶点的入度和出度之和
-
顶点数 \(n\)、边数 \(e\) 和度数的关系:\(e=\frac{1}{2}\sum_{i=1}^n{D(v_i)}\)
-
有向图路径:存在一个顶点序列 \(v,v_1,v_2,\cdots,v_m,u\),且 \((v,v_1),(v_1,v_2),\cdots,(v_m,u)\) 都属于 \(E(G)\),则该顶点序列为 \(v\) 到 \(u\) 的一条路径
-
无向图路径:有向图路径:存在一个顶点序列 \(v,v_1,v_2,\cdots,v_m,u\),且 \(<v,v_1>,<v_1,v_2>,\cdots,<v_m,u>\) 都属于 \(E(G)\),则该顶点序列为 \(v\) 到 \(u\) 的一条路径
-
简单路径:一条路径除了起点 \(v\) 和终点 \(u\) 之外,其余顶点均不相同,该路径称之为一条简单路径
-
简单回路(简单环):起点和终点相同(\(v=u\))的简单路径
-
无向图连通的概念:
- 无向图的连通:顶点 \(v\) 到 \(u\) 之间有路径,则称 \(v\) 和 \(u\) 是连通的
- 连通图(无向图):\(V(G)\) 中任意两个不同的顶点 \(v\) 和 \(u\) 都连通(即有路径),则 \(G\) 为连通图
- 连通分量:无向图 \(G\) 的极大连通子图(任何连通图的连通分量都是其本身,非连通的无向图有多个连通分量)
-
有向图连通的概念:
- 强连通图:\(V(G)\) 中任意两个不同的顶点 \(v\) 和 \(u\),都存在从 \(v\) 到 \(u\) 和从 \(u\) 到 \(v\) 的路径
- 强连通分量:有向图的极大强连通子图(任何强连通图的唯一强连通分量是其自身,非强连通的有向图有多个强连通分量)
-
连通分量和强连通分量注意事项:单个顶点可以属于一个单独的连通分量或强连通分量
-
权:图的每条边上附上相关的数值
-
网络(带权图):图的每条边都赋上一个权
二、图的基本运算
- 略
三、图的基本存储结构
- 注:对于以下图的存储结构,都假定顶点序号从 \(0\) 开始,图 \(G\) 的顶点集一般形式为 \(V(G)=\{v_0,\cdots,v_i,\cdots,v_{n-1}\}\)
3.1 邻接矩阵及其实现
-
图的邻接矩阵表示法:用两个表格分别存储数据元素(顶点)的信息和数据之间的关联(边)信息
- 一维数组(顺序表):存储数据元素的信息
- 二维数组(邻接矩阵):存储数据元素之间的关系
-
邻接矩阵的性质:若 \((v_i,v_j)或<v_i,v_j>\in{E(G)}\),\(A[i,j]=1\);若 \((v_i,v_j)or<v_i,v_j>\notin{E(G)}\),\(A[i,j]=0\)
-
无向图的邻接矩阵:该邻接矩阵一定是对称的,可以采用上(下)三角矩阵进行压缩存储,存储空间为 \(n(n+1)/2\)
-
有向图的邻接矩阵:该邻接矩阵不一定是对称的,存储空间为 \(n^2\)
-
无向图顶点 \(v_i\) 的度数:\(D(v_i) = \sum_{j=0}^{n-1}A[i,j]=\sum_{j=0}^{n-1}A[j,i]\) (对称矩阵\(A=A^T\))
-
有向图顶点 \(v_i\) 的度数:
- 出度:\(OD(v_i)=\sum_{j=0}^{n-1}A[i,j]\)
- 入度:\(ID(v_i) = \sum_{j=0}^{n-1}A[j,i]\)
-
图邻接矩阵图:
-
网络的邻接矩阵性质:
- 图网络邻接矩阵图:
3.1.1 邻接矩阵存储结构
#define FINITY 5000 // 用5000表示无穷大
#define M 20 // 最大顶点数
typedef char vertextype; // 顶点值类型
typedef int edgetype; // 权值类型
typedef struct {
vertextype vexs[M]; // 顶点信息域
edgetype edges[M][M]; // 邻接矩阵
int n, e; // 图中顶点总数和边数
} Mgraph;
3.1.2 建立网络的邻接矩阵算法(算法)
-
算法步骤:
- 打开文件
- 读入图中的顶点数和边数
- 读入图中的顶点值
- 初始化邻接矩阵
- 读入网络中的边
- 建立无向图邻接矩阵
- 关闭文件
3.2 邻接表及其实现
-
邻接表与邻接矩阵的区别:
- 顶点个数为 \(n\) 的图,邻接矩阵的存储空间为 \(n^2\) ,而使用邻接表则可节省很多空间
- 邻接矩阵表示法唯一,而邻接表的表示法不唯一
-
邻接表中的两个结点:
- 表头结点:存储顶点的数据域(\(vertex\))和头指针域(\(firstedge\))
- 边表结点:邻接点域(\(adjvex\))和链域(\(next\))
-
邻接表的随机访问任意顶点的构造:让所有头结点顺序存储在一个向量中
-
图无向图邻接表:
-
有向图
- 出边表(邻接表):表结点存储以顶点 \(v\) 为始点射出的一条边
- 入边表(逆邻接表):表结点存储以顶点 \(v\) 为终点射出的一条边
-
图有向图的邻接表:
-
邻接表存储空间:无向图(\(n\) 个顶点和 \(e\) 条边)用邻接表存储需要 \(n\) 个头结点和 \(2e\) 个边结点
-
注:当 \(e\) 远小于 \(n(n-1)/2\) 时,邻接表存储图比邻接矩阵存储图节省空间
-
无向图的度(邻接表):顶点 \(v_i\) 的度为第 \(i\) 个链表中结点的个数
-
有向图的度(邻接表-出边表):
- 出度:第 \(i\) 个链表中的结点的个数
- 入度:所有链表中其邻接点域的值为 \(i\) 的结点的个数
3.2.1 邻接表存储结构
#define M 20 // 预定义图的最大顶点数
typedef char datatype; // 定点信息数据域
// 边表结点类型
typedef struct node {
int adjvex; // 邻接点
struct node *next;
} edgenode;
// 头结点类型
typedef struct vnode {
datatype vertex; // 顶点信息
edgenode *firstedge; // 邻接链表头指针
} vertexnode;
// 邻接表类型
typedef struct {
vertexnode adjlist[M]; // 存放头结点的顺序表
int n, e; // 图的顶点数与边数
} linkedgraph;
3.2.2 建立无向图的邻接表算法(算法)
-
算法步骤:
- 打开文件
- 读入顶点数和边数
- 读入顶点信息
- 边表置为空
- 循环 e(边数) 次建立边表
输入无序对 \((i,j)\)
邻接点序号为 \(j\)
将新结点 \(*s\) 插入顶点 \(v_i\) 的边表头部
9. 关闭文件
3.3 邻接多重表
-
邻接多重表由两个表组成:
- 表头结点:\(vertex\) 域存储顶点的相关信息;\(firstedge\) 存储第一条关联于 \(vertex\) 顶点的边
- 边表结点:\(mark\) 域标志图的遍历算法中是否被搜索过;\(vex_i\) 和 \(vex_j\) 表示边的两个顶点在图中的位序;\(link_i\) 和 \(link_j\) 表示两个边表结点指针。
\(link_i\) 指向关联于顶点 \(vex_i\)的下一条边;\(link_j\) 指向关联于顶点 \(vex_j\) 的下一条边
4. 边表结点的顺序如下表所示:
\(mark\) | \(vex_i\) | \(link_i\) | \(vex_j\) | \(link_j\) |
---|---|---|---|---|
四、图的遍历
4.1 深度优先遍历(DFS)
-
深度优先遍历步骤:
- 选取一个源点 \(v\in{V}\),把 \(v\) 标记为已被访问
- 使用深度优先搜索方法依次搜索 \(v\) 的所有邻接点 \(w\)
- 如果 \(w\) 未被访问,以 \(w\) 为新的出发点继续进行深度优先遍历
- 如果从 \(v\) 出发,有路的顶点都被访问过,则从 \(v\) 的搜索过程结束
- 如果图中还有未被访问过的顶点(该图有多个连通分量或者强连通分量),则再任选一个未被访问的顶点开始新的搜索
-
注:深度优先遍历的顺序是不一定的,但是,当采用邻接表存储结构并且存储结构已确定的情况下,遍历的结果将是确定的
-
图深度优先遍历:
4.2 广度优先遍历 (BFS)
-
广度优先遍历步骤:
- 从图中某个源点 \(v\) 出发
- 访问顶点 \(v\) 后,尽可能先横向搜索 \(v\) 的所有邻接点
- 在访问 \(v\) 的各个邻接点 \(w_k,\cdots,w_k\) 后,从这些邻接点出发依次访问与 \(w_1,\cdots,w_k\) 邻接的所有未曾访问过的顶点
- 如果 \(G\) 是连通图,访问完成;否则另选一个尚未访问的顶点作为新源点继续上述搜索过程,知道图 \(G\) 的所有顶点均被访问
-
注:广度优先遍历无向图的过程中,调用 \(bfs(函数:从顶点\,i\,出发广度优先遍历图\,g\,的连通分量)\) 的次数就是该图连通分量的个数
-
图广度优先遍历:
五、生成树与最小生成树 (无向网)
- 生成树:对于一个无向的连通图 \(G\),设 \(G'\) 是它的一个子图,如果 \(G'\) 中包含了 \(G\) 中所有的顶点,且 \(G'\) 是无回路的连通图,则称 \(G'\) 为 \(G\) 的一颗生成树
- 图生成树:
- 生成森林:如果 \(G\) 是非连通的无向图,需要若干次从外部调用 \(DFS(或BFS)\) 算法才能完成对 \(G\) 的遍历。每一次外部调用,只能访问 \(G\) 的一个连通分量,经过该连通分量的顶点和边构造出一颗生成树,则 \(G\) 的各个连通分量的生成树组成了 \(G\) 的生成森林
- 图生成森林:
5.1 最小生成树的定义
-
生成树的权:生成树 \(T\) 的各边的权值总和,记作 \(W(T)=\sum_{(u,v)\in{E}}w_{uv}\),其中 \(E\) 表示 \(T\) 的边集,\(w_{uv}\) 表示边 \((u,v)\) 的权
-
最小生成树的权:总权值 \(W(T)\) 最小的生成树称为最小生成树
-
构造最小生成树的准则:
- 必须只使用该网络中的边来构造最小生成树
- 必须使用且仅使用 \(n-1\) 条边来连接网络中的 \(n\) 个顶点
- 不能使用产生回路的边
5.2 最小生成树的普里姆(Prim)算法
-
两栖边:假设 \(G=(V,E)\) 是一个连通图,\(U\) 是顶点集 \(V\) 的一个非空真子集,若 \((u,v)\) 是满足 \(u\in{U},v\in{V-U}\) 的边(称这个边为两栖边),且 \((u,v)\) 在所有的两栖边中具有最小的权值(此时,称 \((u,v)\) 为最小两栖边)
-
Prim算法步骤:
- 初始化 \(U=\{u_0\},TREE=\{\}\)
- 如果 \(U=V(G)\),则输出最小生成树 \(T\),并结束算法
- 在所有两栖边中找一条权最小的边 \((u,v)\)(若候选两栖边中的最小边不止一条,可任选其中的一条),将边 \((u,v)\) 加入到边集 \(TREE\) 中,并将顶点 \(v\) 并入集合 \(U\) 中
- 由于新顶点的加入,\(U\) 的状态发生变化,需要对 \(U\) 与 \(V-U\) 之间的两栖边进行调整
- 转步骤 \(2\)
-
下图步骤顺序(\(V=\{A,B,C,D,E,F\}\)):
- \(U = \{A\}\),\(V-U=\{B,C,D,E,F\}\),\(TREE=\{\}\),两栖边 \((A,B),(A,C),(A,D),(A,E),(A,F)\),最小两栖边 \((A,B)\)
- \(U = \{A,B\}\),\(V-U=\{C,D,E,F\}\),\(TREE=\{(A,B)\}\),两栖边 \((B,C),(B,D),(B,F),(A,E)\)(\((B,C)\) 比 (\(A,C)\) 小,因此做一个替换),最小两栖边 \((B,D)\)
- \(U = \{A,B,D\}\),\(V-U=\{C,E,F\}\),\(TREE=\{(A,B),(B,D)\}\),两栖边 \((B,C),(B,F),(A,E)\),最小两栖边 \((B,F)\)
- \(U = \{A,B,D,F\}\),\(V-U=\{C,E\}\),\(TREE=\{(A,B),(B,D),(B,F)\}\),两栖边 \((B,C),((F,E)\),最小两栖边 \((B,C)\)
- \(U = \{A,B,D,F,C\}\),\(V-U=\{E\}\),\(TREE=\{(A,B),(B,D),(B,F),(B,C)\}\),两栖边 \((F,E)\),最小两栖边 \((F,E)\)
- \(U = \{A,B,D,F,C,E\}\),\(V-U=\{\}\),\(TREE=\{(A,B),(B,D),(B,F),(B,C),(F,E)\}\),两栖边 \(\{\}\),最小两栖边 \(\{\}\)
-
图prim算法:
5.3 最小生成树的克鲁斯卡尔(Kruskal)算法
-
算法步骤:
- 将图 \(G\) 看做一个森林,每个顶点为一棵独立的树
- 将所有的边加入集合 \(S\),即一开始 \(S = E\)
- 从 \(S\) 中拿出一条最短的边 \((u,v)\),如果 \((u,v)\) 不在同一棵树内,则连接 \(u,v\) 合并这两棵树,同时将 \((u,v)\) 加入生成树的边集 \(E'\)
- 重复步骤 \(3\) 直到所有点属于同一棵树,边集 \(E'\) 就是一棵最小生成树
-
图kruskal算法:
六、最短路径 (有向网)
6.1 单源最短路径(Dijkstra算法)
-
距离向量 \(d\):表示源点可以途径 \(S\) 中的顶点到达 \(V-S\) 中顶点的距离
-
路径向量 \(p\):保存路径上第 \(i\) 个顶点的前驱顶点序号(没有的话,默认为 \(-1\))
-
算法步骤:
- 找到与源点 \(v\) 最近的顶点,并将该顶点并入最终集合 \(S\)
- 根据找到的最近的顶点更新从源点 \(v\) 出发到集合 \(V-S\) 上可达顶点的最短路径
- 重复以上操作
-
图Dijkstra算法:
-
上图求单源最短路径步骤:
- 初始化:从源点 \(v1\) 出发得到矩阵,到达个点的最小路径是 \(D_{12}=10\),\(D_{0}=\left[\begin{array}{cccc} v1 &v2 &v3 &v4 &v5\\ 0 &10 &\infty &30 &100\\ \end{array}\right ]\)
- 第一次:从 \(v2\) 点出发,\(v1\) 和 \(v2\) 保持不变,迭代剩下点 \((v3,v4,v5)\) 的距离后,剩余点的最短路径是 \(v4\),\(D_{1}=\left[\begin{array}{cccc} v1 &v2 &v3 &v4 &v5\\ 0 &10 &60(10+50) &30 &100\\ \end{array}\right ]\)
- 第二次:从 \(v4\) 点 出发,\(v1,v2,v4\) 保持不变,优化剩余点 \((v3,v5)\) 的最短距离。剩余点的最短路径是 \(v3\),\(D_{2}=\left[\begin{array}{cccc} v1 &v2 &v3 &v4 &v5\\ 0 &10 &50(20+30) &30 &90(30+60)\\ \end{array}\right ]\)
- 第三次:从 \(v3\) 点出发,\(v1,v2,v4,v3\) 保持不变。优化剩余点 $v5 \(的最短路径,\)D_{3}=\left[\begin{array}{cccc} v1 &v2 &v3 &v4 &v5\ 0 &10 &50 &30 &60(20+30+10)\ \end{array}\right ]$
-
源点 \(v1\) 的 \(Dijkstra算法\) 的最短路径(如下表):
- | 中间顶点 | 终点 | 路径长度 |
| :--: | :--: | :--: |
| | 2 | 10 |
| 4 | 3 | 50 |
| | 4 | 30 |
| 4;3 | 5 | 60 |
- | 中间顶点 | 终点 | 路径长度 |
-
距离向量 \(d\) 和路径向量 \(p\):
-
图Dijkstra算法的辅助数组:
6.2 所有顶点对的最短路径 (Floyd算法)
- 算法步骤:略
七、拓扑排序 (AOV网)
-
\(AOV网\) 边的作用:表示活动间(一个顶点表示一个活动)的优先关系
-
注:(真题)拓扑排序可以判断图中有没有回路(深度遍历优先算法也可以)
-
算法步骤
- 在有向图中找一个没有前驱(入度为 \(0\))的顶点并输出
- 在图中删除该顶点以及所有从该顶点发出的有向边
- 反复执行步骤 \(1\) 和 \(2\),知道所有顶点均被输出,或者 \(AOV\) 网中再也没有入度为 \(0\) 的顶点存在为止
-
图拓扑排序:
八、关键路径 (AOE网)
-
\(AOE网\) 边的作用:表示活动(一个顶点表示一个活动)持续的时间
-
事件可能的最早开始时间 \(v_e(i)\):对于某一事件 \(v_i\),它可能的最早发生时间 \(v_e(i)\) 是从源点到顶点 \(v_i\) 的最大路径长度
-
\[\]
-
\begin{cases}v_l(n-1)=v_e(n-1)\v_l=min{v_l(j)-len(<v_i,v_j>)}(0\leq{i}\leq{n-2})\end{cases}
\begin{cases}e(k)=v_e(i)\l(k)=v_l(j)-len(<v_i,v_j>)\end{cases}