有向无环图 拓扑排序与关键路径
一:基本概念
1:有向无环图
一个无环的有向图称做有向无环图(Directed Acyclic Graph)。简称DAG 图。DAG 图是一类较有向树更一般的特殊有向图,
2:拓扑排序
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
3:关键路径
通俗的讲:关键路径是项目中最长的路径。任何关键路径上的元素的延迟(浮动)时间将直接影响项目的预期完成时间。
4:AOE网
用顶点表示事件,弧表示活动,弧上的权值表示活动持续的时间的有向图AOE(Activity On Edge Network)网。
(1) 活动(弧)开始的最早时间ee(i);
(2) 活动(弧)开始的最晚时间el(i) ;
(3) 事件(顶点)开始的最早时间ve(i);
(4) 事件(顶点)开始的最晚时间vl(i)。
5:关键活动
定义ee(i)=el(i)的活动叫关键活动。
二:拓扑排序
1:储存结构
(1):采用邻接表作为存储结构(无向图)
邻接表详细知识见下:
https://blog.csdn.net/weixin_39956356/article/details/80514672
注意:权值一定要在建好链表后赋值
相关代码
//创建有向图的邻接表
void CreatDLGraph(DLGraph &G)
{
for (int i = 0; i < G.vexnum; i++) {
printf("Please enter %d data:", i + 1);
scanf(" %c", &G.vertices[i].data); //输入顶点值
G.vertices[i].firstarc = NULL; //弧表头指针置为空
}
VertexType v1, v2; //输入的两条弧(A B C···)
EdgeType w;
int i, j; //获取(v1,v2)各自在图中位置
printf("Please input the two data of arc,for example: A C\n");
for (int k = 0; k < G.arcnum; k++) {
printf("The %d arc: ", k + 1);
scanf(" %c", &v1); //输入第一个顶点
getchar(); //把输入的空格读走
v2 = getchar(); //输入弧的第二个顶点
scanf("%d", &w); //输入权值
i = LocateVex(G, v1); //获取弧的第一个节点位置
j = LocateVex(G, v2); //获取弧的第二个节点位置
/*******************************************************
** 1:无向图相互的,所以下面的两段代码就i,j互换
** 2:这里要清楚怎么接上去的
新结点的next后面接的是之前的一串上(可以这么想,新的节点接在原来的左弧)
不断把上次firtarc赋值给新节点的next,新的p又接在firtarc
********************************************************/
ArxNode *p = (ArxNode *)malloc(sizeof(ArxNode)); //分配弧结点空间
p->adjvex = j;
p->nextarc = G.vertices[i].firstarc; //i是弧尾,且出发的弧是一链表,说明这里的p是
G.vertices[i].firstarc = p;
//注意:这里必须在邻接表创建后才能赋值w,不然G.vertices[i].firstarc是个空指针-error
G.vertices[i].firstarc->weight = w; //某活动持续的时间
//p = (ArxNode *)malloc(sizeof(ArxNode)); //重新分配弧结点空间(上面的消逝了)
//p->adjvex = i;
//p->nextarc = G.vertices[j].firstarc; //将结点i链接到j的单链表中
//G.vertices[j].firstarc = p;
}
}
(2):邻接表求出度——这个很简单,邻接本来就是一个节点所有出发的弧
相关代码
//求出度
void FindOutDegree(DLGraph &G, int *indegree)
{
ArxNode *p; //切记:这里创建一个临时p,我们不可以把之前邻接表结构改变了,就像给你U盘拷贝东西,你只能复制而不是剪切!你不可以改变之前的结构,不然之后邻接表是个空的
int num;
for (int i = 0; i < G.vexnum; i++)
{
num = 0;
p = G.vertices[i].firstarc; //先指向一个结点
while (p != NULL) {
p = p->nextarc; //指向下一条弧
num++; //出度加一
}
indegree[i] = num;
}
}
(3):邻接表求入度——这个对邻接表而言是不可以直接求出,需要一次遍历才能求出一个节点的入度
相关代码
//求入度
void FindInDegree(DLGraph &G, int *indegree)
{
ArxNode *p; //切记:这里创建一个临时p,我们不可以把之前邻接表结构改变了,就像给你U盘拷贝东西,你只能复制而不是剪切!你不可以改变之前的结构,不然之后邻接表是个空的
int num;
for (int i = 0; i < G.vexnum; i++) //求所有节点的入度
{
num = 0;
for(int j = 0; j < G.vexnum; j++) //一个结点的入度需要遍历所有结点才可以知道
{
p = G.vertices[j].firstarc; //先指向一个结点
while (p != NULL) {
if (p->adjvex == i) //从另外一个结点出发的链域,发现与该次所求结点一致,入读加一,break
{
num++;
break;
}
p = p->nextarc; //指向下一条弧
}
}
indegree[i] = num;
//printf("%d\t", indegree[i]);
}
}
2:拓扑排序
(1):这里涉及两个栈(S,T),事件开始的最早时间ve[i]
S—-拓扑有序栈
T—-逆拓扑有序栈
初始值全为0ve[i] = 0; 最早发生时间
(2):入度为0,全部入栈S
(3):S出栈(直到S为空),T入栈
(4):ve[k] (终点)值更新 popDataIndex(起点)–>k(终点)
(5):关于栈的基础函数
stack.h
#ifndef _STACK_H
#define _STACK_H
#include <stdlib.h>
#define STACK_INIT_SIZE 8 //储存空间初始量
#define STACKINCREMENT 5 //储存空间分配增量
#define ERROR 0
#define OK 1
#define TRUE 2
#define OVERFLOW -2
typedef int Status;
typedef char SElemType;
typedef struct
{
SElemType *base; //栈底
SElemType *top; //栈顶
int stacksize; //每次的新元素个数
}SqStack;
Status InitStack(SqStack &s); //构造一个空栈操作
Status Push(SqStack &s, SElemType e); //栈的插入操作 Push
SElemType Pop(SqStack &s); //取栈顶元素 Pop
SElemType GetTop(SqStack &s); //获取栈顶元素
SElemType StackLength(SqStack &s); //获取栈当前元素个数
Status StackEmpty(SqStack &s);
#endif // !_STACK_H
stack.cpp
#include "stdafx.h"
#include "stack.h"
//构造一个空栈操作
Status InitStack(SqStack &s)
{
s.base = (SElemType *)malloc(STACK_INIT_SIZE * sizeof(SElemType));
if (!s.base)
return OVERFLOW;
memset(s.base, 0, STACK_INIT_SIZE * sizeof(SElemType)); //初始化每个都弄为0
s.top = s.base;
s.stacksize = STACK_INIT_SIZE;
return OK;
}
//栈的插入操作 Push
Status Push(SqStack &s, SElemType e)
{
if ((s.top - s.base) >= s.stacksize)
{
s.base = (SElemType *)realloc(s.base, (STACK_INIT_SIZE + STACKINCREMENT) * sizeof(SElemType));
if (!s.base)
return OVERFLOW;
s.top = s.base + s.stacksize;
s.stacksize += STACKINCREMENT;
}
*s.top++ = e;
return OK;
}
//取栈顶元素 Pop
SElemType Pop(SqStack &s)
{
if (s.base == s.top)
return ERROR;
return *--s.top;
}
//获取栈顶元素
SElemType GetTop(SqStack &s)
{
if (s.base == s.top)
return ERROR;
return *(s.top - 1);
}
//获取栈当前元素个数
SElemType StackLength(SqStack &s)
{
return (s.top - s.base) / sizeof(SElemType);
}
//判栈空
Status StackEmpty(SqStack &s)
{
if (s.base == s.top)
return 1;
return 0;
}
相关代码
int *ve;
//拓扑排序
int TopologicalOrder(DLGraph &G, SqStack &S, SqStack &T)
{
int *indegree = (int *)malloc(G.vexnum * sizeof(int));
ve = (int *)malloc(G.vexnum * sizeof(int));
FindInDegree(G, indegree); //求出每个节点的入度
int count = 0; //累计访问的结点个数
InitStack(S); //拓扑有序栈
for (int i = 0; i < G.vexnum; i++) {
if (!indegree[i])
Push(S, G.vertices[i].data); //入度为0,全部入栈S
ve[i] = 0; //最早发生时间
}
InitStack(T); //逆拓扑有序栈
while(!StackEmpty(S)){
char popData = Pop(S); //出栈元素
int popDataIndex = LocateVex(G, popData); //出栈元素的位置
Push(T, popData); //出栈元素马上入栈T
count++; //访问节点数+1
ArxNode *p = G.vertices[popDataIndex].firstarc;
for (; p; p = p->nextarc) {
int k = p->adjvex;
if (--indegree[k] == 0) //减1,入度为0,入栈S
Push(S, G.vertices[k].data);
if (ve[popDataIndex] + p->weight > ve[k]) //更新k最早发生时间
ve[k] = ve[popDataIndex] + p->weight;
}
}
printf("\n\nve[]:\n");
for(int i = 0; i < G.vexnum; i++)
printf("%d\t", ve[i]);
if (count < G.vexnum) //该有向网有环
return ERROR;
return OK;
}
三:关键路径
1:vl[i]初始值是关键路径长度(最长时间),也就是T栈顶元素对应的ve[ ]
2:逆序求vl[ ],T出栈(直至为0),下面用一个图将清楚
3:求所有的关键路径并输出,关键路径长度,注意怎么计算ee(正着) 与 el (反着)的
4:ee = el,关键路径
相关代码
//求关键路径及长度
int CriticalPath(DLGraph &G)
{
SqStack S;
SqStack T;
int *vl = (int *)malloc(G.vexnum * sizeof(int));
if (!TopologicalOrder(G, S, T)){ //有环结束,不需要继续了
printf("The direction graph contains rings, Can't output critical path, Test end!!!\n");
return ERROR;
}
else
{
ArxNode *p;
int k;
for (int i = 0; i < G.vexnum; i++)
vl[i] = ve[LocateVex(G, GetTop(T))]; //获取栈T的栈顶元素,找到其位置,把ve[]赋值给vl[]的所有元素(就是把最大值赋给所有的vl[])
while (!StackEmpty(T)) {
char popData = Pop(T); //出栈元素
int popDataIndex = LocateVex(G, popData); //出栈元素的位置
p = G.vertices[popDataIndex].firstarc;
for (; p; p = p->nextarc) {
k = p->adjvex;
if (vl[k] - p->weight < vl[popDataIndex]) { //倒序求,已知k(弧头)求popDataIndex(弧尾),弧尾-弧头<弧头,替换
vl[popDataIndex] = vl[k] - p->weight;
}
}
}
printf("\n\nvl[]:\n"); //输出vl[]
for(int i = 0; i < G.vexnum; i++)
printf("%d\t", vl[i]);
/*******************************************************************
** 这里我举个例子,比如A->B
** 在第一次循环中,i是A k是B
el是'vl[B]'- 权值 ---如果等于--ee = ve[i],关键路径
********************************************************************/
int ee, el;
printf("\n\ncritical path weight\tee el 是否为关键路径");
for (int i = 0; i < G.vexnum; i++) //求所有的关键路径
for (p = G.vertices[i].firstarc; p; p = p->nextarc) { //不防从第一个点开始,比如A
k = p->adjvex;
ee = ve[i]; el = vl[k] - p->weight;
if (ee == el) {
printf("\n%c --> %c weight: %d\tee: %d el:%d 关键路径", G.vertices[i].data, G.vertices[k].data, p->weight, ee, el);
}
}
printf("\n\nThe length of critical path is: %d", vl[G.vexnum-1]); //关键路径长度
}
return OK;
}
三:主函数与输出
(1):主函数
#include "stdafx.h"
#include "Topological.h"
#include "stack.h"
int main()
{
DLGraph G;
printf("Please enter vexnum and arcnum: ");
scanf("%d %d", &G.vexnum, &G.arcnum); //输入结点数,弧数
CreatDLGraph(G); //创建有向图的邻接表
printf("\n无向图的邻接表输出:\n");
PrintDLGraph(G); //输出有向图的邻接表
CriticalPath(G); //输出关键路径与长度
return 0;
}
(2):输出
四:感谢与源代码(VS2017)
(1):感谢以下文章对我的帮助
https://www.cnblogs.com/acmtime/p/6106148.html
(2):源代码
链接: https://pan.baidu.com/s/1-n70wJc42KfQvtHOsJn-DQ 密码: zjbg