思考在一些项目和工程中,完成一些任务之前需具备一些先决条件;或者是已经完成了某些活动才能开始另一项活动。引入数据结构中的一种特殊图来表示
一个无环的有向图被称为有向无环图(DAG图)。在图中用顶点表示活动,用弧表示活动的优先关系的DAG图被称为顶点表示活动的网,简称AOV-网(activity on vertex)。
本文的图存储结构使用的是图的邻接矩阵,在算法的实现大同小异,读者可自行修改。也可以参考本专栏的对应知识点—数据结构_图
另外会借助的数据结构有栈,也可参考—— 数据结构_栈和队列
拓扑排序
算法思想
前述AOV-网必须满足是DAG图,所以需要对于一个有向图进行判断它是否有环,方式是拓扑排序。拓扑排序就是将AOV-网中的所有顶点排成一个线性序列,该序列需要满足:如果在AOV-网中存在顶点Vi 到 Vj 的路径,则在该序列中Vi 必在Vj 前。
对于有向图有无环的检验,重点在对顶点入度的检验。
①如果是图的起点,那么它的入度必定为0
②如果图中不存在环,那么消去一个入度为 0 的顶点和以它作为弧尾的所有弧,则必然会存在或产生一个入度为 0 的顶点
③反复②操作,消去入度为 0 的顶点时将它输出,那么如果是有向无环图最后一定会产生一个关于图中所有顶点的序列;反之,如果存在有顶点未输出,那么图中含有环。
代码实现
①调用一个数组来存储顶点的入度,首次用图来初始化顶点的入度,然后通过消除一个入度为0的顶点和将它所在弧的弧头的顶点的入度-1(也就是消除顶点和以该顶点作为弧尾所在的弧)
②因为需要对入度为0的顶点进行①的操作,所以借助一些数据结构将入度为0的结点存储,不然得不断遍历存储顶点入度得数组,时间复杂度增加。辅助的数据结构可以选择栈,队列甚至链表都可以。我们需要保证的是最后经过①操作的反复实现后,遍历入度为0的顶点数是等于图的顶点数目的,这就保证没有回路。
/ ========================================================
/ 拓扑排序实现
/ ========================================================
int indegree[MAX]; //记录各个顶点的入度
int Ts[MAX]; //记录排序后的顶点序号序列
void Tsort(G_Amatreix g) {
int m;
for (int i = 0; i < g.n; ++i) { //初始化入度数组
m = 0;
for (int j = 0; j < g.n; ++j) {
if (g.E[j][i] != Maxint)
++m;
}
indegree[i] = m;
}
Lkstack s;
Lkstackinit(s);
int m1 = 0; //记录输出的顶点数目
for (int i = 0; i < g.n; ++i) { //将入度为0的入栈
if (indegree[i] == 0)
Push(s, i);
}
while (s) { //栈不为空时,顶点出栈输出并将以它作为弧尾的弧的弧头顶点入度-1
int i;
Pop(s, i);
Ts[m1] = i;
++m1;
for (int j = 0; j < g.n; ++j) { // 弧头顶点入度-1
if (g.E[i][j] != Maxint) {
if (--indegree[j] == 0)
Push(s, j);
}
}
}
if (m1 < g.n)
printf("含有回路");
else {
for (int i = 0; i < g.n; ++i)
printf("%c\t", g.V[Ts[i]]);
}
printf("\n");
Lkstackfree(s); //链式栈的释放
}
关键路径
思考在一些工程开始后,一些项目同时进行,每个项目需要的时间不同,而且需要某几个项目都结束后才可以开始下一个项目,所有的项目都完成后工程才算结束。那么从整个工程来看,一定存在一条路径上的所有项目完成后,该工程就结束了(也就是该项目的时间花销总和之内,所有的项目都能完成)。将工程起点看出源点(入度为0),将工程的结束点看出汇点(出度为0),上述路径的带权路径长度就是整个工程需要的时间总合,被称为关键路径,路径上的活动被称为关键活动,它们直接影响者工程的总时间。
与AOV-网相对于的是AOE-网(activity on edge),即用边表示活动的网。AOE-网是一个带权的有向无环图。其中,顶点表示事件,弧表示活动,权表示活动持续的时间。AOE-网可以用来估算工程完成的时间。
算法思想
- 算法思想
关键路径的四个描述
①事件Vi的最早发生时间:进入事件Vi的每一个前需活动都结束,事件才可以发生。所以事件的最早发生时间是源点到Vi的最长路径长度。起点时间的最早发生时间为0。
如上图:对于图中E事件的发生必须A,C,D事件都发生,而两条路径分别完成的时间为10(a3+a4),8(a2+a5)。所以时间 10 之后这三个事件都完成了。即选择路径长度最长的作为事件Vi的最早发生时间。最后一个顶点的最早发生时间就是整个工程的完成所需的时间总和,选择的路径就是关键路径(上图中为F,关键路径为{A,C,E,F},所需时间为14,关键活动为{a3,a4,a7})。
②事件Vi的最迟发生时间:事件Vi的完成时间不能耽误后序事件的发生,所以最迟发生时间为所有Vi的后续事件Vj的最迟发生时间减去对应活动<Vi,Vj>的权值时间中的最小值。最迟时间的确认需要在①的基础把所有顶点的最早发生时间求出后,回退求事件的最迟发生时间。
如上图,由①得到的最后图的关键路径为{A,C,E,F},所需时间为14,关键活动为{a3,a4,a7}。根据定义得事件的最迟时间E = 10,C = 7, B = 9。而由{A,C,E,F}路径得A = 0,和{A,B,F}路径得A = 3中的最小值A = 0。可以发现,在关键路径上的事件的最迟和最早发生时间是相同的;相对于不是关键路径上的事件其存在的差值为该路径和关键路径上的总差值。
③活动a = <vi,vj>的最早开始时间:因为活动的开始是对应事件的发生才能进行,所以活动的最早开始时间等于事件Vi的最早发生时间。
④活动a = <vi,vj>的最晚开始时间:事件Vj最迟发生时间减去<vi , vj>活动的持续时间。相等于事件Vi的最迟发生时间。
可以说图中活动的最早、最晚开始时间是图中事件的最早、最迟发生时间在活动(弧)上的另一种体现。这样我们就可以实现操作事件(也就是顶点)来代替活动(弧)的过程。通过对于整个图中的事件的的最早、最迟发生时间的求值,然后其中事件的这俩个时间相同的就是关键路径上的事件。
- 算法实现
需要借助的数据结构
根据上述实现的描述,对于整个工程的顶点,是按照一定顺序对它的事件的最早、迟发生时间进行求值的。而且这个求值顺序是按照前需事件完成后才能开始的,而且整个工程只有一个源点和汇点不能有回路,这就涉及前面的拓扑排序的排序思想。所以这个求值顺序就是拓扑排序求得的顺序。
①一维数组Ve[ i ] : 事件Vi的最早发生时间
ve(0) = 0
ve(i) = Max{ ve(k)+wk,i } <vk,vi>∈T,1≤i≤n−1,wk为<vk,vi>上活动的时间消耗
②一维数组VL[ i ] : 事件Vi的最迟发生时间
vL( n−1 ) = ve( n−1 )
vL(i) = Min{ vL(k)−wi,k } <vi,vk>∈S,0≤i≤n−2
③一维数组TS[ i ] : 记录拓扑序列的顶点序号。
求出来的两个数组后,就可以根据事件最早、迟发生时间和活动的最早、晚开始时间之间关系求出关键活动、关键路径。
代码实现
/ ========================================================
/ 关键路径实现
/ ========================================================
int Ve[MAX]; //记录事件的最早发生时间
int VL[MAX]; //记录事件的最迟发生时间
bool Cpath(G_Amatreix g) {
if (!Tsort(g)) //拓扑排序检查是否有回路,并生成排序序列
return false;
int n = g.n;
//因为要更新比较的是最大值,所以每个顶点先赋初值为这图中能存在的最小值。
for (int i = 0; i < n; ++i) //初始化每个事件的最早发生时间为 0
Ve[i] = 0;
int m = 0;
int i = 0;
for (; i < n; ++i) { //从拓扑排序序列的第一项开始对顶点更新事件的最早发生时间
m = Ts[i];
for (int j = 0; j < n; ++j) {
if (g.E[m][j] != Maxint) { //存在活动时更新事件用活动的时间消耗的最大值
if (Ve[m] + g.E[m][j] > Ve[j])
Ve[j] = Ve[m] + g.E[m][j];
}
}
}
//因为要更新比较的是最小值,所以每个顶点先赋初值为这图中能存在的最大值。
for (i = 0; i < n; ++i) //初始化每个事件的最迟发生时间为 最后的总消耗时间
VL[i] = Ve[n - 1];
for (i = n - 1; i >= 0; --i) { //从拓扑排序序列的最后一项逆序对顶点更新事件的最迟发生时间
m = Ts[i];
for (int j = 0; j < n; ++j) {
if (g.E[m][j] != Maxint) {
if (VL[j] - g.E[m][j] < VL[m]) //存在活动时更新事件用活动的时间消耗的最小值
VL[m] = VL[j] - g.E[m][j];
}
}
}
for (i = 0; i < n; ++i) //找出活动中最早开始时间和最迟开始时间相同的,就是关键活动
for (int j = 0; j < n; ++j)
if (g.E[i][j] != Maxint) {
int e = Ve[i];
int L = VL[j] - g.E[i][j];
if (e == L)
printf("<%c,%c>\t", g.V[i], g.V[j]);
}
printf("\n");
}
完整代码
- 所需头文件
①GAmatrix.h 图的邻接矩阵头文件
///GAmatrix.h
#pragma once
#include<stdio.h>
#define Maxint 32767
#ifndef MAX
#define MAX 20
#endif // MAX
typedef char VertexType;
typedef int Edgetype;
typedef struct {
VertexType V[MAX];
Edgetype E[MAX][MAX];
int n, e;
}G_Amatreix;
//创建图
void CreateG(G_Amatreix& g);
//找到顶点在顶点集中的标号
int LocateV(G_Amatreix g, VertexType v);
②Lkstack.h 链式栈的头文件
///Lkstack.h
#pragma once
#include<stdbool.h>
#include<stdio.h>
#ifndef Item
typedef struct item {
//数据项
int data;
bool operator==(const item& a) {
return data == a.data;
}
}Item;
#endif // !Item
//栈数据元素(更改数据元素,修改基本元素结构体和接口的实现即可)
typedef struct Stack{
Item e;
struct Stack* next;
}Stack, *Lkstack;
//栈初始化
bool Lkstackinit(Lkstack& L);
//入栈
bool Push(Lkstack& L, Item e);
//出栈
bool Pop(Lkstack& L, Item& e);
//栈取值
bool GetItem(const Lkstack L, Item& e);
- 主函数测试c文件
#include<stdio.h>
#include<stdlib.h>
#define Item int
#include"Lkstack.h"
#include"GAmatrix.h"
/ ========================================================
/ 链式栈接口实现
/ ========================================================
//栈初始化
bool Lkstackinit(Lkstack& L) {
L = NULL;
return true;
}
//入栈
bool Push(Lkstack& L, Item e) {
Lkstack p = new Stack;
p->e = e;
p->next = L;
L = p;
return true;
}
//出栈
bool Pop(Lkstack& L, Item& e) {
if (!L)
return false;
e = L->e;
Lkstack p = L;
L = L->next;
delete p;
return true;
}
//栈取值
bool GetItem(const Lkstack L, Item& e) {
if (!L)
return false;
e = L->e;
return true;
}
void Lkstackfree(Lkstack& L) {
Lkstack p = NULL;
while (L) {
p = L->next;
delete L;
L = p;
}
if(!p)
printf("freed");
}
/// ===============================================
/// 图邻接矩阵接口实现
/// ===============================================
//创建图
void CreateG(G_Amatreix& g) {
printf("input n , e:\n");
scanf_s("%d,%d", &g.n, &g.e);
getchar();
//初始化V
for (int i = 0; i < g.n; ++i) {
printf("input V[%d]:", i);
scanf_s("%c", &g.V[i], 1);
char ch = getchar();
while (ch != '\n')
ch = getchar();
}
//初始化E
for (int i = 0; i < g.n; ++i) {
for (int j = 0; j < g.n; ++j) {
g.E[i][j] = Maxint; //创立网是为无穷大,图时为0
}
}
//输入边
char v1 = 0, v2 = 0;
int m = 0;;
int j, k;
for (int i = 0; i < g.e; ++i) {
printf("input the edge:(v1 v2 w)\n");
scanf_s("%c %c %d", &v1, 1, &v2, 1, &m);
char ch = getchar();
while (ch != '\n')
ch = getchar();
j = LocateV(g, v1);
k = LocateV(g, v2);
g.E[j][k] = m; //创立网时为权值w,图时为1
//g.E[k][j]=g.E[j][k]; //无向时为对称矩阵
}
输出边
//for (int i = 0; i < g.n; ++i) {
// printf("顶点%c:\n", g.V[i]);
// for (int j = 0; j < g.n; ++j) {
// if (g.E[i][j] != Maxint)
// printf("边为<%c,%c>,权值为%d:\n", g.V[i], g.V[j], g.E[i][j]);
// }
//}
}
//找到顶点在顶点集中的标号
int LocateV(G_Amatreix g, VertexType v) {
int j = 0;
for (; j < g.n; ++j) {
if (g.V[j] == v)
return j;
}
return j;
}
/ ========================================================
/ 拓扑排序实现
/ ========================================================
int indegree[MAX]; //记录各个顶点的入度
int Ts[MAX]; //记录排序后的顶点序号序列
bool Tsort(G_Amatreix g) {
int m;
for (int i = 0; i < g.n; ++i) { //初始化入度数组
m = 0;
for (int j = 0; j < g.n; ++j) {
if (g.E[j][i] != Maxint)
++m;
}
indegree[i] = m;
}
Lkstack s;
Lkstackinit(s);
int m1 = 0; //记录输出的顶点数目
for (int i = 0; i < g.n; ++i) { //将入度为0的入栈
if (indegree[i] == 0)
Push(s, i);
}
while (s) { //栈不为空时,顶点出栈输出并将以它作为弧尾的弧的弧头顶点入度-1
int i;
Pop(s, i);
Ts[m1] = i;
++m1;
for (int j = 0; j < g.n; ++j) { // 弧头顶点入度-1
if (g.E[i][j] != Maxint) {
if (--indegree[j] == 0)
Push(s, j);
}
}
}
if (m1 < g.n)
return false;
else {
for (int i = 0; i < g.n; ++i)
printf("%c\t", g.V[Ts[i]]);
return true;
}
printf("\n");
Lkstackfree(s);
}
/ ========================================================
/ 关键路径实现
/ ========================================================
int Ve[MAX]; //记录事件的最早发生时间
int VL[MAX]; //记录事件的最迟发生时间
bool Cpath(G_Amatreix g) {
if (!Tsort(g)) //拓扑排序检查是否有回路,并生成排序序列
return false;
int n = g.n;
for (int i = 0; i < n; ++i) //初始化每个事件的最早发生时间为 0
Ve[i] = 0;
int m = 0;
int i = 0;
for (; i < n; ++i) { //从拓扑排序序列的第一项开始对顶点更新事件的最早发生时间
m = Ts[i];
for (int j = 0; j < n; ++j) {
if (g.E[m][j] != Maxint) { //存在活动时更新事件用活动的时间消耗的最大值
if (Ve[m] + g.E[m][j] > Ve[j])
Ve[j] = Ve[m] + g.E[m][j];
}
}
}
for (i = 0; i < n; ++i) //初始化每个事件的最迟发生时间为 最后的总消耗时间
VL[i] = Ve[n - 1];
for (i = n - 1; i >= 0; --i) { //从拓扑排序序列的最后一项逆序对顶点更新事件的最迟发生时间
m = Ts[i];
for (int j = 0; j < n; ++j) {
if (g.E[m][j] != Maxint) {
if (VL[j] - g.E[m][j] < VL[m]) //存在活动时更新事件用活动的时间消耗的最小值
VL[m] = VL[j] - g.E[m][j];
}
}
}
printf("\n");
for (i = 0; i < n; ++i) //找出活动中最早开始时间和最迟开始时间相同的,就是关键活动
for (int j = 0; j < n; ++j)
if (g.E[i][j] != Maxint) {
int e = Ve[i];
int L = VL[j] - g.E[i][j];
if (e == L)
printf("<%c,%c>\t", g.V[i], g.V[j]);
}
printf("\n");
}
int main() {
G_Amatreix g;
CreateG(g);
Cpath(g);
return 0;
}
- 测试