【数据结构-图】拓扑排序与关键路径(C语言)

目录

题目描述

学习流程

拓扑排序

关键路径理论

算法实现

数据结构和全局变量定义,main函数

建立邻接表

拓扑排序并计算ve

遍历拓扑逆序列计算vl,e,l,求关键路径


题目描述

说明: AOE 网络是有向无环加权图,其中顶点表示事件,弧表示活动,权表示活动持续的时间,通常可以用来估算工程完成的时间,即图中从开始点到结束点之间最长的路径对应的时间。请完成一个程序,完成下列任务:

1 、计算 AOE 网络对应的拓扑排序。如果排序结果不唯一,请输出按照从小到大的顺序排列的结果。从小到大的顺序就是输入的节点序列顺序(参见下面关于输入格式的说明)。如图1中满足要求的拓扑排序是: a-b-c-d-e-f-g-h-k ,图2中满足要求的拓扑排序是:v1-v3-v5-v2-v6-v4-v7-v8-v9

2 、计算 AOE 网络的关键路径。注意关键路径可能不唯一,要求输出所有的关键路径。同样,按照是按照从小到大的顺序输出。例,如果得到两条关键路径,分别是0-1-3-6-8-9和0-1-3-4-5-8-9,那么先输出后一条路径,因为两条路径中前三个节点相同,而后一条路径第四个节点的编号小。

测试用例的输入输出格式说明:

输入:

9,11
a,b,c,d,e,f,g,h,k
<0,1,6>,<0,2,4>,<0,3,5>,<1,4,1>,<2,4,1>,<4,6,8>,<4,7,7>,<3,5,2>,<5,7,4>,<6,8,2>,<7,8,4>

节点的个数,边的条数;

各个节点的名称序列

边: < 起点 , 终点 , 权值 > 。说明起点和终点是在各个点在输入序列中的位置,如图1中边 表示为 <0,1,6> 。

输出:

关键路径

from 0(a) to 1(b) w 6
from 1(b) to 4(e) w 1
from 4(e) to 7(h) w 7
from 7(h) to 8(k) w 4

学习流程

拓扑排序

先把拓扑排序学了,这个流程不难,无非是把源头节点和对应的弧删掉,如果其中暴露出新的源头节点,就把新源头节点再进行这样的操作,循环直到结束。

关键是拓扑排序的理解。

拓扑排序排出来的到底是个什么顺序?

其实关键就是,凡是出栈的节点,其前置节点必然已经被访问过了!

所以拓扑排序的本质就是按照事情的前后顺序,把需要先完成的事情放在前面,只要顺着走下来,肯定不会碰到没有完成前置条件而无法进行下去的情况。

关键路径理论

什么是关键路径?

直观理解就是最耗时的一条路径,不完成这一条,走了别的也没用。

从严蔚敏《数据结构》里看,有四个变量ve,vl,这两个是事件最早和最晚完成的时间,如果要完成一件事,必须要完成之前的所有事情。

显而易见,设target为汇点,则ve(target)就是完成所有任务的最早时间。

这个时候我们先别管任务,只管事件,所以我们继续。

从我们实际角度理解,最早完成不就是从头开始不断往后推嘛,所以我们也很好理解ve的递推算法,ve(j)=max{ve(i)+w(i,j)},实际是以j为头,在所有的路径里找一条最长的。

同理,大家计算最晚完成也轻车熟路了,ddl怎么算我们就怎么算,从任务截止(最后一件事完成之前)往前推,vl(i)=min{vl(j)-w(i,j)},实际是以i为尾,在所有路径里找一条最长的。

为什么都是最长的?因为我们所有的假设都是在所有任务都完成的前提之下的,所以要走消耗最大的路径就可以代表所有路径。

好,ve和vl说完了,该说e和l了

e是活动最早开始的时间,自然是从前往后推,i事件一旦完成,那就可以马上进行活动,所以e(i,j)=ve(i)

l是活动最晚开始的时间,自然是从后往前推,合情合理分析,一个活动最晚开始的时间+消耗时间是不是就是下一个事件的最晚完成时间?如果活动最晚开始时间往后推,那么下一个事件的最晚完成时间也要后推,整个活动都要后推。

把上式变形:l(i,j)=vl(j)-w(i,j);

为什么要先进行拓扑排序?

我们所有事件ve的计算都是建立在前置事件已经达成的基础上,而拓扑排序本质就是进行一个完成顺序的排序,所以完美契合。

算法实现

数据结构和全局变量定义,main函数

//使用邻接链表,进行拓扑排序和关键路径计算
#include
#include
#include
#include
#include
#define MAX_V 20
typedef struct Arc {
	int hver;
	int weight;
	struct Arc* nextarc;
}Arc;
typedef struct Ver {
	char data;
	struct Arc* firstarc;
}Ver;
typedef struct Gragh {
	Ver vertexes[MAX_V];
	int vernum, arcnum;
}Gragh;
int in_arcs[MAX_V] = { 0 };
int ve[MAX_V] = { 0 };
int vl[MAX_V] = { 0 };

int main(void)
{
	Gragh G;
	std::stack T;//储存topologicalOrder的逆序列
	char path[MAX_V];//储存关键路径
	freopen("input.txt", "r", stdin);

	creatAdjList(G);
	puts("新建表后的情况");
	printAdjList(G);
	topologicalOrder(G, T);
	puts("拓扑排序并计算ve后情况");
	printAdjList(G);
	criticalPath(G, T, path);
	puts("计算了vl以及e,l后");
	printAdjList(G);


	return 0;
}


建立邻接表

没什么好说的,就是多加了个入度计算。 

void creatAdjList(Gragh& G)//构造有向图,并且计算入度
{
	scanf("%d,%d\n", &G.vernum, &G.arcnum);
	for (int i = 0; i < G.vernum; i++)//读取节点
	{
		//不用初始化inarc
		scanf("%c", &G.vertexes[i].data);
		G.vertexes[i].firstarc = NULL;
		getchar();//清理一下分隔符
	}
	for (int i = 0; i < G.arcnum; i++)//读取arc
	{
		int v1, v2, w;
		scanf("<%d,%d,%d>", &v1, &v2, &w);
		//增加弧
		Arc* arc = (Arc*)malloc(sizeof(Arc));
		if (!arc) exit(0);
		arc->nextarc = G.vertexes[v1].firstarc;
		G.vertexes[v1].firstarc = arc;
		arc->hver = v2;
		arc->weight = w;
		//增加入度
		in_arcs[v2]++;
		getchar();//清理分隔符
	}
}

void printAdjList(Gragh G)
{
	for (int i = 0; i < G.vernum; i++)
	{
		printf("in %d\t ve %d\t le %d\t data %c     ",
			in_arcs[i], ve[i], vl[i], G.vertexes[i].data);
		Arc* arc = G.vertexes[i].firstarc;
		while (arc)
		{
			printf("%d(%c) weight %d\t", arc->hver, 
				G.vertexes[arc->hver].data, arc->weight);
			arc = arc->nextarc;
		}
		putchar('\n');
	}
}

拓扑排序并计算ve

这个和我们之前遇到的有所不同,因为要额外计算个ve,vl,e,l,入度。

解决办法之一就是加全局变量,前提是数据结构已经定死了不可变了(然而实际上完全可以在vertex里加ve,vl,in_arcs,在arc里加e,l),我们采取全局变量。(虽然我一直觉得这就像脱裤子放屁一样,程序的原子性已经被破坏)

算法:

将所有0入度点加入S栈,然后进行非空栈循环

{

        首先弹出S的top,把S->T,T用来储存拓扑逆序列。同时counter++,计数判断是否有环

        注意这个时候我们的ve已经知道了,因为凡是在栈顶的,其前置任务一定都完成了

        那么我们用这个ve去计算后面点的ve也就完全逻辑无误了。

        遍历弧。遍历弧的时候就要增加一步

        {

                给下一个事件的入度-1,如果为0,就入S

                下一个点的计算下一个点的ve如果是更大的,就覆盖

                (这样就保证能最后弄出最大的ve)

        }

}

void topologicalOrder(Gragh& G, std::stack& T)//拓扑排序,求ve,并记录拓扑序列
{
	int counter = 0;//计数,判断环
	std::stack S;//临时用
	//将源点位置加入S
	for (int i = 0; i < G.vernum; i++)
	{
		if (in_arcs[i] == 0)
			S.push(i);
	}
	//进行空栈循环
	while (!S.empty())
	{
		int top = S.top();
		T.push(top);
		S.pop();
		counter++;

		Arc* arc = G.vertexes[top].firstarc;
		while (arc)
		{
			int target = arc->hver;//头部
			if (--in_arcs[target] == 0)//删除arc并且判断为0增加新源点
				S.push(target);
			int temp_ve = ve[top] + arc->weight;//计算下一个点ve
			if (temp_ve > ve[target])
				ve[target] = temp_ve;

			arc = arc->nextarc;
		}

	}
	if (counter != G.vernum)
		puts("loop");
}

遍历拓扑逆序列计算vl,e,l,求关键路径

逆向求出vl

然后计算每条arc的e,l,如果相等就记录在path数组里,同时也可以选择打印出来。

//先计算vl

初始化。先给所有vll赋值,赋最后一个ve即可,因为最后一个点相当于最早完成和最晚完成时间是相同的。

用T逆序计算vl

//计算e,l,找到关键步骤

遍历所有arc,算e,l,如果e==l,就输出

void criticalPath(Gragh G, std::stackT, char path[])//求关键路径,先使用拓扑逆序列求le
{
	//用逆序列求vl
	for (int i = 0; i < G.vernum; i++)//初始化,令最大
	{
		vl[i] = ve[G.vernum - 1];
	}
	while (!T.empty())
	{
		int top = T.top();
		T.pop();

		Arc* arc = G.vertexes[top].firstarc;
		
		while (arc)
		{
			int target = arc->hver;
			int temp_vl = vl[target] - arc->weight;
			if (temp_vl < vl[top])
				vl[top] = temp_vl;
			arc = arc->nextarc;
		}
	}
	//计算所有arc的e,l,将e==l的记录在path中
	int p = 0;//指向path尾部
	for (int i = 0; i < G.vernum; i++)
	{
		Arc* arc = G.vertexes[i].firstarc;
		while (arc)
		{
			int target = arc->hver;
			int e = ve[i];
			int l = vl[target] - arc->weight;
			if (e == l)
			{
				printf("from %d(%c) to %d(%c) w %d\n", i, G.vertexes[i].data,
					target, G.vertexes[target].data, arc->weight);
				path[p++] = i;
			}
			arc = arc->nextarc;
		}
	}

}

 但是我们这里有一些情况,就是,如果有多个关键任务,其实都是会被记录下来的,理论上可以有多条关键路径,比如下面这组数据

4,4
a,b,c,d
<0,1,5>,<0,2,5>,<1,3,2>,<2,3,2>

就会输出这个结果

from 0(a) to 2(c) w 5
from 0(a) to 1(b) w 5
from 1(b) to 3(d) w 2
from 2(c) to 3(d) w 2

那么也就是说我们需要利用求得的关键任务来组合形成关键路径,这就可能会产生多条。

具体判断方法可以是,用这些再构建一个图,然后进行dfs或者bfs,找出所有路径,我懒得实现了,放篇文章即可。

遍历寻找给定两点之间的所有路径_tan_chi_she的博客-CSDN博客_遍历两点之间所有路径

你可能感兴趣的:(数据结构+算法,图论,c语言,算法)