一. 有向无环图
有向无环图:无环的有向图,简称 DAG 图
有向无环图常用来描述一个工程或系统的进行过程。(通常把计划、施工、生产、程序流程等当成是一个工程)
一个工程可以分为若干个子工程(活动),只要完成了这些子工程,就可以导致整个工程的完成。
AOV 网: 用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称 AOV 网--------------拓扑排序
AOE 网: 用一个有向图表示一个工程的各子工程及其相互制约的关系,以弧表示活动,以顶点表示活动的开始或结束事件,程这种有向图为边表示活动的网,简称为 AOE 网--------------关键路径
二. 拓扑排序
1. 分析
AOV 网的特点:
- 若从 i 到 j 有一条有向路径,则 i 是 j 的前驱,j 是 i 的后继
如:C1是C5的前驱,C5是C1的后继 - 若 <i,j> 是网中有向边,则 i 是 j 的直接前驱;j 是 i 的直接后继
如:C1是C3的直接前驱,C3是C1的直接后继 - AOV 网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然不对
如:C1学完学C2,C2学完学C3,C3学完学C1,完全是荒谬的
2. 拓扑排序的定义及方法
在 AOV 网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若 AOV 网中有弧 <i,j> 存在,则在这个序列中,i 一定排在 j 的前面,具有这种性质的线性排序称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序
实现图例:
- 在有向图中选一个没有前驱的顶点输出(C1)
- 从图中删除该顶点和所有以它为尾的弧
- 重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止
选择C2
选择C3
依次选择再C4,C5,C7,C9,C10,C11,C6,C12,C8
该选择的序列就是一个拓扑序列,该序列不唯一
3. 拓扑排序的重要应用
检测 AOV 网中是否存在环:
对有向图构造器顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该 AOV 网必定不存在环
成环的顶点不存在前驱
4. 拓扑排序的算法实现
在求最小生成树和最短路径是我们使用的是邻接矩阵,但是由于拓扑排序的过程中需要删除结点,用邻接表会更方便
结构:
//在原来的顶点表结点结构中,增加了一个入度域 in
typedef struct EdgeNode
{
int weight;
int adjvex;
struct EdgeNode* next;
}EdgeNode;
typedef struct VertexNode
{
int in; //顶点入度
int data;
EdgeNode* fistedge;
}VertexNod,AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numv;
int nume;
}GraphAdjList;
算法实现:
//使用栈储存入度为 0 的点,目的是为了避免每个查找时都要遍历顶点表查找有没有入度为0的点
int TopologicalSort(GraphAdjList *G,int *topo)
{
EdgeNode* p; //边表结点
int i, k, gettop;
int top = 0; //栈顶指针
int count = 0; //统计输出顶点的个数
int* stack; //栈
stack = (int*)malloc(G->numv * sizeof(int));
for (i = 0; i < G->numv; i++)
{
if (G->adjList[i].in == 0) //将入度为零的顶点入栈
{
stack[++top] = i;
}
}
//只要栈不为空就循环
while (top != 0)
{
gettop = stack[top--]; //出栈
topo[count] = i;
count++;
p = G->adjList[i].fistedge; //e 指向 Vi 的第一个邻接点
while (p != NULL)
{
k = p->adjvex; //Vk 为 Vi 的邻接点
--G->adjList[k].in; //Vk 入度-1
if (G->adjList[k].in == 0) //若减为 0 就入栈
{
stack[++top]= k;
}
p = p->next;
}
}
if (count < G->numv)
{
return false;
}
else
{
return true;
}
}
时间复杂度:O(n+e)
三. 关键路径
1.分析
拓扑排序主要是解决一个工程能否顺利进行的问题,但是有时我们还需要解决工程完成需要最短实践问题
例如:若想在某个具体日期完成装修工作,最迟应该多久开始
把工程计划表示为边表示活动的网络,即 AOE 网,用顶点表示事件,弧表示活动,弧的权表示活动持续的时间
事件表示在它之前的活动已经完成,在它之后的活动可以开始。
2. 什么是关键路径
源点: 入度为 0 的顶点,表示整个工程的开始
汇点: 初度为 0 的顶点,表示整个工程结束
关键路径:从源点到汇点路径长度最长的路径
路径长度:路径上各活动持续时间之和
完成整项工程至少需要的时间就是关键路径的长度;关键路径上的活动是影响工程进度的关键。
确定关键路径需要定义4个描述量:
- ve(vj):表示事件 vj 的最早发生时间
如上图:ve(v1) = 0 ve(v2) = 30
源点的最早发生时间为 0 - vl(vj):表示事件 vj 的最晚发生时间
假如整个工程最晚结束时间为180分钟,则 a7 的最晚发生时间为 180 -15 = 165 分钟 - e(i):表示活动 ai 的最早开始时间
如上图:e(a3) = 30 - l(i):表示活动 ai 的最迟开始时间
如上图:l(a3) = 120
l(i) - e(i) :表示完成活动 ai 的时间余量
如:l(3) - e(3) = 90
关键活动----(关键路径上的活动),时间余量为 0。
如何找 l(i) == e(i) 的关键活动?
设活动 ai 用弧 <j,k> 表示,其持续时间记为:Wj,k
则有:
① e(i) = ve(j)
② l(i) = vl(k) - Wj,k
如何计算 ve(j) 和 vl(k) 呢?
- 从 ve(1) = 0 开始向前递推
ve(j) = Max { ve(i) + Wi,j },< i,j > ∈ T,2 <= j<= n
其中 T 是所有以 j 为头的弧的集合
- 从 vl(n) = ve(n) 开始往后递推
vl(i) = Min { vl(j) - Wi,j } , <i,j> ∈ S,1 <= i <= n - 1
其中 S 是所有以 i 为尾的弧的集合
求关键路径的步骤:- 求 ve(i)、vl(j) ,事件的最早、最晚发生时间
- 求 e(i)、l(i) , 活动的最早、最晚发生时间
- 计算 l(i) - e(i)
图示:
1. 求 ve(i)、vl(j) ,事件的最早、最晚发生时间
ve(j) = Max { ve(i) + Wi,j }
vl(i) = Min { vl(j) - Wi,j }
2. 求 e(i)、l(i) , 活动的最早、最晚发生时间
e(i) = ve(j)
l(i) = vl(k) - Wj,k
3. 计算 l(i) - e(i)
找到了关键活动:
- 若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。
- 如果一个活动处于所有的关键路径上,那么提高这个活动的速度,就能缩短整个工程的完成时间。
- 处于关键路径上的活动时间不能缩短太多,否则会使原来的关键路径变成不是关键路径。这时必须重新找关键路径。
3. 关键路径的算法实现
步骤:
-
调用拓扑排序算法,将拓扑序列保存在数组 topo 中
-
将每个事件的最早发生时间 ve(i) 初始化为0
-
根据 topo 中的值,按照从前向后的拓扑次序,依次求每个事件的最早发生时间,循环几次,执行以下操作:
① 取得拓扑序列中顶点序号 k,k = topo[i]
② 用指针 p 依次指向 k 的每个邻接顶点,取得每个邻接顶点的序号 j = p->adjvex,依次更新顶点 j 的最早发生时间 -
将每个事件的最迟发生时间初始化为汇点的最早发生时间
-
根据 topo 中的值。按从后向前的顺序,依次求每个事件的最晚发生时间,循环几次,执行以下操作:
① 取得拓扑序列中顶点序号 k,k = topo[i]
②用指针 p 依次指向 k 的每个邻接顶点,取得每个邻接顶点的序号 j = p->adjvex,依次更新顶点 j 的最晚发生时间
//声明几个全局变量
int* topo; //存放拓扑序列的数组
int* ve; //事件最早发生时间
int* vl; //事件最晚发生时间
int e,l;
void Criticalpath(GraphAdjList* G)
{
EdgeNode* p;
int i, j, k;
if (!TopologicalSort(G,topo)) //调用函数失败,则存在有向环
{
return;
}
for (i = 0; i < G->numv; i++)
{
ve[i] = 0; //初始化最早发生时间为 0
}
/*----------------求每个事件最早发生时间-------------*/
for (i = 0; i < G->numv; i++)
{
k = topo[i]; //取得拓扑序列中顶点序号 k
p = G->adjList[k].fistedge;//p 依次指向 k 的第一个邻接顶点
while (p != NULL)
{
j = p->adjvex; //邻接顶点的序号
if (ve[j] < ve[k] + p->weight)
{
ve[j] = ve[k] + p->weight;
}
p = p->next; p 依次指向 k 的下一个邻接顶点
}
}
for (i = 0; i < G->numv; i++)
{
vl[i] = ve[G->numv - 1];
}
/*----------------求每个事件的最晚发生时间-------------*/
for (i = G->numv - 1; i >= 0; i++)
{
k = topo[i];
p = G->adjList[k].fistedge;
while (p != NULL)
{
j = p->adjvex;
if (vl[k] > vl[j] - p->weight)
{
vl[k] = vl[j] - p->weight;
}
p = p->next;
}
}
/*----------判断每一活动是否为关键活动----------*/
for (i = 0; i < G->numv; i++)
{
p = G->adjList[i].fistedge;
while (p != NULL)
{
j = p->adjvex;
e = ve[i]; //计算活动<vi,vj>最早开始时间
l = vl[j] - p->weight; //计算活动<vi,vj>最晚开始时间
if (e == l) //若为关键活动则输出
{
printf("<v%d,v%d> length:%d",G->adjList[i].data,G->adjList[j].data,p->weight);
}
p = p->next;
}
}
}