有向无环图及其应用
一.有向无环图的概念
一个无环
的有向图
称作有向无环图
。简称DAG图
。DAG
图是相较于有向树的更特殊的图。比如:
检查一个图是否有环,可以通过遍历+标记的方式进行检查,若某个顶点的弧指向了另一个已经遍历过的顶点,则该图必定含有环。
二.拓扑排序(AOV网)
1.概念
来自百度:
对一个 有向无环图 (Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个 偏序 得到该集合上的一个 全序 ,这个操作称之为拓扑排序。
2.偏序与全序
定义可以自己百度,这里记一下我对于这个偏序与全序的理解。
a).偏序
偏序里的集合的成员,只能部分的访问到,存在某个顶点,无法通过其指向访问的顶点,比如这个图,V2 无法访问到 V3, V3无法访问到V2 。V1可以通过指向V2指向V4访问到V4。
b).全序
全序是指,所有的成员都可以互相比较。
比如这个图,图中任意两个顶点,都是可以从图中找到一条以上的通路而访问到。
c).偏序与全序的区别
用具体例子来说,偏序可以表示为某些科目依赖关系,而全序可以理解为设计出来的学习计划。
举个例子:偏序表告诉我们学习控制理论需要先学习高数1、2和信号与系统,信号与系统需要先学习高数1,2。
那么全序就是我们的学习计划:先学习高数1,再学习高数2,再学习信号与系统,再学习控制理论。
3.拓扑有序
如果我们通过自己添加弧来使得一个偏序变成全序,那么这个操作就是拓扑排序
而这个全序被称作拓扑有序
。
某个偏序的拓扑有序是不唯一的:排序的方式不同,所以人为添加的弧的方式也不同,所以一个偏序的拓扑有序是不唯一的,它会有多个。
4.拓扑排序的过程
- 再有向图中,选择一个没有前驱的顶点且输出。
- 从图中删除该顶点和它为尾的弧。
- 重复1、2直到所有的顶点都已输出,或当前图中不存在无前驱的顶点为止(有环)。
三.关键路径(AOE网)
1.概念
AOE 网是在 AOV 网的基础上
,其中每一个边都具有各自的权值,是一个有向无环网。其中权值表示活动持续的时间。
所以我们AOE网的建立至少要满足拓扑有序
。
使用 AOE 网可以帮助解决这样的问题:如果将 AOE 网看做整个项目,那么完成整个项目至少需要多少时间?
解决这个问题的关键在于从 AOE 网中找到一条从起始点到结束点长度最长的路径,这样就能保证所有的活动在结束之前都能完成。
起始点是入度为 0 的点,称为“源点”;结束点是出度为 0 的点,称为“汇点”。这条最长的路径,被称为”关键路径“。
为了求出一个给定 AOE 网的关键路径,需要知道以下 4 个统计数据:
- 对于 AOE 网中的顶点有两个时间:最早发生时间(用 Ve(j) 表示)和最晚发生时间(用 Vl(j) 表示);
- 对于边来说,也有两个时间:最早开始时间(用 e(i) 表示)和最晚开始时间( l(i) 表示)。
2.实现
对照这个图来讲:
a).最早发生时间Ve(j)
这里的最早发生时间,实际上是事件最早能多久开始,也就是前面的时间路线的最长的那一条,就比如你要沏茶肯定得水先烧好,就算把茶叶放进去了你还是得等水。
Ve(j):对于 AOE 网中的任意一个顶点来说,从源点到该点的最长路径代表着该顶点的最早发生时间,通常用 Ve(j) 表示。
例如,图 1 中从 V1 到 V5 有两条路径,V1 作为源点开始后,a1 和 a2 同时开始活动,但由于 a1 和 a2 活动的时间长度不同,最终 V1-V3-V5 的这条路径率先完成。但是并不是说 V5 之后的活动就可以开始,而是需要等待 V1-V2-V5 这条路径也完成之后才能开始。所以对于 V5 来讲,Ve(5) = 7。
b).最晚发生时间Vl(j)
还是用泡茶的例子,因为烧水需要一定的时间,所以放茶叶并不需要在刚烧水就开始,也可以在水开之前开始,后者就是我们的最晚发生时间。
Vl(j):表示在不推迟整个工期的前提下,事件 Vk 允许的最晚发生时间。
例如,在得知整个工期完成的时间是 18 天的前提下,V7 最晚要在第 16 天的时候开始,因为 a10 活动至少需要 2 天时间才能完成,如果在 V7 事件在推迟,就会拖延整个工期。所以,对于 V7 来说,它的 Vl(7)=16。
c).e(i)
表示活动 ai 的最早开始时间,如果活动 ai 是由弧 <Vk,Vj> 表示的,那么活动 ai 的最早开始的时间就等于时间 Vk 的最早发生时间,也就是说:e[i] = ve[k]
。
d).l(i)
l(i):表示活动 ai 的最晚开始时间,如果活动 ai 是由弧 <Vk,Vj> 表示,ai 的最晚开始时间的设定要保证 Vj 的最晚发生时间不拖后。所以,l[i]=Vl[j]-len<Vk,Vj>
。
四.代码
1.AOV
样例输入与输出:
please input the number of Vertex and Arc:6 8
please input Vertexs' name:
123456
please input (tail,head) as the arc:
0 1
0 2
0 3
2 1
2 4
3 4
5 3
5 4
No.0 inArc:0 Vex:1 to 2 3 4
No.1 inArc:2 Vex:2 to
No.2 inArc:1 Vex:3 to 2 5
No.3 inArc:2 Vex:4 to 5
No.4 inArc:3 Vex:5 to
No.5 inArc:0 Vex:6 to 4 5
One AOV is: 6 1 4 3 5 2
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define OK 1
#define ERROR 0
#define MaxLen 100
typedef struct ArcNode{
int Head;
struct ArcNode *nextArc;
}ArcNode; // 创建弧结点
typedef struct VNode{
char NodeName;
int inNum;
ArcNode *nextArc;
}VNode; // 创建Vertex顶点结点
typedef struct Graph{
int len;
VNode *Nbase; // 线性存储顶点结点
}Graph; // 创建一张图
int MarkGraph[MaxLen]; // 标记数组
Graph *GraphCreat(void);
int GraphShowList(Graph *G);
int GraphShowAOV(Graph *G);
int main()
{
Graph *map = GraphCreat();
GraphShowList(map);
GraphShowAOV(map);
return 0;
}
Graph *GraphCreat(void)
{
Graph *G = (Graph*)malloc(sizeof(Graph));
int lenV, lenA;
int i, tail, head;
char chname[MaxLen];
char temp;
ArcNode *ArcNow, *ArcLast;
printf("please input the number of Vertex and Arc:");
scanf("%d %d", &lenV, &lenA); // 获取 顶点数和弧的个数
if (lenV > MaxLen)
{
printf("Over MaxLenth");
exit(ERROR);
} // 限制下大小
// 创建顶点线性表
G->len = lenV;
G->Nbase = (VNode*)malloc(sizeof(VNode)*lenV);
// 存顶点名字
printf("please input Vertexs' name:\n");
//rewind(stdin);
scanf("%s", chname);
for (i = 0; i < lenV; ++i)
{
//scanf("%c", &temp);
temp = chname[i];
(G->Nbase+i)->NodeName = temp;
(G->Nbase+i)->inNum = 0;
(G->Nbase+i)->nextArc = NULL;
}
// 存弧
printf("please input (tail,head) as the arc:\n");
for (i = 0; i < lenA; ++i)
{
scanf("%d %d", &tail, &head);
(G->Nbase+head)->inNum += 1;
// 如果是顶点的第一个弧
if ((G->Nbase+tail)->nextArc == NULL)
{
(G->Nbase+tail)->nextArc = (ArcNode*)malloc(sizeof(ArcNode));
(G->Nbase+tail)->nextArc->Head = head;
(G->Nbase+tail)->nextArc->nextArc = NULL;
}
else // 非顶点的第一个弧
{
ArcNow = (G->Nbase+tail)->nextArc;
while(ArcNow->nextArc != NULL) // 循环移动到最后一个弧
{
ArcNow = ArcNow->nextArc;
}
// 创建弧结点
ArcNow->nextArc = (ArcNode*)malloc(sizeof(ArcNode));
ArcNow->nextArc->Head = head;
ArcNow->nextArc->nextArc = NULL;
}
}
return G;
}
int GraphShowList(Graph *G) // 循环输出每个顶点的弧信息
{
int i;
ArcNode *ArcNow;
printf("\n");
for (i = 0; i < G->len; ++i) // 循环
{
printf("No.%d inArc:%d Vex:%c to ", i, (G->Nbase+i)->inNum, (G->Nbase+i)->NodeName); // 输出顶点名
ArcNow = (G->Nbase+i)->nextArc;
while(ArcNow != NULL) // 循环输出每个弧
{
printf("%c ", (G->Nbase+ArcNow->Head)->NodeName);
ArcNow = ArcNow->nextArc;
}
printf("\n");
}
}
int GraphShowAOV(Graph *G)
{
int *Stack = (int*)malloc(sizeof(int) * G->len);
int Sptr = -1, cont = 0, i;
ArcNode *ArcNow;
for (i = 0; i < G->len; ++i)
{
if ((G->Nbase+i)->inNum == 0)
{
++Sptr;
Stack[Sptr] = i;
}
}
printf("\nOne AOV is: ");
while (Sptr >= 0)
{
printf("%c ", (G->Nbase+Stack[Sptr])->NodeName);
ArcNow = (G->Nbase+Stack[Sptr])->nextArc;
--Sptr;
while(ArcNow != NULL)
{
(G->Nbase + ArcNow->Head)->inNum -= 1;
if ((G->Nbase + ArcNow->Head)->inNum == 0)
{
++Sptr;
Stack[Sptr] = ArcNow->Head;
}
ArcNow = ArcNow->nextArc;
}
++cont;
if (cont > G->len)
{
printf("G IS NOT A DAG!\n");
exit(ERROR);
}
}
}
2.AOE
代码是抄的,懒得写了。
样例输入与输出:
9,11
1
2
3
4
5
6
7
8
9
1,2,6
1,3,4
1,4,5
2,5,1
3,5,1
4,6,2
5,7,9
5,8,7
6,8,4
7,9,2
8,9,4
0 3 5 0 3
0 2 4 0 2
0 1 6 0 0 *
1 4 1 6 6 *
2 4 1 4 6
3 5 2 5 8
4 7 7 7 7 *
4 6 9 7 7 *
5 7 4 7 10
6 8 2 16 16 *
7 8 4 14 14 *
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTEX_NUM 20//最大顶点个数
#define VertexType int//顶点数据的类型
typedef enum{false,true} bool;
//建立全局变量,保存边的最早开始时间
VertexType ve[MAX_VERTEX_NUM];
//建立全局变量,保存边的最晚开始时间
VertexType vl[MAX_VERTEX_NUM];
typedef struct ArcNode{
int adjvex;//邻接点在数组中的位置下标
struct ArcNode * nextarc;//指向下一个邻接点的指针
VertexType dut;
}ArcNode;
typedef struct VNode{
VertexType data;//顶点的数据域
ArcNode * firstarc;//指向邻接点的指针
}VNode,AdjList[MAX_VERTEX_NUM];//存储各链表头结点的数组
typedef struct {
AdjList vertices;//图中顶点及各邻接点数组
int vexnum,arcnum;//记录图中顶点数和边或弧数
}ALGraph;
//找到顶点对应在邻接表数组中的位置下标
int LocateVex(ALGraph G,VertexType u){
for (int i=0; i<G.vexnum; i++) {
if (G.vertices[i].data==u) {
return i;
}
}
return -1;
}
//创建AOE网,构建邻接表
void CreateAOE(ALGraph **G){
*G=(ALGraph*)malloc(sizeof(ALGraph));
scanf("%d,%d",&((*G)->vexnum),&((*G)->arcnum));
for (int i=0; i<(*G)->vexnum; i++) {
scanf("%d",&((*G)->vertices[i].data));
(*G)->vertices[i].firstarc=NULL;
}
VertexType initial,end,dut;
for (int i=0; i<(*G)->arcnum; i++) {
scanf("%d,%d,%d",&initial,&end,&dut);
ArcNode *p=(ArcNode*)malloc(sizeof(ArcNode));
p->adjvex=LocateVex(*(*G), end);
p->nextarc=NULL;
p->dut=dut;
int locate=LocateVex(*(*G), initial);
p->nextarc=(*G)->vertices[locate].firstarc;
(*G)->vertices[locate].firstarc=p;
}
}
//结构体定义栈结构
typedef struct stack{
VertexType data;
struct stack * next;
}stack;
stack *T;
//初始化栈结构
void initStack(stack* *S){
(*S)=(stack*)malloc(sizeof(stack));
(*S)->next=NULL;
}
//判断栈是否为空
bool StackEmpty(stack S){
if (S.next==NULL) {
return true;
}
return false;
}
//进栈,以头插法将新结点插入到链表中
void push(stack *S,VertexType u){
stack *p=(stack*)malloc(sizeof(stack));
p->data=u;
p->next=NULL;
p->next=S->next;
S->next=p;
}
//弹栈函数,删除链表首元结点的同时,释放该空间,并将该结点中的数据域通过地址传值给变量i;
void pop(stack *S,VertexType *i){
stack *p=S->next;
*i=p->data;
S->next=S->next->next;
free(p);
}
//统计各顶点的入度
void FindInDegree(ALGraph G,int indegree[]){
//初始化数组,默认初始值全部为0
for (int i=0; i<G.vexnum; i++) {
indegree[i]=0;
}
//遍历邻接表,根据各链表中结点的数据域存储的各顶点位置下标,在indegree数组相应位置+1
for (int i=0; i<G.vexnum; i++) {
ArcNode *p=G.vertices[i].firstarc;
while (p) {
indegree[p->adjvex]++;
p=p->nextarc;
}
}
}
bool TopologicalOrder(ALGraph G){
int indegree[G.vexnum];//创建记录各顶点入度的数组
FindInDegree(G,indegree);//统计各顶点的入度
//建立栈结构,程序中使用的是链表
stack *S;
//初始化栈
initStack(&S);
for (int i=0; i<G.vexnum; i++) {
ve[i]=0;
}
//查找度为0的顶点,作为起始点
for (int i=0; i<G.vexnum; i++) {
if (!indegree[i]) {
push(S, i);
}
}
int count=0;
//栈为空为结束标志
while (!StackEmpty(*S)) {
int index;
//弹栈,并记录栈中保存的顶点所在邻接表数组中的位置
pop(S,&index);
//压栈,为求各边的最晚开始时间做准备
push(T, index);
++count;
//依次查找跟该顶点相链接的顶点,如果初始入度为1,当删除前一个顶点后,该顶点入度为0
for (ArcNode *p=G.vertices[index].firstarc; p ; p=p->nextarc) {
VertexType k=p->adjvex;
if (!(--indegree[k])) {
//顶点入度为0,入栈
push(S, k);
}
//如果边的源点的最长路径长度加上边的权值比汇点的最长路径长度还长,就覆盖ve数组中对应位置的值,最终结束时,ve数组中存储的就是各顶点的最长路径长度。
if (ve[index]+p->dut>ve[k]) {
ve[k]=ve[index]+p->dut;
}
}
}
//如果count值小于顶点数量,表明有向图有环
if (count<G.vexnum) {
printf("该图有回路");
return false;
}
return true;
}
//求各顶点的最晚发生时间并计算出各边的最早和最晚开始时间
void CriticalPath(ALGraph G){
if (!TopologicalOrder(G)) {
return ;
}
for (int i=0 ; i<G.vexnum ; i++) {
vl[i]=ve[G.vexnum-1];
}
int j,k;
while (!StackEmpty(*T)) {
pop(T, &j);
for (ArcNode* p=G.vertices[j].firstarc ; p ; p=p->nextarc) {
k=p->adjvex;
//构建Vl数组,在初始化时,Vl数组中每个单元都是18,如果每个边的汇点-边的权值比源点值小,就保存更小的。
if (vl[k]-p->dut<vl[j]) {
vl[j] = vl[k]-p->dut;
}
}
}
for (j = 0; j < G.vexnum; j++) {
for (ArcNode*p = G.vertices[j].firstarc; p ;p = p->nextarc) {
k = p->adjvex;
//求各边的最早开始时间e[i],等于ve数组中相应源点存储的值
int ee = ve[j];
//求各边的最晚开始时间l[i],等于汇点在vl数组中存储的值减改边的权值
int el = vl[k]-p->dut;
//判断e[i]和l[i]是否相等,如果相等,该边就是关键活动,相应的用*标记;反之,边后边没标记
char tag = (ee==el)?'*':' ';
printf("%3d%3d%3d%3d%3d%2c\n",j,k,p->dut,ee,el,tag);
}
}
}
int main(){
ALGraph *G;
CreateAOE(&G);//创建AOE网
initStack(&T);
TopologicalOrder(*G);
CriticalPath(*G);
return 0;
}