【C语言】关键路径/最长路径模拟实现

一、问题描述

1.拓扑排序:

在AOV网中为了更好地完成工程,必须满足活动之间先后关系,需要将各活动排一个先后次序即为拓扑排序。拓扑排序可以应用于教学计划的安排,根据课程之间的依赖关系,制定教学课程安排计划。按照用户输入的课程数,课程间的先后关系数目以及课程两两间的先后关系,程序执行后应该给出符合拓扑排序的课程安排计划。例如下图所示的课程优先关系:
在这里插入图片描述
在这里插入图片描述

程序执行后应该给出拓扑排序的结果为:
(C1,C2,C3,C4,C5,C7,C9,C10,C11,C6,C12,C8)
或者(C9,C10,C11, C6,C1, C12, C4,C2,C3, C5,C7, C8),或者其他符合要求的序列。

2.关键路径:

通常把计划、施工过程、生产流程、程序流程等都当成一个工程。工程通常分为若干个称为“活动”的子工程。完成了这些“活动”,这个工程就可以完成了。通常用AOE-网来表示工程。AOE-网是一个带权的有向无环图,其中,顶点表示事件(EVENT),弧表示活动,权表示活动持续的时间。
AOE-网可以用来估算工程的完成时间。可以使人们了解:
(1)研究某个工程至少需要多少时间?
(2)哪些活动是影响工程进度的关键?
由于AOE-网中的有些活动可以并行进行,从开始点到各个顶点,以致从开始点到完成点的有向路径可能不止一条,这些路径的长度也可能不同。完成不同路径的活动所需的时间虽然不同,但只有各条路径上所有活动都完成了,这个工程才算完成。因此,完成工程所需的最短时间是从开始点到完成点的最长路径的长度,即在这条路径上的所有活动的持续时间之和.这条路径长度就叫做关键路径(Critical Path)。
例:AOE图如下:
在这里插入图片描述

程序执行结束后应该输出:
关键活动为a1,a4,a7,a10,a8,a11
关键路径为: a1->a4->a7->a10(或者V1->V2->V5->V7->V9)和a1->a4->a8->a11(或者V1->V2->V5->V8->V9)
花费的时间为至少为18(时间单位)。

二、设计思路

主要数据结构:链表,顺序栈,顺序队列
主要算法设计:邻接表存储顶点之间的关系,二维数组存储弧的权重(活动的时长),栈辅助拓扑排序计算顶点的ve,vl进而计算弧的e和l。利用一个记录访问过的关键活动的二维数组,配合递归形式的DFS,实现输出多条完整关键路径。

三、具体源码及注释

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define N 510

int arr[N][N];//记录weight
int mark[N] = { 0 };//记录下标对应定点是否为关键顶点

int label[N][N] = { 0 };//用于记录两个定点之间是否存在关键路劲
int cnt = 0;//记录某一条关键路劲的顶点数,方便利用for循环输出

struct Node
{
	int index;
	int inDegree;
	int ve;//最早发生时间,默认为0
	int vl;//最晚发生时间,默认为无穷大 0x3f
	struct Node* next;
};

int stack[N];//栈辅助拓扑排序
int top = 0;
int stack2[N] = { -1 };//记录某路径上关键顶点的顺序(倒序)
int top2 = 0;

int queue[N];
int front = 0;
int rear;

//存储方式:邻接表
struct Node arrNode[N];

/*
给出父节点下标和当前节点下标,

*/
void addNode(int parentIndex, int nodeIndex)
{
	struct Node* parentNode = &arrNode[parentIndex];
	struct Node* temp = (struct Node*)malloc(sizeof (struct Node));

	temp->index = nodeIndex;
	temp->next = parentNode->next;
	parentNode->next = temp;

	arrNode[nodeIndex].inDegree++;
	temp->inDegree = arrNode[nodeIndex].inDegree;
}

//遍历某个结点及其所有相连的节点,并为节点初始化ve和vl,最终输出邻接表
void traverse(int size)
{
	for (int i = 0; i < size; i++)
	{
		struct Node* node = &arrNode[i];
		while (node != NULL)
		{
			printf("V%d --> ", node->index);
			node->ve = 0;
			node->vl = 32676;
			node = node->next;
		}
		printf("∧\r\n");
	}
}

//初始化弧的weight和邻接表的属性
void initMap(int size)
{
	for (int i = 0; i < size; i++)
		for (int j = 0; j < size; j++)
			arr[i][j] = 0;
	for (int i = 0; i < size; i++)
	{
		arrNode[i].index = i;
		arrNode[i].inDegree = 0;
		arrNode[i].next = NULL;
	}
}

//栈实现拓扑排序
int topologicalOrder(int size)
{
	int count = 0;
	int index = 0;
	int i = 0;

	//遍历所有节点,寻找入度为0的节点,并把编号依次存入stack中
	for (i = 0; i < size; i++)
		if (arrNode[i].inDegree == 0)
		{
			stack[top] = i;
			top++;
		}

	//弹出栈中的结点,输出结点编号,同时让该节点的邻接节点入度-1
	//循环操作、直到栈中的节点为0,即top == 0
	while (top > 0)
	{
		top--;//top的位置无内容,先-1
		index = stack[top];
		queue[rear] = index;
		rear++;
		count++;
		
		struct Node* parentNode = &arrNode[index];
		struct Node* node = parentNode->next;

		while (node != NULL)//
		{
			int sonIndex = node->index;

			//计算子节点的ve
			//遍历arrNode数组的好处为,可以考虑到每条弧,通过更新的数据的方式确保某个结点的ve是最大值
			arrNode[sonIndex].ve = max(arrNode[sonIndex].ve, arrNode[index].ve + arr[index][sonIndex]);
			
			//从arrNode中获得结点及其信息
			if (arrNode[sonIndex].inDegree > 0)
			{
				arrNode[sonIndex].inDegree--;

				if (arrNode[sonIndex].inDegree == 0)
				{
					stack[top] = sonIndex;
					top++;
				}
			}
			node = node->next;
		}
	}

	//判断是否存在环
	if (count < size)//如果输出的结点数小于总结点数
		return 0;
	return 1;
}

void showCriticalPathByDFS(int parentNode)
{
	if (parentNode == queue[rear - 1]) //递归结束条件
	{
		for (int i = 0; i <= cnt; i++)
		{
			printf("V%d", stack2[i]);
			if (i != cnt) printf("-->");
		}
		printf("\n");
	}

	for (struct Node* p = arrNode[parentNode].next; p; p = p->next)
		if (label[parentNode][p->index] == 1)
		{
			label[parentNode][p->index] = 0;
			stack2[++top2] = p->index;
			cnt++;
			showCriticalPathByDFS(p->index);
			label[parentNode][p->index] = 1;
			top2--; cnt--;
		}
}

void getKeyRoute(int size)
{
	//最后一个结点的vl就是它的ve
	arrNode[queue[rear - 1]].vl = arrNode[queue[rear - 1]].ve;

	//从终点倒着向起点方向计算
	for (int i = rear - 1; i >= front; i--)
		for (int j = i; j >= front; j--)
			if (arr[queue[j]][queue[i]] > 0)//
				arrNode[queue[j]].vl = min(arrNode[queue[j]].vl, arrNode[queue[i]].vl - arr[queue[j]][queue[i]]);

	//记录关键定点于mark数组
	for (int i = 0; i < size; i++)
	{
		if (arrNode[i].ve == arrNode[i].vl)
		{
			mark[i] = 1;
		}
	}

	printf("各个定点的Ve和Vl:\n");
	for (int i = 0; i < size; i++)
	{
		printf("V%d : ", arrNode[i]);
		printf("ve = %d\tvl = %d\n", arrNode[i].ve, arrNode[i].vl);
	}
	printf("\n各个活动及其e和l:\n");
	for (int i = 0; i < size; i++)
	{
		struct Node* p = arrNode[i].next;
		while (p)
		{
			int l = arrNode[p->index].vl - arr[i][p->index];
			printf("(V%d --> V%d)\t e = %d\t l = %d", i, p->index, arrNode[i].ve, l);
			if (arrNode[i].ve == l) // ve == e, 若 e == l 则为关键活动
			{
				label[i][p->index] = 1;
				printf("\t 【此活动为关键活动】");
			}
			printf("\n");

			p = p->next;
		}
	}

	printf("\n\n 关键路径: \n\n");
	stack2[0] = arrNode[queue[front]].index;//先存个拓扑排序第一个顶点的下标
	showCriticalPathByDFS(queue[front]);
}

int main()
{
	int vexnum, arcnum;
	printf("请输入顶点数:\n");
	scanf("%d", &vexnum);
	printf("请输入弧数:\n");
	scanf("%d", &arcnum);
	initMap(vexnum);

	printf("请连接顶点并赋值权重(起点,终点,权重):\n");
	for (int i = 0; i < arcnum; i++)
	{
		int b, e, w;
		scanf("%d,%d,%d", &b, &e, &w);
		addNode(b, e);
		arr[b][e] = w;
	}

	printf("\n邻接表输出:\n");
	//对每个节点遍历它的相邻节点
	traverse(vexnum);
	
	if (topologicalOrder(vexnum))
	{
		printf("\n存在拓扑排序\r\n");
	}
	else
	{
		printf("\n不存在拓扑排序\r\n");
		exit(0);
	}
	//完成拓扑排序和计算ve 
	getKeyRoute(vexnum); //计算关键路径 
	int startNode = queue[front];
	printf("该工程至少完成时间为:%d", arrNode[queue[rear - 1]].vl);//即为拓扑排序最后一个顶点的ve或vl

	return 0;
}
/*
6
7
0,1,3
0,2,2
0,3,1
1,4,1
2,5,1
3,5,4
4,5,1

*/
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咀才

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值