目录
- 第六章 图
- 6.1 图的基本概念
- 知识回顾
- 6.2 图的储存结构(邻接矩阵法)
- 1. 数组表示法
- (1) 有向图,无向图的邻接矩阵
- 2. 定义邻接矩阵的结构
- 3. 定义图的结构
- 4. 构造图G
- 5. 特点
- 6.3 储存结构(邻接表表示法)
- 1. 储存方式
- 2. 结构
- 3. 图的邻接表存储表示(算法)
- 4. 结论
- 5. 邻接矩阵和邻接表的对比
- 6.4.拓展存储结构(十字链表,邻接多重表)很绕多理解
- 1. 十字链表(存储有向图)
- 2. 邻接多重表(存储无向图)
- 6.5 四种存储结构的对比
- 6.6. 图的基本操作
- 1. Adjacent(G,x,y)边的存在
- 2. Neighbors(G,x):列出图G中与结点x邻接的边
- 3. InsertVertex(G,x):在图G中插入顶点x
- 4. DeleteVertex(G,x):从图G中删除顶点x
- 5. AddEdge(G,x,y):若无向边(x,y)或有向边<x,y>不存在,则向图G中添加该边。
- 6. FirstNeighbor(G,x):求图G中顶点的第一个邻接点
- 7. NextNeighbor(G,x,y):
- 8. get_edge_value,set
- 6.7 图的遍历
- 1. 图遍历的概述
- 2.广度优先遍历(队列)
- (1)算法描述
- (2)算法实现
- (3)分析
- (4)广度优先生成树(森林)
- (5)BFS 单源最短路径问题
- 3. 深度优先遍历(栈)
- (1)算法描述
- (2)算法实现
- (3)时间/空间复杂度
- 6.8 图的应用
- 一、最小生成树
- 1、最小生成树的概念
- 2、求最小生成树的两种方法
- 二、最短路径
- 1.无权图的单源最短路径问题——BFS算法
- 2.单源最短路径问题——Dijkstra算法
- 3.各顶点间的最短路径问题——Floyd算法
- 4.最短路径算法比较
- 三、有向⽆环图描述表达式
- 1.有向⽆环图
- 四、拓扑排序
- 1. AOV网(Activity on Vertex Network,用顶点表示活动的网)
- 2. 拓扑排序
- 3. 拓扑排序的实现:
- 4. 代码实现拓扑排序(邻接表实现):
- 五、 逆拓扑排序
- DFS 逆拓扑排序
- 思考: 如何判断回路
- 六、关键路径
- 1. AOE 网
- 2. 源点 汇点
- 3. 求关键路径
- 4. 特性
- 5. 总结
第六章 图
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一定是非空集。
- 弧头和弧尾
<x,y>表示从顶点到顶点y的一条弧,并称x为弧尾或起始点,称y为弧头或终端点
- 无向图,有向图:有向图<u,v>,其中u表示弧尾,v表示弧头。
- 简单图:简单图:①不存在重复边,② 不存在顶点到自身的边。
- 多重图:图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则G为多重图。
- 度,入度,出度:① 无向图:在具有n个顶点,e条边的无向图中,全部顶点的度的和等于边数的2倍(一条边连两个顶点,所以一条边两个度)。②有向图:度之和入度+出度(一条边一个出度一个入度),在具有n个顶点,e条边的有向图中,入度=出度=e。
- 路径,简单路径,路径长度:① 在路径序列中,顶点不重复出现的路径称为简单路径。②路径长度:路径上边的数目(简单就意味着顶点不重复)。
- 回路,简单回路:除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
- 点到点到距离:从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为u到v的距离。若u到v根本不存在路径,则记该距离为无穷(∞)。
- 连通图,强连通图:①若图G中任意两个顶点都是连通的,则称图G为连通图(无向图中的连通就是两个顶点之间路径存在;有向图中的强连通是两个顶点之间存在往返的路径,就是w到v和v到w都有路径,强连通是有向图独有的概念),否则称为非连通图。对于n个顶点的无向图G,若G是连通图,则至少有n-1条边(就是把每个顶点都连线一个顶点,就存在路径了),若G是非连通图,则最多可能有 C n − 1 2 C_{n-1}^{2} Cn−12,(就是除去一个顶点,其他顶点两两相连,只要不连接最后一个顶点,这个图就是不连通的最极端情况)。②若图中任何一项顶点都是强连通的,则称为图为强连通图。对于n个顶点的有向图G,若G是强连通图,则最少为n条边(形成回路)。
- 连通的补充图解:
- 连通的最少边数:
- 不连通的最多边数:
- 强连通的最少边数
-
子图(不用包含所有顶点)、生成子图:若包含所有顶点的子图,就称为生成子图。并不是任意几个点,任意几条边都能构成子图(例如不构成图的情况)。
-
连通分量:无向图中极大连通子图(子图必须连通,且包含尽可能多的顶点和边)称为连通分量。
-
强连通分量:有向图中有极大强连通子图(子图必须强连通,同时保留尽可能多的边)称为有向图的强连通分量。
-
生成树:连通图的生成树是包含图中全部顶点的一个极小连通子图(边尽可能的小,但要保持连通)若图中顶点数是n,则它的生成树含有n-1条边,对于生成树而言,若砍去一条边,则会变成非连通图,若加上一条边则会形成一个回路(环)。
-
生成森林:在非连通图中,连通分量的生成树构成了非连通图的生成森林。
-
边的权,带权图(网)
-
无向完全图:无向图中任意两个顶点之间都存在边。
-
有向完全图:有向图中任意两个顶点之间都存在方向相反的两条弧。
-
稀疏图,稠密图:这二者是相对的
-
树:不存在回路,且连通的无向图,n个顶点的树,必有n-1条边。
-
有向树:一个顶点的入度为0,其余顶点的入度均有1的有向图,称为有向树。
n个顶点的图,若|E|>n-1,则一定有回路。
知识回顾
6.2 图的储存结构(邻接矩阵法)
1. 数组表示法
(1) 有向图,无向图的邻接矩阵
① 对于无向图,顶点vi的度是邻接矩阵中第i行(或第i列)的元素之和, TD(vi)= ∑ a[i][j]。
② 对于有向图,顶点VI的出度OD(vi)是邻接矩阵中第i行的元素之和,顶点vi的出度ID(vi)是邻接矩阵中第j列)的元素之和。
③ 邻接矩阵法求顶点的度。入度。出度的时间复杂度为O(|V|)。
2. 定义邻接矩阵的结构
#define INFINITY INT_MAX
#define MAX_VERTEXT_NUM 20
typedef enum{DG,DN,AG,AN}GraphKind;
typedef struct ArcCell{
VRType adj;//对无向图,用0,1表示是否相邻,对于带权图,为权值
InfoType *info;//该弧相关信息的指针
}RrcCell,AdjMatrix[MAX_VERTEXT_NUM][MAX_VERTEXT_NUM];//邻接矩阵
3. 定义图的结构
(1)代码:
//定义图的结构
typedef struct {
VertextType exs[MAX_VERTEXT_NUM]; //顶点
AdjMatrix arcs[MAX_VERTEXT_NUM][MAX_VERTEXT_NUM]; //边的邻接矩阵
int vexnum,arcnum; //个数
GraphKind kind;//有向图?无向图?
}MGraph;
4. 构造图G
status CreateGraph(MGraph &G)
{
scanf(&G.kind)
{
case DG:return CreateDG(G);
case DN:return CreateDN(G);
case UDG:return CreateUDG(G);
case UDN:return CreateDGN(G);
default:return ERROR;
}
}
//用无向图为例
status CreateUDN(MGraph &G)
{
scanf(&G.arcnum,&G.vexnum,&IncInfo);//输入点数和边数
//给顶点进行数字化编号
for(i=0;i<G.vexnum;i++)
{ scanf(&G.exs[i]);//定义顶点数组(如果顶点本身就是1~n的数字无需这一步)
}
//初始化邻接矩阵
for(i=0;i<G.vexnum;i++)
{
for(j=0;j<G.vexnum;j++)
{
G.arcs[i][j]={ININITY,NULL};
}
}
//通过边数进行遍历
for(k=0;k<G.arcnum;K++)
{
scanf(&V1,&V2,&W);//输入邻接的连个顶点
i=locatteVex(G,V1);j=locateVex(G,V2);//查找V1,V2的位置
G.arcs[i][j].adj=w;//给邻接矩阵赋值
if(IncInfo)
{
INPUT(*G.arcs[i][j].info);
}
G.arcs[j][i]=G.arcs[i][j];//由于是无向图,对称
}
return ok;
}
5. 特点
优点:
- 无向图邻接矩阵是对称矩阵,同一条边表示了两次
- 顶点v的度:等于二维数组对应行(或列)中1的个数
- 判断两顶点v、u是否为邻接点:只需判二维数组对应分量是否为1
- 在图中增加、删除边:只需对二维数组对应分量赋值1或清0
- 占用存储空间只与它的顶点数有关,与边数无关;适用于边稠密的图
- 对有向图的数组表示法可做类似的讨论’‘
缺点:
- 不便于删除和增加顶点(增删边简单)
- 不便于统计边的数目,需要扫描邻接矩阵所有元素才能统计完毕,时间复杂度为O( n 2 n^2 n2 )
- 空间复杂度高,对于有向图,n个顶点需要 n 2 n^2 n2 个单元存储边,对于无向图,n(n-1)/2个单元,空间复杂度为O( n 2 n^2 n2 )
6.3 储存结构(邻接表表示法)
1. 储存方式
2. 结构
【1】顶点的结点结构
———————
| data | firstarc |
———————
data数据域:储存顶点vi
firstarc链域:指向链表中第一个结点
【2】弧的结点结构
——————————
| adjvex | info | nextarc |
——————————
adjvex邻接点域:与顶点vi邻接的点在图中的位置
info数据域:储存和边相关的信息,如权值
nextarc链域:与顶点vi的点在图中的位置
3. 图的邻接表存储表示(算法)
#define MAX_VERTEXT_NUM 20
//建立边结点
typedef struct ArcNode {
int adjvex; // 该弧所指向的顶点的位置
struct ArcNode *nextarc; // 指向下一条弧
InfoType *info; // 该弧相关信息(可选)
}ArcNode;
// 顶点结点
typedef struct VNode{
VertexType data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的弧
}VNode,AdjList[MAX_VERTEXT_NUM];
//邻接表
typedef struct {
Adjlist vertices;
int vexnum,arcnum;
int kind;
}ALGraph;
//建立邻接表算法
//初始化一个结点总数为num的图,k为图的类型,num为结点总数
void InitG(ALGraph G,enum GraphKind k,int num)
{
G.kind=k;
G.vexnum=num;
G.vertices=new VNode[vexnum];
for(int i=0;i<G.vexnum;i++)
{G.vertices[i].Firstarc=NULL;
cin>>G.vertics[i].data;
}
}
//有向图(网)增加弧的算法,将弧(from,to,weight)加入图
void InsertArc(ALGragh G,int from,int to,int weight)
{
ArcNode *s=new ArcNode;
s->weight=weight;
s->adjvex=to;
s->nextarc=G.vertices[from].firstarc;//插到链表vertices[from]的头
G.vertices[from].firstarc=s;
}
4. 结论
(1)在邻接表中,同一条边对应两个结点。
(2)无向图中顶点v的度:等于v 对应的链表的长度;但是,在有向图中,要求顶点A的的入度,则需要遍历所有的顶点连接的链表,判断有几个存在顶点A;求出度,则是A顶点链表有几个点。
(3)判定两顶点v,w是否邻接:要看v对应的链表中有无对应的结点w(相反判断也行);
(4)对于一个图,给定的邻接表是并不唯一的(区分与邻接矩阵)
(5)增减边:要在两个单链表插入、删除结点;
(6)占用存储空间与顶点数、边数均有关;适用于边稀疏的图
注意,在有向图的邻接表中不易找到指向该顶点的弧。
邻接矩阵表示唯一,邻接表表示不唯一
5. 邻接矩阵和邻接表的对比
6.4.拓展存储结构(十字链表,邻接多重表)很绕多理解
1. 十字链表(存储有向图)
- 空间复杂度:O(|V|+|E|)
- 同层可找所有出边即出度(绿色)
- 不同层相连的即所有入边(橙色)
- 比邻接表空间复杂度的
v
2
v^2
v2更小
2. 邻接多重表(存储无向图)
- 解决无向图冗余信息的问题,空间大
- 删除边,删除结点操作更简单(只要顺着指针找,就跟删除链表结点一样)
- 空间复杂度:O(|V|+|E|),比邻接表的v+2e,因为不存储无关的结点
6.5 四种存储结构的对比
6.6. 图的基本操作
无向图
有向图==(稀疏图也挺方便)==
1. Adjacent(G,x,y)边的存在
(无向边)<有向边>
-
思路:
①无向图:邻接矩阵,判断aij是否为1,邻接表,i点的邻接表是否有j点;
②有向图类似 -
时间复杂度
2. Neighbors(G,x):列出图G中与结点x邻接的边
- 思路:
①无向图:邻接矩阵,罗列出x点的行为1的所有点,邻接表,遍历x的链表的所有结点;
②有向图:邻接矩阵,出边遍历行,入边遍历列,邻接表,(出边)遍历x的链表,(入边)遍历所有的结点查看哪个结点值是x。 - 时间复杂度
3. InsertVertex(G,x):在图G中插入顶点x
- 思路:
① 无向图:邻接矩阵,给矩阵增加一个x行增加一个x列;邻接表,给表的最后一行增加x项,不接任何一个结点,指向NULL。
② 有向图:类似 - 时间复杂度
4. DeleteVertex(G,x):从图G中删除顶点x
- 思路:
① 无向图:邻接矩阵,将x的所有行列全部清空为-1,在顶点集中x的值表示为-1或false;邻接表,将所有与x有关的信息删除,需要遍历所有的结点。
② 有向图:邻接矩阵,与无向图类似,邻接表,删出边需要将x的链表都删除,删入边,需要遍历所有的结点。 - 时间复杂度
5. AddEdge(G,x,y):若无向边(x,y)或有向边<x,y>不存在,则向图G中添加该边。
- 思路:
① 无向图,邻接矩阵,将axy的值由0改为1=,邻接表,将x的链表后或前加上结点y==,最好使用头插法。
② 有向图,类似。 - 时间复杂度
6. FirstNeighbor(G,x):求图G中顶点的第一个邻接点
- 思路
① 无向图:邻接矩阵,扫描x的一行第一个为1的元素,可能第一个就是也可能到最后一个都没有;邻接表,查看x链表的第一个结点。
② 有向图:邻接矩阵,找x有关的行列(出边入边),也就是出边入边的第一个邻接点,邻接表,出边寻找简单,但是入边需要遍历所有的边。 - 时间复杂度
7. NextNeighbor(G,x,y):
假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1
8. get_edge_value,set
6.7 图的遍历
1. 图遍历的概述
1、定义——从某顶点出发,沿着一些边访问连通图中所有顶点,且使每个顶点仅访问一次的运算。
2、为避免重复访问,可设置辅助数组Visited[ ],各分量初值为0,当顶点被访问,对应分量被置为1。
3、方法
- 深度优先(depth first search DFS)
- 广度优先(breadth first search BFS)
2.广度优先遍历(队列)
(1)算法描述
- 从图中某个顶点V0出发,并在访问此顶点之后依次访问V0的所有未被访问过的邻接点,之后按这些顶点被访问的先后次序依次访问它们的邻接点,直至图中所有和V0有路径相通的顶点都被访问到。
- 对于非连通图,可能此时尚有顶点未被访问,则另选图中一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
- 因此关键在于:①找到与一个顶点相邻的所有结点;② 标记哪些顶点被访问过;③ 需要一个辅助队列存储。
使用邻接矩阵(唯一)所以广度优先遍历序列唯一
使用邻接表(不唯一)所以广度优先遍历序列不唯一
(2)算法实现
采用邻接表存储实现无向图的广度优先遍历
//visited是访问标记数组
//处理非连通图的情况
bool BFSTraverse(Graph G){
for(int i=0;i<G.vexnum;++i)
visited[i] = false;
InitQueue(Q);
for(int i=0;i<G.vexnum;++i){
if(!visited[i])
BFS(G,i);
}
}
void BFS(Graph G,int v){
visit(v); //访问v顶点
visited[v] = True; //修改该顶点对应数组的值为true
EnQueue(Q,v); //入队
while(!isEmpty(Q)){ //不空还有未遍历到的节点
DeQueue(Q,v); //出队v
for(w = FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)) //找到所有符合条件的邻接节点
if(!visited[w]){ //w是否被访问
visit[w]; //访问
visited[w] = true; //修改该顶点对应数组的值为true
EnQueue(Q,w); //入队
}
}
}
bool BFSTraverse(Graph G,int v){
for(int i=0;i<G.vexnum;++i)
visited[i] = false;
InitQueue(Q);
for(int i=0;i<G.vexnum;++i){
if(!visited[i])
visit(v); //访问v顶点
visited[v] = True; //修改该顶点对应数组的值为true
EnQueue(Q,v); //入队
while(!isEmpty(Q)){ //不空还有未遍历到的节点
DeQueue(Q,v); //出队v
for(w = FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)) //找到所有符合条件的邻接节点
if(!visited[w]){ //w是否被访问
visit[w]; //访问
visited[w] = true; //修改该顶点对应数组的值为true
EnQueue(Q,w); //入队
}
}
}
}
(3)分析
① 如果使用邻接表,则从同一个顶点广度优先遍历序列会随着链接表不同而不同,但是由于邻接矩阵是唯一的,所以从同一个广度优先遍历得到的顺序是唯一的。
②
- 对于无向图,调用BFS函数的次数=连通分量数
- 对于有向图而言,若起始顶点到其他顶点都有路径,则只需调用一次BFS/DFS函数。
- 对于强连通图,从任一结点出发只需调用1次BFS/DFS函数。
③ 空间复杂度:O(|V|) 辅助队列的最坏的情况(所有结点都入队)
④ 时间复杂度:
- a.使用邻接矩阵存储的图:访问|V|个顶点的需要O(|V|)的时间,查找每个顶点的邻接点都需要O(|V|)的时间,而总共有|V|个顶点,时间复杂度=O(|V|2)+o(|V|)=O(|V|2)。
- b.使用邻接表的图:访问|V|个顶点的需要O(|V|)的时间,查找各个顶点的邻接点都需要O(|E|)的时间,时间复杂度=O(|V|)+o(|E|)。
(4)广度优先生成树(森林)
① 通过广度优先遍历可以的得到一棵遍历树
② 由于邻接表不唯一,则树不唯一;由于邻接矩阵唯一,则树唯一。
遍历非连通图,可以得到广度优先生成森林。
(5)BFS 单源最短路径问题
3. 深度优先遍历(栈)
(1)算法描述
从图中某个顶点V0 出发,访问此顶点,然后依次从V0的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和V0有路径相通的顶点都被访问到。
(2)算法实现
邻接矩阵
int visited[MAX];//设置一个数组,判断是否遍历过,false/1为遍历过
void DFGTraverse(Graph G,int v)
{
for(v=0;v<G.vexnum;v++)
visited[v]=0;//初始化判断数组
for(v=0;v<G.vexnum;v++)
{
if(!visited[v])//如果没有遍历过
DFS(G,V);//进行遍历
}
}
void DFS(Graph G,int v)//进行递归遍历
{
visited[v]=1;printf(v);//改变判断数组,输出点
for(w=FirstVex(G,v);w!=0;w=NextVex(G,v))//从每一行第一个邻接矩阵值为1的,跳转到下一个值为1的
{
if(!visited[w])
DFS(G,v);
}
}
int FirstVex(Graph G,int v)//判断第一个不是0的
{
int i;
for(i=0;i<G.vexnum;i++)
{
if(G.arcs[v][i]==1&&visited[i]==False)
return i;
}
return -1;
}
void NextVex(Graph G,int v)//判断下一个不是0的
{
int i;
for(i=w;i<G.vexnum;i++)
{
if(G.arcs[v][i]==1&&visited[i]!=False)
return i;
}
return -1;
}
2.邻接表
void DFS(Graph G,int v)
{
cout<<G.vertices[v].data<<" ";
visited[v]=true;
ArcNode *p=G.vertices[v].firstarc;
while(p!=NULL)
{
int w=p->adjvex;
if(!visited[w])
DFS(G,w);
p=p->nextarc;a
}
}
(3)时间/空间复杂度
- 空间复杂度:邻接矩阵,最坏情况O(|V|),最好情况O(1),栈深为1,所有的结点都连着遍历初始的那个结点。
- 时间复杂度:
T(V)=o(|V|2) 邻接矩阵
T(V)=O(|E|+n) 邻接表
6.8 图的应用
一、最小生成树
1、最小生成树的概念
对于一个带权连通无向图G = (V,E),生成树,每棵树的权(即树中所有边上的权值之和)也可能不同。设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(Minimum-Spannino-Tree,MST).。
- 最小生成树可能有多个,但边的权值之和总是唯一且最小的。
- 最小生成树的边数 =顶点数 -1。砍掉一条则不连通,增加一条边则会出现回路。
- 如果一个连通图本身就是一棵树,则其最小生成树就是它本身。
- 只有连通图才有生成树,非连通图只有生成森林。
2、求最小生成树的两种方法
- Prim算法
- Kruskal算法
- Prim算法(普里姆):
从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
时间复杂度: O(V2)
适合用于边稠密图
- Kruskal算法(克鲁斯卡尔):并查集
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选)直到所有结点都连通。
时间复杂度: O(l E l * log2l E l )
适合用于边稀疏图
二、最短路径
1.无权图的单源最短路径问题——BFS算法
使用 BFS算法求无权图的最短路径问题,需要使用三个数组
这是寻找结点2的最短路径的数组情况
- d[]数组用于记录顶点 u 到其他顶点的最短路径。
- path[]数组用于记录最短路径从那个顶点过来。
- visited[]数组用于记录是否被访问过。
#define MAX_LENGTH 2147483647 //地图中最大距离,表示正无穷
// 求顶点u到其他顶点的最短路径
void BFS_MIN_Disrance(Graph G,int u){
for(i=0; i<G.vexnum; i++){
visited[i]=FALSE; //初始化访问标记数组
d[i]=MAX_LENGTH; //初始化路径长度
path[i]=-1; //初始化最短路径记录
}
InitQueue(Q); //初始化辅助队列
d[u]=0;
visites[u]=TREE;
EnQueue(Q,u);
while(!isEmpty[Q]){ //BFS算法主过程
DeQueue(Q,u); //队头元素出队并赋给u
for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w)){
if(!visited[w]){
d[w]=d[u]+1;
path[w]=u;
visited[w]=TREE;
EnQueue(Q,w); //顶点w入队
}
}
}
}
2.单源最短路径问题——Dijkstra算法
BFS算法的局限性:BFS算法求单源最短路径只适⽤于⽆权图,或所有边的权值都相同的图。
Dijkstra算法能够很好的处理带权图的单源最短路径问题,但不适⽤于有负权值的带权图
使用 Dijkstra算法求最短路径问题,需要使用三个数组:
- final[]数组用于标记各顶点是否已找到最短路径。
- dist[]数组用于记录各顶点到源顶点的最短路径长度。
- path[]数组用于记录各顶点现在最短路径上的前驱。
#define MAX_LENGTH = 2147483647;
// 求顶点u到其他顶点的最短路径
void BFS_MIN_Disrance(Graph G,int u){
for(int i=0; i<G.vexnum; i++){ //初始化数组
final[i]=FALSE;
dist[i]=G.edge[u][i];
if(G.edge[u][i]==MAX_LENGTH || G.edge[u][i] == 0)
path[i]=-1;
else
path[i]=u;
final[u]=TREE;
}
for(int i=0; i<G.vexnum; i++){
int MIN=MAX_LENGTH;
int v;
// 循环遍历所有结点,找到还没确定最短路径,且dist最⼩的顶点v
for(int j=0; j<G.vexnum; j++){
if(final[j]!=TREE && dist[j]<MIN){
MIN = dist[j];
v = j;
}
}
final[v]=TREE;
// 检查所有邻接⾃v的顶点路径长度是否最短
for(int j=0; j<G.vexnum; j++){
if(final[j]!=TREE && dist[j]>dist[v]+G.edge[v][j]){
dist[j] = dist[v]+G.edge[v][j];
path[j] = v;
}
}
}
}
3.各顶点间的最短路径问题——Floyd算法
Floyd算法:求出每⼀对顶点之间的最短路径,使⽤动态规划思想,将问题的求解分为多个阶段。
Floyd算法可以⽤于负权值带权图,但是不能解决带有“负权回路”的图(有负权值的边组成回路),这种图有可能没有最短路径。
Floyd算法使用到两个矩阵:
- dist[][]:目前各顶点间的最短路径。
- path[][]:两个顶点之间的中转点。
int dist[MaxVertexNum][MaxVertexNum];
int path[MaxVertexNum][MaxVertexNum];
void Floyd(MGraph G){
int i,j,k;
// 初始化部分
for(i=0;i<G.vexnum;i++){
for(j=0;j<G.vexnum;j++){
dist[i][j]=G.Edge[i][j];
path[i][j]=-1;
}
}
// 算法核心部分
for(k=0;k<G.vexnum;k++){
for(i=0;i<G.vexnum;i++){
for(j=0;j<G.vexnum;j++){
if(dist[i][j]>dist[i][k]+dist[k][j]){
dist[i][j]=dist[i][k]+dist[k][j];
path[i][j]=k;
}
}
}
}
}
4.最短路径算法比较
三、有向⽆环图描述表达式
1.有向⽆环图
若⼀个有向图中不存在环,则称为有向⽆环图,简称 DAG图(Directed Acyclic Graph)
四、拓扑排序
1. AOV网(Activity on Vertex Network,用顶点表示活动的网)
用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行。
2. 拓扑排序
在图论中,由⼀个有向⽆环图的顶点组成的序列,当且仅当满⾜下列条件时,称为该图的⼀个拓扑排序:
- 每个顶点出现且只出现⼀次;
- 若顶点 A 在序列中排在顶点 B 的前⾯,则在图中不存在从顶点 B 到顶点 A 的路径。存在则有环
- 或定义为:拓扑排序是对有向⽆环图的顶点的⼀种排序,它使得若存在⼀条从顶点 A 到顶点 B 的路径,则在排序中顶点 B 出现在顶点 A 的后⾯。每个 AOV ⽹都有⼀个或多个拓扑排序序列。
3. 拓扑排序的实现:
- 从AoV网中选择一个没有前驱 (入度为0) 的顶点并输出
- 从网中删除该顶点和所有以它为起点的有向边。
- 重复D和2直到当前的AOV网为空或当前网中不存在无前驱的顶点为止
保存度为零的顶点可用队列,栈,数组
4. 代码实现拓扑排序(邻接表实现):
#define MaxVertexNum 100 //图中顶点数目最大值
typedef struct ArcNode{ //边表结点
int adjvex; //该弧所指向的顶点位置
struct ArcNode *nextarc; //指向下一条弧的指针
}ArcNode;
typedef struct VNode{ //顶点表结点
VertexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct{
AdjList vertices; //邻接表
int vexnum,arcnum; //图的顶点数和弧数
}Graph; //Graph是以邻接表存储的图类型
// 对图G进行拓扑排序
bool TopologicalSort(Graph G){
InitStack(S); //初始化栈,存储入度为0的顶点
for(int i=0;i<g.vexnum;i++){
if(indegree[i]==0)
Push(S,i); //将所有入度为0的顶点进栈
}
int count=0; //计数,记录当前已经输出的顶点数
while(!IsEmpty(S)){ //栈不空,则存入
Pop(S,i); //栈顶元素出栈
print[count++]=i; //输出顶点i
for(p=G.vertices[i].firstarc;p;p=p=->nextarc){
//将所有i指向的顶点的入度减1,并将入度为0的顶点压入栈
v=p->adjvex;
if(!(--indegree[v]))
Push(S,v); //入度为0,则入栈
}
}
if(count<G.vexnum)
return false; //排序失败
else
return true; //排序成功
}
五、 逆拓扑排序
实现方法一:将上述代码改为出度为零
DFS 逆拓扑排序
思考: 如何判断回路
六、关键路径
1. AOE 网
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如 完成活动所需的时间),称之为⽤边表示活动的⽹络,简称 AOE ⽹ (Activity On Edge NetWork)。
2. 源点 汇点
在 AOE ⽹中仅有⼀个⼊度为 0 的顶点,称为开始顶点(源点),它表示整个⼯程的开始;== 也仅有⼀个出度为 0 的顶点,称为结束顶点(汇点)==,它表示整个⼯程的结束。
- 从源点到汇点的有向路径可能有多条,所有路径中,具有最⼤路径⻓度的路径称为关键路径,⽽把关键路径上的活动称为关键活动。
- 完成整个⼯程的最短时间就是关键路径的⻓度,若关键活动不能按时完成,则整个 ⼯程的完成时间就会延⻓。
3. 求关键路径
4. 特性