文章目录
注:内容参考王道2024考研复习指导以及《数据结构》
图的基本概念
图的定义
图G由顶点集V和边集E组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系(边)集合。若 V = { v 1 , v 2 , . . . , v n } V= \{ v_1,v_2,...,v_n \} V={v1,v2,...,vn},则用|V|表示图G中顶点的个数,也称图G的阶, E = { ( u , v ) ∣ u ∈ V , v ∈ V } E=\{ (u,v) | u \in V,v \in V \} 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|=n,则 ∣ E ∣ ∈ [ 0 , C n 2 ] = [ 0 , n ( n − 1 ) / 2 ] |E| \in [0,C^2_n]=[0,n(n-1)/2] ∣E∣∈[0,Cn2]=[0,n(n−1)/2]
有向完全图
有向图中任意两个顶点之间都存在方向相反的两条弧。
若有向图的顶点数|V|=n,则 ∣ E ∣ ∈ [ 0 , C n 2 ] = [ 0 , n ( n − 1 ) ] |E| \in [0,C^2_n]=[0,n(n-1)] ∣E∣∈[0,Cn2]=[0,n(n−1)]
子图
设有两个图G = (V, E)和G‘ = (V’, E‘),若V’是V的子集,且E‘是E的子集,则称G’是G的子图。
若有满足V(G‘) = V(G)的子图G’,则称其为G的生成子图。
连通图、连通分量
在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。
常见考点:对于n个顶点的无向图G,若G是连通图,则最少有 n-1 条边;若G是非连通图,则最多可能有 C n − 1 2 C^2_{n-1} Cn−12条边
无向图中的极大连通子图称为连通分量。
强连通图、强连通分量
在有向图中,若有一对顶点v和w,从v到w和从w到v之间都有路径,则称这两个顶点是强连通的。若图中任意一对顶点都是强连通的,则称此图为强连通图。
常见考点:对于n个顶点的有向图G,若G是强连通图,则最少有 n 条边(形成回路)
有向图中的极大强连通子图称为有向图的强连通分量。
生成树、生成森林
连通图的生成树是包含图中全部顶点的一个极小连通子图。若图中顶点数为n,则它的生成树含有 n-1 条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。
在非连通图中,连通分量的生成树构成了非连通图的生成森林。
顶点的度、入度、出度
- 对于无向图
顶点v的度是指依附于该顶点的边的条数,记为TD(v)。
在具有n个顶点、e条边的无向图中, ∑ i = 1 n T D ( v i ) = 2 e \sum ^n _{i=1} TD(v_i)=2e ∑i=1nTD(vi)=2e
即无向图的全部顶点的度的和等于边数的2倍。
- 对于有向图
入度是以顶点v为终点的有向边的数目,记为ID(v);
出度是以顶点v为起点的有向边的数目,记为OD(v)。
顶点v的度等于其入度和出度之和,即TD(v) = ID(v) + OD(v)。
在具有n个顶点、e条边的有向图中, ∑ i = 1 n I D ( v i ) = ∑ i = 1 n O D ( v i ) = e \sum ^n _{i=1}ID(v_i)= \sum ^n _{i=1} OD(v_i)=e ∑i=1nID(vi)=∑i=1nOD(vi)=e
边的权、带权图/网
边的权,在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。
带权图/网,边上带有权值的图称为带权图,也称网。
带权路径长度,当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度。
稀疏图、稠密图
顶点-顶点的关系描述
- 路径——顶点vp到顶点vq之间的一条路径是指顶点序列。
- 回路——第一个顶点和最后一个顶点相同的路径称为回路或环。
- 简单路径——在路径序列中,顶点不重复出现的路径称为简单路径。
- 简单回路——除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
- 路径长度——路径上边的数目。
- 点到点的距离——从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。若从u到v根本不存在路径,则记该距离为无穷(∞)。
树、森林、有向树
树,不存在回路,且连通的无向图。
有向树,一个顶点的入度为0、其余顶点的入度均为1的有向图,称为有向树。
n个顶点的树,必有n-1条边。
注:n个顶点的图,若 |E|>n-1,则一定有回路。
图的存储及基本操作
邻接矩阵法
#define MaxVertexNum 100
typedef struct {
char Vex[MaxVertexNum];//顶点表
int Edge[MaxVertexNum][MaxVertexNum];//邻接矩阵、边表
int vexnum,arcnum;//图当前的顶点数和边数/弧数
}MGraph;
结点数为n的图G = (V, E)的邻接矩阵A是n´n的。将G的顶点编号为v1, v2,…, vn ,则
邻接矩阵存储带权图
#define MaxVertexNum100//顶点数目的最大值
#define INFINITY 最大的int值//宏定义常量”无穷”
typedef char VertexType;//顶点的数据类型
typedef int EdgeType;//带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum];//顶点
EdgeType Edge[MaxVertexNum][MaxVertexNum];//边的权
int vexnum,arcnum;//图的当前顶点数和弧数
}MGraph;
邻接矩阵法的性能分析
空间复杂度: O ( ∣ V ∣ 2 ) O({|V|}^2) O(∣V∣2) ——只和顶点数相关,和实际的边数无关。
适合用于存储稠密图。
邻接矩阵法的性质
- 无向图的邻接矩阵是对称矩阵,可以压缩存储(只存储上三角区/下三角区)。
- 对于无向图,第i个结点的度 = 第i行(或第i列)的非零元素个数。
- 对于有向图,第i个结点的出度 = 第i行的非零元素个数;第i个结点的入度 = 第i列的非零元素个数;第i个结点的度 = 第i行、第i列的非零元素个数之和。
- 邻接矩阵法求顶点的度/出度/入度的时间复杂度为 O(|V|)。
- 设图G的邻接矩阵为A矩阵元素为0/1,则 A n A^n An的元素 A n [ i ] [ j ] A^n[i][j] An[i][j]等于由顶点i到顶点j的长度为n的路径的数目。
邻接表法
//边
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;
注:只要确定了顶点编号,图的邻接矩阵表示方式唯一;图的邻接表表示方式并不唯一。
对比:
十字链表
注:只用于存储有向图的链式存储;图的十字链表不是唯一的,但一个十字链表唯一确定一个图。
邻接矩阵、邻接表存储有向图
十字链表存储有向图
弧结点
顶点结点
十字链表法性能分析
空间复杂度:O(|V|+|E|)
顺着绿色线路找,找到指定顶点的所有出边;顺着橙色线路找,找到指定顶点的所有入边
邻接多重表
注:只用于存储无向图的链式存储。
邻接矩阵、邻接表存储无向图
邻接表每条边对应两份冗余信息,删除顶点、删除边等操作时间复杂度高。
邻接矩阵空间复杂度高。
邻接多重表存储无向图
边结点
顶点结点
邻接多重表性能分析
空间复杂度:O(|V|+|E|),每条边只对应一份数据
删除边、删除节点等操作很方便
四种存储方式的总结
图的基本操作
基于邻接矩阵和邻接表进行基本操作的复杂性讨论。
- Adjacent(G,x,y):判断图G是否存在边<x, y>或(x, y)。
- Neighbors(G,x):列出图G中与结点x邻接的边。
- InsertVertex(G,x):在图G中插入顶点x。
- DeleteVertex(G,x):从图G中删除顶点x。
- AddEdge(G,x,y):若无向边(x, y)或有向边<x, y>不存在,则向图G中添加该边。
- RemoveEdge(G,x,y):若无向边(x, y)或有向边<x, y>存在,则从图G中删除该边。
- FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点
或图中不存在x,则返回-1。 - NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1。
- Get_edge_value(G,x,y):获取图G中边(x, y)或<x, y>对应的权值。
- Set_edge_value(G,x,y,v):设置图G中边(x, y)或<x, y>对应的权值为v。
图的遍历
广度优先遍历(BFS)
图的广度优先遍历
- 找到与⼀个顶点相邻的所有顶点
- 标记哪些顶点被访问过
- 需要⼀个辅助队列
FirstNeighbor(G,x):求图G中顶点x的第⼀个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1。
NextNeighbor(G,x,y):假设图G中顶点y是顶点x的⼀个邻接点,返回除y之外顶点x的下⼀个邻接点的顶点号,若y是x的最后⼀个邻接点,则返回-1。
代码实现
bool visited[MAX_VERTEX_NUM];//访问标记数组,初始都为false
//广度优先遍历
void BFS(Graph G,int v){
visit(V);
visited[v]=true;
EnQueue(Q,v);
while(!isEmpty(Q)){
DeQueue(Q,v);
for(w=FirtstNeighbor(G,v);w>=0;W=NextNeighbor(G,v,w)){
if(!visited[w]){
visit(w);
visited[w]=true;
EnQueue(Q,w);
}
}
}
}
注:同⼀个图的邻接矩阵表示方式唯⼀,因此广度优先遍历序列唯⼀;同⼀个图邻接表表示方式不唯⼀,因此⼴度优先遍历序列不唯⼀。
BFS算法(Final版)
算法存在的问题:如果是非连通图,则无法遍历完所有结点
bool visited[MAX_VERTEX_NUM];//访问标记数组,初始都为false
void 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);
visited[v]=true;
EnQueue(Q,v);
while(!isEmpty(Q)){
DeQueue(Q,v);
for(w=FirtstNeighbor(G,v);w>=0;W=NextNeighbor(G,v,w)){
if(!visited[w]){
visit(w);
visited[w]=true;
EnQueue(Q,w);
}
}
}
}
对于无向图,调用BFS函数的次数=连通分量数。
复杂性分析
空间复杂度:最坏情况,辅助队列大小为 O ( ∣ V ∣ ) O(|V|) O(∣V∣)
邻接矩阵存储的图:访问 |V| 个顶点需要O(|V|)的时间,查找每个顶点的邻接点都需要O(|V|)的时间,⽽总共有|V|个顶点,时间复杂度= O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
邻接表存储的图:访问 |V| 个顶点需要O(|V|)的时间,查找各个顶点的邻接点共需要O(|E|)的时间,时间复杂度= O(|V|+|E|)
广度优先生成树
广度优先生成树由广度优先遍历过程确定。
由于邻接表的表示方式不唯一,因此基于邻接表的广度优先生成树也不唯一。同一个图的邻接矩阵存储表示是唯一的,所以其广度优先生成树也是唯一的。
对于非连通图的广度优先遍历,可得到广度优先生成森林。
深度优先遍历(DFS)
图的深度优先遍历
基本思想:首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任意一个顶点w1,再访问与w1邻接且未被访问的任意一个顶点w2重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。
代码实现
bool visited[MAX_VERTEX_NUM];
void DFS(Graph G,int v){
visit(v);
visited[v]=true;
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
if(!visited[w]){
DFS(G,w);
}
}
}
DFS算法(Final版)
如果是非连通图,则无法遍历完所有结点
bool visited[MAX_VERTEX_NUM];
void DFSTraverse(Graph G){
for(v=0;v<G.vexnum;++v){
visited[v]=false;
}
for(v=0;v<G.vexnum;++v){
if(!visited[v]){
DFS(G,v);
}
}
}
void DFS(Graph G,int v){
visit(v);
visited[v]=true;
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
if(!visited[w]){
DFS(G,w);
}
}
}
复杂度分析
空间复杂度:来自函数调用栈,最坏情况,递归深度为 O ( ∣ V ∣ ) O(|V|) O(∣V∣),最好情况O(1)。
时间复杂度=访问各结点所需时间+探索各边所需时间
邻接矩阵存储的图:访问 |V| 个顶点需要O(|V|)的时间,查找每个顶点的邻接点都需要O(|V|)的时间,⽽总共有|V|个顶点,时间复杂度= O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
邻接表存储的图:访问 |V| 个顶点需要O(|V|)的时间,查找各个顶点的邻接点共需要O(|E|)的时间,时间复杂度= O(|V|+|E|)。
深度优先遍历序列
同⼀个图的邻接矩阵表示方式唯⼀,因此深度优先遍历序列唯⼀;同⼀个图邻接表表示方式不唯⼀,因此深度优先遍历序列不唯⼀。
深度优先生成树
同⼀个图的邻接矩阵表示方式唯⼀,因此深度优先遍历序列唯⼀,深度优先⽣成树也唯⼀。
同⼀个图邻接表表示方式不唯⼀,因此深度优先遍历序列不唯⼀,深度优先⽣成树也不唯⼀。
图的遍历与图的连通性
对无向图进⾏BFS/DFS遍历,调用BFS/DFS函数的次数=连通分量数;对于连通图,只需调用1次 BFS/DFS。
对有向图进⾏BFS/DFS遍历,调用BFS/DFS函数的次数要具体问题具体分析,若起始顶点到其他各顶点都有路径,则只需调用1次BFS/DFS 函数。
对于强连通图,从任⼀结点出发都只需调用1次 BFS/DFS。
图的应用
最小生成树
生成树
连通图的生成树是包含图中全部顶点的一个极小连通子图。
若图中顶点数为n,则它的生成树含有n-1条边。
对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。
广度优先生成树
深度优先生成树
最小生成树
对于一个带权连通无向图 G = ( V , E ) G=(V,E) G=(V,E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(Minimum-Spanning-Tree, MST)。
性质:
- 最小生成树可能有多个,但边的权值之和总是唯一且最小的
- 最小生成树的边数=顶点数-1。砍掉一条则不连通,增加一条边则会出现回路
- 如果连通图本身就是一棵树,则其最小生成树就是它本身
- 只有连通图才有生成树,非连通图只有生成森林
注:最小生成树中所有边的权值之和最小,但不能保证任意两个顶点之间的路径是最短路径。
Prim 算法(普里姆)
从某⼀个顶点开始构建⽣成树,每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
Prim 算法实现思想
由点构树。
初始:从 V 0 V_0 V0开始;
第1轮:循环遍历所有结点,找到lowCost最低且还没有加入树的顶点;将其加入树后,再次循环遍历,更新还没加入的各个顶点的lowCost值;
每轮重复遍历,直到所有结点都加入树。
时间复杂度:每一轮时间复杂度 O ( 2 n ) O(2n) O(2n),总时间复杂度 O ( n 2 ) O(n^2) O(n2),即 O ( ∣ V 2 ∣ ) O(|V^2|) O(∣V2∣)
Kruskal 算法(克鲁斯卡尔)
每次选择⼀条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直到所有结点都连通。
Kruskal算法的实现思想
由边构树。
初始:将各条边按权值排序;
第1轮,检查第1条边的两个顶点是否连通(使用并查集检查是否属于同一个集合),如果不连通则连起来,如果连通则跳过;
遍历完所有的边,得到最小生成树。
时间复杂度:共执行e轮,每轮需要判断两个顶点是否属于同一集合需要 O ( α ( ∣ V ∣ ) ) O(\alpha(|V|)) O(α(∣V∣)),可以视为常熟,每次选取权值最小的边需要 O ( l o g 2 e ) O(log_2e) O(log2e),总时间复杂度 O ( e l o g 2 e ) O(e{log_2e}) O(elog2e)
Prim 算法 v.s. Kruskal 算法
普里姆算法时间复杂度为 O ( ∣ V 2 ∣ ) O(|V^2|) O(∣V2∣),适合用于边稠密图。
克鲁斯卡尔算法时间复杂度为 O ( ∣ E ∣ l o g 2 ∣ E ∣ ) O(|E|{log_2|E|}) O(∣E∣log2∣E∣),适合用于边稀疏图。
最短路径问题
注:也可用 Dijkstra 算法求所有顶点间的最短路径,重复 |V| 次即可,总的时间复杂度也是O(|V|^3)
BFS求无权图的单源最短路径
代码实现
bool visited[MAX_VERTEX_NUM];//标记访问数组,初始均为false
//广度优先遍历
void BFS(Graph G,int v){
visit(v);
visited[v]=true;
EnQueue(Q,v);
while(isEmpty(Q)){
DeQueue(Q,v);
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
if(!visited[w]){
visit(w);
visited[w]=true;
EnQueue(Q,w);
}
}
}
}
/*
就是对BFS的小修改,在visit⼀个顶点时,
修改其最短路径长度 d[ ] 并在 path[ ] 记录前驱结点
*/
//求顶点u到其他顶点的最短路径
void BFS_MIN_Distance(Graph G,int u){
//d[i]表示u到i结点的最短路径
for(int i=0;i<G.vexnum;i++){
d[i]=INT_MAX;
path[i]=-1;
}
d[u]=0;
visited[u]=true;
EnQueue(Q,u);
while(!isEmpty(Q)){
DeQueue(Q,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]=true;
EnQueue(Q,w);
}
}
}
}
Dijkstra算法
BFS算法的局限性:BFS算法求单源最短路径只适用于无权图,或者所有边权值都相同的图。
注:Dijkstra算法不适用带负权值的图。
辅助数组作用:
- final[]:标记各顶点是否已找到最短路径。
- dist[]:记录从源点 v 0 v_0 v0到其他各顶点当前的最短路径长度,它的初始值为:若从 v − 0 v-0 v−0到 v i v_i vi,有弧,则dist[i]为弧上的权值;否则置dist[i]为∞。
- path[]:path[i]表示从源点到顶点i之间的最短路径的前驱结点。在算法结束时,可根据其值追溯得到源点v到顶点v的最短路径。
初始:从 V 0 V_0 V0开始,初始化三个数组信息如上。
第1轮:循环遍历所有的结点,找到还没确定最短路径,且dist最小的顶点 V i V_i Vi,令final[i]=true。检查所有邻接⾃ Vi 的顶点,若其 final 值为false,则更新 dist 和 path 信息。
重复该过程,知道final数组值全部为true。
如何使用数组信息
V0到V2 的最短(带权)路径长度为:dist[2] = 9
通过 path[ ] 可知,V0到V2 的最短(带权)路径:V2 <—— V1 <—— V4 <—— V0
时间复杂度
初始:若从V0开始,令 final[0]=ture; dist[0]=0; path[0]=-1。其余顶点final[k]=false; dist[k]=arcs[0,k]; path[k]= (arcs[0,k]==∞) ? -1 : 0
n-1轮处理:循环遍历所有顶点,找到还没确定最短路径,且dist 最小的顶点Vi,令final[i]=ture。并检查所有邻接⾃Vi 的顶点,对于邻接⾃Vi 的顶点 Vj ,若 final[j]==false 且 dist[i]+arcs[i,j] < dist[j],则令 dist[j]=dist[i]+arcs[i,j]; path[j]=i。(注:arcs[i,j]表示Vi 到Vj 的弧的权值)。
时间复杂度: O ( n 2 ) O(n^2) O(n2),即 O ( ∣ V 2 ∣ ) O(|V^2|) O(∣V2∣)
Floyd算法
以每个点为「中转站」,刷新所有「入度」和「出度」的距离。
Floyd 算法不能解决带有“负权回路”的图(有负权值的边组成回路),这种图有可能没有最短路径。
//准备工作,根据图的信息初始化矩阵A和Path
for(int k=0;k<n;k++){//考虑以Vk作为中转点
for(int i=0;i<n;i++){//遍历整个矩阵,1为行号,j为列号
for(int j=0;j<n;j++){
if(A[i][j]>A[i][k]+A[k][j]){//以Vk为中转点的路径更短
A[i][j]=A[i][k]+A[k][j];//更新最短路径长度
path[i][j]=k;//中转点
}
}
}
}
/*
时间复杂度,O(|V|^3)
空间复杂度,O(|V|^2)
*/
有向无环图——描述表达式
有向无环图:若一个有向图中不存在环,则称为有向无环图,简称DAG图
有向无环图是描述含有公共子式的表达式的有效工具,例如下图表达式:
解题方法
第一步:把各个操作数不重复的排成一排
第二步:标出各个运算符生效的顺序(先后顺序可能有所出入)
第三步:按顺序加入运算符,注意“分层”,例如乘法运算符需要依靠下层的加法的结果。
第四步:从底向上逐层检查同层的运算符是否可以合体
拓扑排序
AOV网(Activity On Vertex NetWork,用顶点表示活动的网):
用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边 < V i , V j > <V_i,V_j> <Vi,Vj>表示活动Vi必须先于Vj进行
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
- 每个顶点出现且只出现一次。
- 若顶点A在序列中排在顶点B的前面,则在图中不存在从顶点B到顶点A的路径。
或定义为:拓扑排序是对有向无环图的顶点的⼀种排序,它使得若存在⼀条从顶点A到顶点B的路径,则在排序中顶点B出现在顶点A的面。每个AOV网都有⼀个或多个拓扑排序序列。
拓扑排序的实现
- 从AOV网中选择一个没有前驱的顶点并输出。
- 从网中删除该顶点和所有以它为起点的边。
- 重复1和2知道当前AOV网为空或当前网中不存在无前驱的顶点为止(说明有回路)。
代码实现
#define MaxVertexNum 100 //图中顶点数目的最大值
typedef struct ArcNode{//边表结点
int adjvex;
struct ArcNode *nextarc;
//InfoType info;//网的边权值
}ArcNode;
typedef struct VNode{
VertexType data;
ArcNode *firstarc;
}VNode,AdjList[MaxVertexNum];
typedef struct{
AdjList vertices;
int vexnum,arcnum;
}Graph;
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)){//栈不空,则存在入度为0的顶点
Pop(S,i) ;//栈顶元素出栈
print[count++]=i;//输出顶点
for(p=G.vertices[i].firstarc;p;p=p->nextarc){//将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈s
v=p->adjvex;
if (!(--indegree[v])){
Push(S,v) ;//入度为0,则入栈
}
}
}//while
if(count<G.vexnum){
false;//排序失败,有向图中有回路
}
else{
return true;//拓扑排序成功
}
}
时间复杂度分析
采用邻接表时,每个顶点都需要处理一次,每条边都需要处理一次,则时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)
若采用邻接矩阵,则需要 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
逆拓扑排序
对⼀个AOV网,如果采用下列步骤进行排序,则称之为逆拓扑排序:
- 从AOV网中选择⼀个没有后继(出度为0)的顶点并输出。
- 从网中删除该顶点和所有以它为终点的有向边。
- 重复1和2直到当前的AOV网为空。
逆拓扑排序的实现
void DFSTraverse(Graph G){
for(int v=0;v<G.vexnum;v++){
visited[v]=false;
}
for(int v=0;v<G.vexnum;v++){
if(!visited[v]){
DFS(G,v);
}
}
}
void DFS(Graph G,int v){//从顶点v出发,深度优先
visited[v]=true;//设已访问标记
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighor(G,v,w){
if(!visited[w]){
DFS(G,w);
}
}
print(v);
}
关键路径
AOE网,在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如 完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edge NetWork)
AOE网具有以下两个性质:
- 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
- 只有进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。
另外,有些活动可以并行进行。
在AOE网中仅有⼀个⼊度为0的顶点,称为开始顶点(源点),它表示整个工程的开始;也仅有⼀个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。
关键路径
从源点到汇点的有向路径可能有多条,所有路径中,具有最⼤路径长度的路径称为关键路径,⽽把关键路径上的活动称为关键活动。
完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个工程的完成时间就会延长。
寻找关键路径的一些参量定义:
- 事件的 v k v_k vk最早发生时间 v e ( k ) v_e(k) ve(k)一一决定了所有从 v k v_k vk开始的活动能够开工的最早时间
按拓扑排序序列,依次求各个顶点的ve(k):
ve(源点)=0
ve(k)= M a x { v e ( j ) + W e i g h t ( v j , v k ) } Max\{ ve(j)+Weight(v_j,v_k) \} Max{ve(j)+Weight(vj,vk)},vj为vk的任意前驱
- 事件vk的最迟发生时间vl(k)一一它是指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间
按逆拓扑排序序列,依次求各个顶点的vl(k):
vl(汇点)=ve(汇点)
ve(k)= M i n { v l ( j ) − W e i g h t ( v k , v j ) } Min\{ vl(j)-Weight(v_k,v_j) \} Min{vl(j)−Weight(vk,vj)},vj为vk的任意后继
- 活动 a i a_i ai的最早开始时间 e ( i ) e(i) e(i)一一指该活动弧的起点所表示的事件的最早发生时间
若边<vk, vj>表⽰活动aj,则有e(i) = ve(k)
- 活动ai的最迟开始时间l(i)一一它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差
若边<vk, vj>表⽰活动ai,则有l(i) = vl(j) - Weight(vk, vj)
- 活动ai的时间余量d(i)=l(i)-e(i),表⽰在不增加完成整个工程所需总时间的情况下,活动ai可以拖延的时间
d(i) = l(i) - e(i)
求得关键活动、关键路径
若⼀个活动的时间余量为零,则说明该活动必须要如期完成,d(i)=0即l(i) = e(i)的活动ai是关键活动,由关键活动组成的路径就是关键路径。
关键活动、关键路径的特性
若关键活动耗时增加,则整个工程的工期将增长。
缩短关键活动的时间,可以缩短整个工程的工期。
当缩短到⼀定程度时,关键活动可能会变成非关键活动。
可能有多条关键路径,只提高⼀条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的⽬的。