C语言数据结构与算法---拓扑排序、关键路径

一. 有向无环图

有向无环图:无环的有向图,简称 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个描述量:

  1. ve(vj):表示事件 vj 的最早发生时间
    如上图:ve(v1) = 0 ve(v2) = 30
    源点的最早发生时间为 0
  2. vl(vj):表示事件 vj 的最晚发生时间
    假如整个工程最晚结束时间为180分钟,则 a7 的最晚发生时间为 180 -15 = 165 分钟
  3. e(i):表示活动 ai 的最早开始时间
    如上图:e(a3) = 30
  4. 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) 呢?

  1. ve(1) = 0 开始向前递推
    ve(j) = Max { ve(i) + Wi,j },< i,j > ∈ T,2 <= j<= n
    其中 T 是所有以 j 为头的弧的集合
    在这里插入图片描述
  2. vl(n) = ve(n) 开始往后递推
    vl(i) = Min { vl(j) - Wi,j } , <i,j> ∈ S,1 <= i <= n - 1
    其中 S 是所有以 i 为尾的弧的集合
    在这里插入图片描述
    求关键路径的步骤:
    1. 求 ve(i)、vl(j) ,事件的最早、最晚发生时间
    2. 求 e(i)、l(i) , 活动的最早、最晚发生时间
    3. 计算 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. 关键路径的算法实现

步骤:

  1. 调用拓扑排序算法,将拓扑序列保存在数组 topo 中

  2. 将每个事件的最早发生时间 ve(i) 初始化为0

  3. 根据 topo 中的值,按照从前向后的拓扑次序,依次求每个事件的最早发生时间,循环几次,执行以下操作:
    ① 取得拓扑序列中顶点序号 k,k = topo[i]
    ② 用指针 p 依次指向 k 的每个邻接顶点,取得每个邻接顶点的序号 j = p->adjvex,依次更新顶点 j 的最早发生时间

  4. 将每个事件的最迟发生时间初始化为汇点的最早发生时间

  5. 根据 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;
		}
	}
}
发布了40 篇原创文章 · 获赞 32 · 访问量 5454

猜你喜欢

转载自blog.csdn.net/myjess/article/details/104629610
今日推荐