第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> otin{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)
-
深度优先遍历步骤:
- 选取一个源点 (vin{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)) 是满足 (uin{U},vin{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[egin{array}{cccc} v1 &v2 &v3 &v4 &v5\ 0 &10 &infty &30 &100\ end{array} ight ])
- 第一次:从 (v2) 点出发,(v1) 和 (v2) 保持不变,迭代剩下点 ((v3,v4,v5)) 的距离后,剩余点的最短路径是 (v4),(D_{1}=left[egin{array}{cccc} v1 &v2 &v3 &v4 &v5\ 0 &10 &60(10+50) &30 &100\ end{array} ight ])
- 第二次:从 (v4) 点 出发,(v1,v2,v4) 保持不变,优化剩余点 ((v3,v5)) 的最短距离。剩余点的最短路径是 (v3),(D_{2}=left[egin{array}{cccc} v1 &v2 &v3 &v4 &v5\ 0 &10 &50(20+30) &30 &90(30+60)\ end{array} ight ])
- 第三次:从 (v3) 点出发,(v1,v2,v4,v3) 保持不变。优化剩余点 $v5 (的最短路径,)D_{3}=left[egin{array}{cccc} v1 &v2 &v3 &v4 &v5 0 &10 &50 &30 &60(20+30+10) end{array} ight ]$
-
源点 (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) 的最大路径长度
-
[]
-
egin{cases}v_l(n-1)=v_e(n-1)v_l=min{v_l(j)-len(<v_i,v_j>)}(0leq{i}leq{n-2})end{cases}
egin{cases}e(k)=v_e(i)l(k)=v_l(j)-len(<v_i,v_j>)end{cases}