好好学习,天天向上
本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star
⭐⭐⭐⭐⭐
转载请注明出处:
https://blog.csdn.net/weixin_43461520/article/details/124176292
6.1 图的基本概念
图的定义
图G由
顶点集V
和边集E
组成,记为G = (V, E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系(边)集合。若V = {v1, v2, … , vn},则用|V|
表示图G中顶点的个数
,也称图G的阶
,E = {(u, v) | u∈V, v∈V},用|E|
表示图G中边的条数
。V一定是非空集,E可以是空集。
无向图、有向图
若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中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则G为
多重图
。
顶点的度、入度、出度
对于
无向图
:顶点v的度是指依附于该顶点的边的条数,记为TD(v)。
例如,图中A的度为1。
无向图全部顶点的度之和为边数的2倍。
对于
有向图
:入度是以顶点v为终点的有向边的数目,记为ID(v);出度是以顶点v为起点的有向边的数目,记为OD(v)。顶点v的度等于其入度和出度之和
,即TD(v) = ID(v) + OD(v)。
例如,图中A的入度为1,出度为4。
有向图中所有顶点的 入度之和=出度之和=边数。
顶点-顶点的关系描述
路径
:一个顶点到另一个顶点之间的一条路径是指其经过的顶点序列。例如左图中A到C的路径就是ABDC或者ABEC等。回路
:第一个顶点和最后一个顶点相同的路径称为回路或环。例如右图中ABCDA。简单路径
:在路径序列中,顶点不重复出现的路径称为简单路径。简单回路
:除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。路径长度
:路径上边的数目。点到点的距离
:从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。若从u到v根本不存在路径,则记该距离为无穷(∞)。连通
:无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。强连通
:有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的。
连通图、强联通图
若图G中任意两个顶点都是连通的,则称图G为连通图
,否则称为非连通图
。
若图中任何一对顶点都是强连通的,则称此图为强连通图
。
图的局部——子图
对于有向图和无向图而言,设有两个图G = (V , E)和G' = (V', E'),若 V' 是V的子集,且E'是E的子集,则称 G' 是G的子图
。
若有满足V(G')=V(G)的子图G',则称其为G的生成子图
。
连通分量和强连通分量
无向图中的极大连通子图
称为连通分量
。其中,子图必须连通,且包含尽可能多的顶点和边。
有向图中的极大强连通子图
称为有向图的强连通分量
。其中,子图必须强连通,同时保留尽可能多的边。
生成树和生成森林
连通图的生成树
是包含图中全部顶点的一个极小连通子图
。
若图中顶点数为n,则它的生成树含有n-1 条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。
在非连通图
中,连通分量的生成树
构成了非连通图的生成森林
。
边的权、带权图/网
边的权
:在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值
。带权图/网
:边上带有权值的图称为带权图
,也称网
。带权路径长度
:当图是带权图时,一条路径上所有边的权值之和
,称为该路径的带权路径长度。
几种特殊形态的图
无向完全图
:图中任意两个顶点之间都存在边
有向完全图
:图中任意两个顶点之间都存在方向相反的两条弧
稀疏图
:边数很少的图
稠密图
:边数很多的图
树
:不存在回路,且连通的无向图,必有n-1条边。
有向树
:一个顶点的入度为0、其余顶点的入度均为1的有向图,称为有向树。
6.2图的存储及基本操作
邻接矩阵法
领接矩阵存储是指用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(即各个顶点之间的关系),存储顶点之间的邻接关系的二维数组称为邻接矩阵。
#define MaxVertexNum 100 //顶点的数目的最大值
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct {
VertexType Vex[MaxVertexNum]; //顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexNum, arcNum; //图的当前顶点数和弧数
} MGraph;
- 无向图中,第i个结点的
度
=第i行(或第i列)的非零元素个数。例如结点A的度=1+1+1=3。 - 有向图中,第i个结点的
出度
= 第i行的非零元素个数。第i个结点的入度
=第i列的非零元素个数。第i个结点的度
= 第i行、第i列的非零元素个数之和。 - 邻接矩阵法求
顶点的 度/出度/入度 的时间复杂度
为 O(|V|)。 空间复杂度
:O(|V|²),只和顶点数相关,和实际的边数无关。- 适合用于存储稠密图
- 无向图的邻接矩阵是对称矩阵,可以压缩存储(只存储上三角区/下三角区)
设图G的邻接矩阵为A(矩阵元素为0/1),则A^n的元素A^n[i][j]等于由顶点i到顶点j的长度为n的路径的数目
。
领接表法
对图中的每个顶点 Vi 都建立一个单链表,第 i 个单链表中的结点表示依附于顶点 Vi 的边
(有向图则是以顶点Vi为尾的弧),这个单链表就称为顶点的边表
(对于有向图则称为出边表)。边表的头指针和顶点的数据信息采用顺序存储(称为顶点表
),所以领接表中包括顶点表结点
和边表结点
。
#define MaxVertexNum 100 //图中顶点的数目的最大值
typedef char VertexType; //顶点的数据类型
typedef int InfoType; //边权值类型
typedef struct ArcNode { //边表节点
int adjVex; //该弧所指向的顶点的位置
struct ArcNode *next; //指向下一条弧的指针
InfoType info; //网的边权值
} ArcNode;
typedef struct VNode { //顶点表节点
VertexType data; //顶点信息
ArcNode *first; //指向第一条依附该顶点的弧的指针
} VNode, AdjList[MaxVertexNum];
typedef struct {
AdjList vertices; //邻接表
int vexNum, arcNum; //图的顶点数和弧数
} ALGraph; //以邻接表存储的图类型
- 边结点的数量是2|E|,整体空间复杂度为O(|V| + 2|E|)。
- 求无向图的
度
:遍历结点的边链表,比如图中顶点A的边链表数为3,即A的度为3。 - 有向图的
出度
:遍历对应结点的边链表即可。 - 有向图的
入度
:遍历所有结点的边链表,找到对应结点的入度信息。
十字链表
十字链表是有向图
的一种链式存储结构。有向图中每条弧对应一个弧结点
,每个顶点也对应一个顶点结点
。
空间复杂度
:O(|V|+|E|)- 顺着绿色的线路找,可以找到指定顶点的
出边
。 - 顺着橙色的线路找,可以找到指定顶点的
入边
。 - 顶点与顶点之间是顺序存储。
- 图的十字链表不是唯一的,但一个十字链表表示确定一个图。
邻接多重表
邻接多重表是无向图
的一种存储方式。每条边对应一个边结点
,每个顶点对应一个顶点结点
。
空间复杂度
:O(|V|+|E|)寻找指定顶点的边
:以B为例,通过firstedge找到AB边,由于B是绿色,沿着绿色找,找到CB边,B还是绿色,再沿着绿色找到EB边。删除边
:将与边相连接的两个顶点的firstedge分别指向删除边的下一条边。如删除AB边结点,A是橙色,所以A的firstedge指向橙色ILink所指的AD边,B是绿色,B的firstedge指向绿色jLink所指的CB边。
删除顶点
:如删除顶点E,将顶点E和与其相连的EB边、CE边与随之删除。由于CB边结点的jLink指向了EB边结点,所以将CB边的jLink置为null,同理,将CD边结点的iLink也置为null。
基本操作
-
Adjacent(G , x , y)
:判断图G是否存在边<x, y>或(x, y)
在邻接矩阵中,比如判断是否存在CE边,只需要判断G[2][4]
或G[4][2]
的值是否为1即可,所以时间复杂度是O(1)。
在邻接表中,遍历顶点C或者顶点E的边链表,即可判断出是否存在对应的边。最好的情况是边链表的第一个结点就是要找的,最坏的情况是遍历完都没有找到,由于一个顶点最多可以连接n-1个顶点,所以遍历的结点数为1(n-1),时间复杂度就是O(1)O(|V|)。
有向图也是如此。 -
Get_edge_value(G , x , y)
:获取图G中边(x, y)或<x, y>对应的权值。与Adjacent(G , x , y)操作雷同,核心在于找到边。 -
Set_edge_value(G , x , y , v)
:设置图G中边(x, y)或<x, y>对应的权值为v。与Adjacent(G , x , y)操作雷同,核心在于找到边。 -
Neighbors(G , x)
:列出图G中与结点x邻接的边
无向图邻接矩阵:遍历结点X所在的那一行或者那一列即可找到与X邻接的边;
无向图邻接表:遍历结点X所对应的边链表,即可找到与X邻接的边。
有向图邻接矩阵:遍历结点X所在的那一行和那一列,即可找到X的入边和出边;
有向图邻接表:遍历结点X的边链表,即可找到X的出边,遍历所有的边链表可以找到X的入边。 -
InsertVertex(G , x)
:在图G中插入顶点x
邻接矩阵插入顶点时,在二维数组中添加一行和一列。
邻接表插入顶点时,在顶点表中添加一个元素即可。 -
DeleteVertex(G , x)
:从图G中删除顶点x
无向图邻接矩阵:在边表中将顶点所在的行和列元素都置为0,再将顶点表中的对应元素置为null。
无向图邻接表:将对应顶点的边链表清空,再遍历其余的边链表,将元素X删除。 -
AddEdge(G , x , y)
:若无向边(x, y)或有向边<x, y>不存在,则向图G中添加该边
比如添加CF边。在邻接矩阵中,将G[2][5]
和G[5][2]
的值置为1;在邻接表中,在顶点C和F的边链表中,采用头插法添加对应结点。 -
RemoveEdge(G , x , y)
:若无向边(x, y)或有向边<x, y>存在,则从图G中删除该边。与AddEdge(G , x , y) 操作类似。 -
FirstNeighbor(G , x)
:求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1
邻接矩阵中,遍历顶点X 对应的那一行或者那一列,找到第一个值为1的顶点。
邻接表中,遍历顶点X对应的边链表,找到第一个链表元素。
有向图邻接表找入边结点时,需要遍历处顶点X外所有的边链表,时间复杂度 O(1)~O(|E|) -
NextNeighbor(G , x , y)
:假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1。与FirstNeighbor(G , x) 操作类似。
6.3 图的遍历
广度优先遍历(BFS)
广度优先遍历(Breadth-First-Search,BFS)类似于二叉树的层序遍历算法。首先访问起始结点v,接着由v出发,依次访问v的各个未访问过的邻接顶点w1,w2...,然后依次访问w1,w2...的所有未被访问过的邻接顶点;再从这些访问过的顶点出发,访问它们所有未被访问过的邻接顶点,直到图中所有顶点都被访问过为止。
#include <iostream>
using namespace std;
#define MaxVertexNum 100 //顶点的数目的最大值
typedef int VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct {
VertexType Vex[MaxVertexNum]; //顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexNum, arcNum; //图的当前顶点数和弧数
} MGraph;
//求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1
VertexType FirstNeighbor(MGraph graph, int x) {
for (int j = 1; j <= graph.vexNum; ++j) {
if (graph.Edge[x][j] == 1) {
return j;
}
}
return -1;
}
//假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1
VertexType NextNeighbor(MGraph graph, int x, int y) {
for (int j = y + 1; j <= graph.vexNum; ++j) {
if (graph.Edge[x][j] == 1) {
return j;
}
}
return -1;
}
bool visited[MaxVertexNum]; //访问标记数组
SqQueue queue; //辅助队列,队列相关代码省略,参考队列那一章
//从顶点编号为v的结点开始 广度优先遍历
void BFS(MGraph graph, int v) {
EnQueue(queue, v); //入队
visited[v] = true; //将访问过的结点标记为true
//队列元素依次出队并输出结点信息
while (!QueueEmpty(queue)) {
DeQueue(queue, v); //出队
cout << graph.Vex[v] << endl; //输出结点信息
//将队头结点的未被访问过的邻接结点依次入队
for (VertexType w = FirstNeighbor(graph, v); w > 0; w = NextNeighbor(graph, v, w)) {
if (!visited[w]) {
EnQueue(queue, w);
visited[w] = true;
}
}
}
}
//对图graph进行广度优先遍历
void BFSTraverse(MGraph graph) {
//初始化标记数组
for (int i = 1; i < MaxVertexNum; ++i) {
visited[i] = false;
}
InitQueue(queue);
//遍历访问数组,将对访问的结点开始BFS,
//非连通图以及有向图从一个顶点出发不一定能对所有的顶点都进行遍历,所以需要多次BFS
//为了方便说明,顶点从1开始
for (int j = 1; j <= graph.vexNum; ++j) {
if (!visited[j]) {
BFS(graph, j);
}
}
}
与二叉树的BFS类似,借助辅助队列实现。从顶点1开始,1出队时与其相邻的2、5入队;2出队时与其相邻的6入队;5出队;6出队时与其相邻的3、7入队;3出队时与其相邻的4入队;7出队时与其相邻的8入队;4出队;8出队。
从不同的顶点开始,所得到的遍历序列不同。
同⼀个图的邻接矩阵表示方式唯⼀,因此从一个顶点出发,⼴度优先遍历序列唯⼀。
同⼀个图邻接表表示方式不唯⼀,因此从一个顶点出发,⼴度优先遍历序列不唯⼀。
广度优先生成树
由于图的广度优先遍历和树的层序遍历类似,所以在基于连通图的广度优先遍历过程中,可以得到一棵遍历树,这棵树就是广度优先生成树
。而基于非连通图的广度优先遍历,得到的就是广度优先遍历森林
。
⼴度优先⽣成树由⼴度优先遍历过程确定。图的邻接矩阵存储表示是唯一的,所以基于邻接矩阵的广度优先生成树是唯一的。由于邻接表的表示⽅式不唯⼀,因此基于邻接表的⼴度优先⽣成树也不唯⼀。
深度优先遍历(DFS)
深度优先遍历(Depth-First-Search,DFS)类似于树的先根遍历。首先访问图中某一起始点v,然后由v出发,访问与v邻接且未被访问的任一顶点w1,再访问与w1邻接且未被访问的任一顶点w2...重复以上过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。
#include <iostream>
using namespace std;
#define MaxVertexNum 100 //顶点的数目的最大值
typedef int VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct {
VertexType Vex[MaxVertexNum]; //顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexNum, arcNum; //图的当前顶点数和弧数
} MGraph;
//求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1
VertexType FirstNeighbor(MGraph graph, int x) {
for (int j = 1; j <= graph.vexNum; ++j) {
if (graph.Edge[x][j] == 1) {
return j;
}
}
return -1;
}
//假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1
VertexType NextNeighbor(MGraph graph, int x, int y) {
for (int j = y + 1; j <= graph.vexNum; ++j) {
if (graph.Edge[x][j] == 1) {
return j;
}
}
return -1;
}
bool visited[MaxVertexNum]; //访问标记数组
//从顶点编号为v的结点开始 深度优先遍历
void DFS(MGraph graph, int v) {
cout << graph.Vex[v] << endl; //输出结点信息
visited[v] = true; //将访问过的结点标记为true
//递归调用DFS,对顶点的未被访问过的邻接结点依次进行DFS
for (VertexType w = FirstNeighbor(graph, v); w > 0; w = NextNeighbor(graph, v, w)) {
if (!visited[w]) {
DFS(graph, w);
}
}
}
//对图graph进行深度优先遍历
void DFSTraverse(MGraph graph) {
//初始化标记数组
for (int i = 1; i < MaxVertexNum; ++i) {
visited[i] = false;
}
//遍历访问数组,将对访问的结点开始DFS
//非连通图以及有向图从一个顶点出发不一定能对所有的顶点都进行遍历,所以需要多次DFS
//为了方便说明,顶点从1开始
for (int j = 1; j <= graph.vexNum; ++j) {
if (!visited[j]) {
DFS(graph, j);
}
}
}
从顶点1开始,1邻接有2,访问2;2邻接有6,访问6;6邻接有3,访问3;3邻接有4,访问4;4邻接有7,访问7;7邻接有8,访问8;调用栈退回到1,1还邻接有未被访问过的5,访问5。得到深度优先遍历序列12634785。
深度优先生成树
与广度优先遍历一样,深度优先遍历也会产生一棵深度优先生成树。即对连通图调用 DFS 才能产生深度优先生成树,否则产生的将是深度优先生成森林,与 BFS 类似,基于邻接表存储的深度优先生成树是不唯一的。
图的遍历与图的连通性
6.4 图的应用
最小生成树
对于⼀个带权连通⽆向图
G = (V, E),⽣成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设R为G的所有⽣成树的集合,若T为R中边的权值之和最小的⽣成树
,则T称为G的最⼩⽣成树
(Minimum-Spanning-Tree, MST)。
- 最⼩⽣成树可能有多个,但边的权值之和总是唯⼀且最小的
- 最⼩⽣成树的边数 = 顶点数 - 1。砍掉⼀条则不连通,增加⼀条边则会出现回路
- 如果⼀个连通图本身就是⼀棵树,则其最⼩⽣成树就是它本身
- 只有连通图才有⽣成树,⾮连通图只有⽣成森林
Prim算法
从某⼀个顶点开始构建生成树;每次将代价最⼩的新顶点纳⼊⽣成树,直到所有顶点都纳⼊为⽌。
时间复杂度:O(|V|²),适合⽤于边稠密图。
Kruskal算法
每次选择⼀条权值最⼩的边,使这条边的两头连通(原本已经连通的就不选)直到所有结点都连通。
时间复杂度:O( |E| log₂|E| ),适合⽤于边稀疏图。
最短路径问题——BFS算法(无权图)
前面提到,图的BFS和树的先根遍历类似,所以从起始顶点出发到达某一顶点的单源最短路径,可以看做是将图转换为广度优先生成树,路径长度为顶点的深度。代码实现方式就是在BFS算法的基础上加以改进。
#include <iostream>
#include <climits>
using namespace std;
/**
* 图和队列的定义及相关操作的代码省略,参考前面章节
* 顶点从1开始
**/
bool visited[MaxVertexNum]; //访问标记数组
SqQueue queue; //辅助队列
int d[MaxVertexNum]; //起始顶点到各个顶点的路径长度
int path[MaxVertexNum]; //记录各个顶点的前驱结点
//从顶点编号为v的结点开始 广度优先遍历 求最短路径
void BFS_MIN_Distance(MGraph graph, int u) {
EnQueue(queue, u); //入队
visited[u] = true; //将访问过的结点标记为true
d[u] = 0; //起始顶点,路径为0
//队列元素依次出队并输出结点信息
while (!QueueEmpty(queue)) {
DeQueue(queue, u); //出队
//将队头结点的未被访问过的邻接结点依次入队
for (VertexType w = FirstNeighbor(graph, u); w > 0; w = NextNeighbor(graph, u, w)) {
if (!visited[w]) {
path[w] = u; //将w的前驱结点设为u
d[w] = d[u] + 1; //u是w的前驱结点,将w的路径置为前驱结点的路径+1
EnQueue(queue, w); //出队
visited[w] = true; //设为已访问
}
}
}
}
//对图graph进行广度优先遍历
void BFSTraverse(MGraph graph) {
//初始化标记数组
for (int i = 1; i < MaxVertexNum; ++i) {
visited[i] = false;
d[i] = INT_MAX;
path[i] = -1;
}
InitQueue(queue);
//遍历访问数组,将对访问的结点开始BFS
for (int j = 1; j <= graph.vexNum; ++j) {
if (!visited[j]) {
BFS_MIN_Distance(graph, j);
}
}
}
添加了两个数组d
和path
,在访问顶点时,修改其最短路径⻓度 d[ ] 并在 path[ ]中 记录前驱结点。
最短路径问题——Dijkstra算法(带权图,无权图)
Dijkstra算法借助三个辅助数组:
final[]
:标记各顶点是否已找到最短路径dist[]
:记录从起始顶点到达各顶点的最短路径长度path[]
:记录各顶点的前驱结点
每一轮都循环遍历所有结点,找到还没确定最短路径,且dist 最⼩的顶点Vi,令final[i]=ture。并检查所有邻接⾃ Vi 的顶点,若其 final 值为false,则更新 dist 和 path 信息。
初始
:从V0开始,初始化三个数组,final[V0]=true,其余false;dist数组中V0直接相邻的顶点记录长度,其余的为∞;path数组中和V0直接相邻的顶点标记为0,表示V0是其前驱结点,其余为-1。
第1轮
:还没确定最短路径,且dist最小的顶点为V4,令final[V4]=true。邻接自V4且final值为false的顶点为V1、V2和V3。更新dist[V1]=8,path[V1]=4;dist[V2]=14,path[V2]=4;dist[V3]=7,path[V3]=4。
第2轮
:还没确定最短路径,且dist最小的顶点为V3,令final[V3]=true。邻接自V3且final值为false的顶点为V2。更新dist[V2]=13,path[V2]=3。
第3轮
:还没确定最短路径,且dist最小的顶点为V1,令final[V1]=true。邻接自V1且final值为false的顶点为V2。更新dist[V2]=9,path[V2]=1。
第4轮
:还没确定最短路径,且dist最小的顶点为V2,令final[V2]=true。
现在通过dist数组和path数组就可以得到各个顶点到起始顶点的最短路径和路径长度的信息了。比如V0到V2 的最短(带权)路径⻓度为:dist[2] = 9。通过 path[ ] 可知,V0到V2 的最短(带权)路径:V2 <— V1 <— V4 <— V0。
每一轮处理都要扫描数组,时间复杂度为O(n),一共n-1轮,时间复杂度为O(|V|²)。
Dijkstra 算法不适⽤于有负权值的带权图。
//求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1
//result[0]存储顶点编号,result[1]存储路径长度
void FirstNeighbor(MGraph graph, int x, int result[2]) {
for (int j = 0; j < graph.vexNum; ++j) {
if (graph.Edge[x][j] != INT_MAX && graph.Edge[x][j] != 0) {
result[0] = j;
result[1] = graph.Edge[x][j];
return;
}
}
result[0] = -1;
result[1] = INT_MAX;
}
//假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1
void NextNeighbor(MGraph graph, int x, int y, int result[2]) {
for (int j = y + 1; j < graph.vexNum; ++j) {
if (graph.Edge[x][j] != INT_MAX && graph.Edge[x][j] != 0) {
result[0] = j;
result[1] = graph.Edge[x][j];
return;
}
}
result[0] = -1;
result[1] = INT_MAX;
}
void Dijkstra_MIN_Distance(MGraph graph) {
VertexType startVex = 0; //V0作为起始顶点
bool final[VEX_NUM]; //标记各顶点是否已找到最短路径,true表示已找到
int dist[VEX_NUM]; //标记各顶点到起始顶点的最短路径
int path[VEX_NUM]; //标记各顶点的直接前驱
//初始化三个数组
for (int i = 0; i < VEX_NUM; ++i) {
final[i] = false;
dist[i] = INT_MAX;
path[i] = -1;
}
final[startVex] = true; //起始顶点的final值设为true
dist[startVex] = 0; //起始顶点到起始顶点的路径长度为0
//用作FirstNeighbor与NextNeighbor的返回值,result[0]存储顶点编号,result[1]存储路径长度
int result[2];
//找到与起始顶点V0邻接的顶点,更新dist数组与path数组
for (FirstNeighbor(graph, startVex, result); result[0] != -1;
NextNeighbor(graph, startVex, result[0], result)) {
dist[result[0]] = result[1]; //邻接顶点到起始顶点V0的路径长度
path[result[0]] = startVex; //邻接顶点的前驱顶点设为V0
}
//n-1轮处理
for (int j = 0; j < VEX_NUM - 1; ++j) {
int minDistVex = -1; //还没确定最短路径且dist最小的顶点
int minDist = INT_MAX; //还没确定最短路径,且dist最小的顶点的路径长度
//找到还没确定最短路径,且dist最⼩的顶点
for (int i = 0; i < VEX_NUM; ++i) {
if (!final[i] && dist[i] < minDist) {
minDistVex = i;
minDist = graph.Edge[startVex][i];
}
}
final[minDistVex] = true;
//检查所有邻接⾃ minDistVex 的顶点,若其 final 值为false,则更新 dist 和 path 信息
for (FirstNeighbor(graph, minDistVex, result);
result[0] != -1; NextNeighbor(graph, minDistVex, result[0], result)) {
if (!final[result[0]] && dist[minDistVex] + result[1] < dist[result[0]]) {
//假设现在是第一轮循环,minDistVex指向V4,result[0]指向V1
//dist[minDistVex]为V0到V4的路径长度,result[1]为V4到V1的路径长度
//dist[minDistVex] + result[1]为V0->V4->V1 =5+3=8 比V0到V1路径10要短
//所以将V1的路径长度设为8,前驱结点设为V4
dist[result[0]] = dist[minDistVex] + result[1];
path[result[0]] = minDistVex;
}
}
}
}
最短路径问题——Floyd算法(带权图,无权图)
Floyd算法使用动态规划思想,将问题的求解分为多个阶段,可以求出每一对顶点之间的最短路径。
对于n个顶点的图G,求任意⼀对顶点 Vi —> Vj 之间的最短路径可分为如下⼏个阶段:
- 初始:不允许在其他顶点中转,最短路径是?
-
0:若允许在 V0 中转,最短路径是?
-
1:若允许在 V0、V1 中转,最短路径是?
-
2:若允许在 V0、V1、V2 中转,最短路径是?
…………
-
n-1:若允许在 V0、V1、V2 …… Vn-1 中转,最短路径是?
为图G建立两个矩阵A和path,A矩阵用来记录各顶点间的最短路径长度,path矩阵用来记录两个顶点之间的中转点。每一轮增加一个新的中转,比较通过中转点进行中转与不进行中转的路径长度,更新A矩阵和path矩阵。
初始
:不允许在其它顶点之间中转的最短路径。
#0
:允许通过V0 中转。用于各个顶点都没有到达V0的路径,所以两个矩阵都保持不变
#1
:允许在 V0、V1之间中转。V2→V3增加作为V1进行中转路径更短,V2→V4增加V1作为中转点进行中转更短,更新矩阵A和path。
#2
:允许在 V0、V1、V2之间中转。更新V0→V1,V0→V3,V0→V4矩阵信息。
#3
:允许在 V0、V1、V2、V3之间中转。更新V0→V4,V1→V4,V2→V4矩阵信息。
#4
:若允许在 V0、V1、V2、V3、V4之间中转。通过比较发现,没有需要更新的信息。
现在通过A矩阵就可以得出任意两个顶点之间的最短路径长度,通过path矩阵可以递归地找到完整路径。比如V0到V4的路径:V0通过V3中转到达V4,V0通过V2中转到达V3,V2又通过V1中转到达V1。所以完整路径为V0→V2→V1→V3→V4。
void Floyd_MIN_Distance(MGraph graph) {
VertexType A[VEX_NUM][VEX_NUM]; //记录各顶点间的最短路径长度
VertexType path[VEX_NUM][VEX_NUM]; //记录两个顶点之间的中转点
//初始化两个矩阵
for (int i = 0; i < VEX_NUM; ++i) {
for (int j = 0; j < VEX_NUM; ++j) {
A[i][j] = graph.Edge[i][j];
path[i][j] = -1;
}
}
//考虑添加Vk作为中转点
for (int k = 0; k < VEX_NUM; ++k) {
//遍历整个矩阵,i为行号,j为列号
for (int i = 0; i < VEX_NUM; ++i) {
for (int j = 0; j < VEX_NUM; ++j) {
//如果以k为中转点的路径更短,则更新A矩阵与path矩阵
if (A[i][k] != INT_MAX && A[k][j] != INT_MAX && A[i][j] > A[i][k] + A[k][j]) {
A[i][j] = A[i][k] + A[k][j];
path[i][j] = k;
}
}
}
}
}
有向无环图描述表达式
若⼀个有向图中不存在环,则称为有向⽆环图
,简称DAG
图(Directed Acyclic Graph)。有向无环图是描述含有公共子式的表达式的有效工具。
Step1:把各个操作数不重复地排成⼀排。
Step2:标出各个运算符的⽣效顺序(先后顺序有点出⼊⽆所谓)。
Step 3:按顺序加⼊运算符,注意“分层”。
Step 4:从底向上逐层检查同层的运算符是否可以合体。
拓扑排序
若用DAG
图(有向无环图)表示一个工程,其顶点表示活动,用有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行的这样一种关系,则将这种有向图称为用顶点表示活动的网
,记为AOV
网(Activity On Vertex NetWork)。简单的说,拓扑排序就是找到做事的先后顺序
。每个AOV⽹都有⼀个或多个拓扑排序序列。
拓扑排序的实现步骤如下:
- 从AOV⽹中选择⼀个没有前驱的顶点并输出。
- 从⽹中删除该顶点和所有以它为起点的有向边。
- 重复①和②直到当前的
AOV⽹为空
或当前⽹中不存在⽆前驱的顶点为⽌
。
使用代码实现时,借助两个数组indegree、print和一个栈S。其中,数组indegree用来保存各个顶点当前的入度值,print数组用来保存拓扑排序序列,栈S用来保存度为0的顶点,栈也可以换成队列。
使用邻接表存储图,时间复杂度为O(|V|+|E|);使用邻接矩阵时间复杂度为O(|V|²)。
void Init(ALGraph graph, SqStack &S, int inDegree[VEX_NUM], int print[VEX_NUM]) {
InitStack(S);
//初始化两个数组
for (int j = 0; j < VEX_NUM; ++j) {
inDegree[j] = 0;
print[j] = -1;
}
//扫描所有的边链表,初始化各顶点当前的入度信息
for (int i = 0; i < VEX_NUM; ++i) {
for (ArcNode *p = graph.vertices[i].first; p; p = p->next) {
if (p->adjVex == 0) {
cout << endl;
}
inDegree[p->adjVex]++;
}
}
}
//拓扑排序,图采用邻接表存储,用邻接矩阵代码略有不同,但思路一致
bool TopologicalSort(ALGraph graph) {
int inDegree[VEX_NUM]; //存储当前各个顶点的入度
int print[VEX_NUM]; //记录拓扑排序序列
SqStack S; //保存度为0的顶点
Init(graph, S, inDegree, print);
int count = 0; //记录当前已输出的顶点数
//将度为0的顶点压入栈
for (int i = 0; i < VEX_NUM; ++i) {
if (inDegree[i] == 0) {
Push(S, i);
}
}
//栈非空,存在度为0的顶点
while (!StackEmpty(S)) {
int popEle; //栈顶元素
Pop(S, popEle); //第二个参数是引用类型,弹出的元素赋值给popEle
print[count++] = popEle;
//遍历顶点popEle的边链表,将边指向的顶点入度减1
for (ArcNode *p = graph.vertices[popEle].first; p; p = p->next) {
int v = p->adjVex; //边p所指向的顶点
//顶点v的入度减1,减1后入度为0的话压入栈中
if (!(--inDegree[v])) {
Push(S, v);
}
}
}
//count小于graph.vexNum说明存在回路,排序失败
return count >= graph.vexNum;
}
逆拓扑排序
对⼀个AOV⽹,如果采⽤下列步骤进⾏排序,则称之为逆拓扑排序
:
- 从AOV⽹中选择⼀个没有后继(
出度为0
)的顶点并输出。 - 从⽹中删除该顶点和所有以它为终点的有向边。
- 重复①和②直到当前的AOV⽹为空。
也就是说,逆拓扑排序序列是拓扑排序序列的逆序列。同样,逆拓扑排序序列也不唯一。可以在DFS算法的基础上做一些小修改实现图的逆拓扑排序序列。通过一个flag数组标记每个顶点的访问状态,如果正在访问的顶点又被访问了说明存在回路,存在回路的图不存在逆拓扑排序序列。
/**顶点编号从0开始**/
int flag[MaxVertexNum]; //访问标记数组,-1表示未被访问,0表示正在被访问,1表示访问访问结束
//从顶点编号为v的结点开始 深度优先遍历
void DFS(MGraph graph, int v) {
flag[v] = 0; //标记顶点正在被访问
//递归调用DFS,对顶点的未被访问过的邻接结点依次进行DFS
for (int w = FirstNeighbor(graph, v); w >= 0; w = NextNeighbor(graph, v, w)) {
if (flag[w] == 0) { //正在被访问的顶点又被访问,说明存在环路
cout << "图中存在回路,不存在逆拓扑排序序列!" << endl;
return;
} else if (flag[w] == -1) { //如果没被访问,则进行DFS
DFS(graph, w);
}
}
flag[v] = 1; //访问过的顶点标记为1
cout << graph.Vex[v] << endl; //输出结点信息
}
//对图graph进行深度优先遍历
void DFSTraverse(MGraph graph) {
//初始化标记数组
for (int i = 0; i < MaxVertexNum; ++i) {
flag[i] = -1;
}
//遍历访问数组,将对访问的结点开始DFS
//非连通图以及有向图从一个顶点出发不一定能对所有的顶点都进行遍历,所以需要多次DFS
for (int j = 0; j < graph.vexNum; ++j) {
if (flag[j] == -1) {
DFS(graph, j);
}
}
}
比如这张图,0未访问,DFS顶点0,将0标记为正在访问;0邻接1,DFS顶点1,将1标记为正在访问;1邻接3,DFS顶点3,将3标记为正在访问;3邻接4,DFS顶点4,将顶点4标记为正在访问;4邻接2,DFS顶点2,将2标记为正在访问;2邻接3,DFS顶点3,由于3已经被标记为正在访问,所以说明图存在回路,没有逆拓扑排序序列。如果图中没有红色箭头,按照递归DFS的逻辑,可以正常输出逆拓扑排序序列43102。
关键路径
在带权有向图中,以顶点表示事件
,以有向边表示活动
,以边上的权值表示完成该活动的开销
(如完成活动所需的时间),称之为⽤边表示活动的⽹络
,简称AOE⽹
(Activity On Edge NetWork)。在AOE⽹中仅有⼀个⼊度为0的顶点,称为开始顶点(源点,图中V1)
,它表示整个⼯程的开始;也仅有⼀个出度为0的顶点,称为结束顶点(汇点,图中V4)
,它表示整个⼯程的结束。从源点到汇点的多条路径中,具有最⼤路径⻓度的路径称为关键路径
(V1→V2→V3→V4),⽽把关键路径上的活动称为关键活动
。
AOE⽹具有以下如下几个性质:
- 只有在某顶点所代表的事件发⽣后,从该顶点出发的各有向边所代表的活动才能开始;
- 只有在进⼊某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发⽣。有些活动是可以并⾏进⾏的。
- 若关键活动耗时增加,则整个⼯程的⼯期将增⻓。
- 缩短关键活动的时间,可以缩短整个⼯程的⼯期。
- 当缩短到⼀定程度时,关键活动可能会变成非关键活动。
事件vk的最早发⽣时间ve(k)
:决定了所有从vk开始的活动能够开⼯的最早时间。例ve(V3)=1+3=4。事件vk的最迟发⽣时间vl(k)
:它是指在不推迟整个⼯程完成的前提下,该事件最迟必须发⽣的时间。例vl(V3)=4。活动ai的最早开始时间e(i)
:指该活动弧的起点所表⽰的事件的最早发⽣时间。例e(a4)=1+3=4。活动ai的最迟开始时间l(i)
:它是指该活动弧的终点所表示事件的最迟发⽣时间与该活动所需时间之差。例l(a1)=4-2=2。活动ai的时间余量d(i)=l(i)-e(i)
:表⽰在不增加完成整个⼯程所需总时间的情况下,活动ai可以拖延的时间。d(i)=0的活动ai是关键活动。例d(a1)=l(a1)-e(a1)=2-0=2。
① 按拓扑排序序列,依次求各个顶点的最早发生时间
ve(k)
:
ve(源点) = 0。
ve(k) = Max{ve(j) + Weight(vj,vk)},vj为vk 的任意前驱;以V4为例,V2和V3都是V4的前驱,所以等V2和V3都执行完了才可以执行V4,所以ve(4)=ve(3)+4=6。
- ve(1) = 0
- ve(3) = 2
- ve(2) = 3
- ve(5) = 6
- ve(4) = max{ve(2)+2, ve(3)+4} = 6
- ve(6) = max{ve(5)+1, ve(4)+2, ve(3)+3} = 8
② 按逆拓扑排序序列,依次求各个顶点的最迟发生时间
vl(k)
:
vl(汇点) = ve(汇点)
vl(k) = Min{vl(j) - Weight(vk , vj)} , vj为vk的任意后继;以V3为例,V4和V6是后继结点,V4最迟在时间6执行,V6最迟在时间8执行,也就是V4是先于V6执行的,V3→V4要时间4,V3→V6要时间3,如果V3不在6-4=3时执行,便会延迟V4的执行,所以vl(3)=Min(6-4 , 8-3)=2。
- vl(6) = 8
- vl(5) = 7
- vl(4) = 6
- vl(2) = min{vl(5)-1, vl(4)-2} = 4
- vl(3) = min{vl(4)-4, vl(6)-3} = 2
- vl(1) = 0
③求所有活动的最早发生时间。若边<vk, vj>表⽰活动ai,则有
e(i) = ve(k)
。活动在图中是用弧来表示的,活动的最早发生时间是弧尾所指向的顶点的最早发生时间。
④求所有活动的最迟发⽣时间。若边<vk, vj>表⽰活动ai,则有l(i) = vl(j) - Weight(vk, vj)。就是弧头指向顶点的最晚发生时间-弧的权值。例如V6的最晚发生时间是8,所以活动a7的最晚发生时间就是8-2=6。
⑤ 求所有活动的时间余量 d( )。d(i) = l(i) - e(i)。活动的最迟发生时间-最早发生时间。d(k)=0的活动就是关键活动, 由关键活动可得关键路径。
⭐⭐⭐⭐⭐
转载请注明出处:
https://blog.csdn.net/weixin_43461520/article/details/124176292本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star
如果您觉得文章还不错,请给我来个
点赞
,收藏
,关注
学习更多编程知识,WeChat扫描下方二维码关注公众号『 R o b o d 』: