【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>

int arr[9][9]; //存放weight的二维数组
int mark[99] = {
    
     0 };//记录下表对应顶点是否为关键顶点

int label[9][9] = {
    
     0 };//用于记录两个顶点之间是否存在关键路径
int totalT = 0;//记录关键路径总时长,即工程完成至少时间
int cnt = 0;//用于记录某一条关键路径的顶点数。

//声明结构体:结点 
struct Node {
    
    
	int index; //编号 
	int inDegree; //入度 
	int ve; //最早的发生时间,默认为0 
	int vl; //最晚的发生时间,默认为无穷大 
	struct Node* next; //下一个结点的指针 
};

int stack[100]; //用栈辅助实现拓扑排序,用数组实现栈
int top = 0; //栈的头元素的index 
int stack2[9] = {
    
     -1 };//用于用于记录路径上关键顶点的顺序(倒序)
int top2 = 0;

int queue[100]; //一个用数组实现的队列 
int front = 0; //队列的头 
int rear = 0; //队列的尾 

//存储方式:邻接表
//用一个数组存放首元素。每个首元素后面用指针连接各个结点 
struct Node arrNode[9];

//邻接表存储各个顶点(之间的关系)
//头插法,得到的结果的次序是反的
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++; //入度 + 1
	temp->inDegree = arrNode[nodeIndex].inDegree;
}

//遍历一个结点以及它的所有相连的结点 
void traverse(int size)
{
    
    
	for (int i = 0; i < size; i++)
	{
    
    
		struct Node* node = &arrNode[i];
		while (node != NULL)
		{
    
    
			printf("%d --> ", node->index);
			node->ve = 0;
			node->vl = 32676;//顺便初始化每个顶点的ve和vl
			node = node->next;
		}
		printf("∧\r\n");
	}
}

int max(int a, int b) //求两个数中的最大值 
{
    
    
	return a > b ? a : b; //三元运算符
}

int min(int a, int b) //求两个数中的最小值
{
    
    
	return a < b ? a : b; //三元运算符
}

//用二维数组储存图的weight,此处为权重值的初始化
void initMap(int size)
{
    
    
	for (int i = 0; i < size; i++)
	{
    
    
		for (int j = 0; j < size; j++)
		{
    
    
			arr[i][j] = 0;
		}
	}
}

//栈辅助实现的拓扑排序 
int TopologicalOrderByStack(int size) 
{
    
    
	int count = 0; //用来计数
	int index = 0; //输出的结点的编号 
	int i = 0; //循环变量 

	//1、遍历所有结点,寻找入度为0的结点,并把编号存放在stack中 
	for (i = 0; i < size; i++)
	{
    
    

		if (arrNode[i].inDegree == 0)
		{
    
    
			stack[top] = i; //把结点的编号存放到stack中 
			top++; //top + 1 
		}
	}

	//2、弹出栈中的结点,输出结点编号。同时让该结点的下一级结点的入度-1 
	//3、循环,直到栈中的结点为0,即top == 0 
	while (top > 0)
	{
    
    
		top--; //top的位置没有内容,所以要先 - 1 
		index = stack[top]; //得到存放在stack中的编号 
		queue[rear] = index; //把编号存放到queue中 
		rear++; //存放数据后,队列的rear + 1 
		count++; //计数 + 1 

		//从arrNode中获得结点的指针 
		struct Node* parentNode = &arrNode[index]; //得到数组arrNode中的指定节点 
		struct Node* node = parentNode->next; //得到arrNode中的指定节点的子节点

		//遍历
		while (node != NULL) //如果子节点不是NULL就循环 
		{
    
    
			int sonIndex = node->index; //子节点的index 

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

				//如果结点的inDegree == 0
				if (arrNode[sonIndex].inDegree == 0)
				{
    
    
					//把结点的index (也就是sonIndex) 加入到stack中 
					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");//删除最后多余的-->并换行
	}
	else 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) //关键路径 
{
    
    
	//最终结点的最晚发生时间就是它的最早发生时间
	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)
			{
    
    
				//注意,这是一个有向图。所以不能用arr[queue[i]][queue[j]],
				//arr[queue[i]][queue[j]]的值一定是0,用arr[queue[j]][queue[i]] 
				arrNode[queue[j]].vl = min(arrNode[queue[j]].vl, arrNode[queue[i]].vl - arr[queue[j]][queue[i]]);
			}
		}
	}

	//记录关键顶点(下标对应)
	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   |   vl = %d\n", arrNode[i].ve, arrNode[i].vl);
	}
	printf("\r\n\r\n各个活动及其e和l:\r\n");
	//通过邻接表的下标的顺序遍历,查找关键路径
	for (int i = 0; i < size; i++)
	{
    
    
		struct Node* p = arrNode[i].next;
		while (p)
		{
    
    
			printf("(v%d-->v%d)\t e = %d   |   l = %d", i, p->index, arrNode[i].ve, arrNode[p->index].vl - arr[i][p->index]);
			if (arrNode[i].ve == arrNode[p->index].vl - arr[i][p->index])    //活动的e = l即为关键活动
			{
    
    
				label[i][p->index] = 1;
				printf("  此活动为关键活动");
			}
			printf("\n");
			
			p = p->next;
		}
	}
	printf("\r\n\r\n关键路径:\r\n");
	stack2[0] = arrNode[queue[front]].index;//先入栈存储拓扑排序第一个顶点的下标
	//因为下面的操作是从第一个顶点的关键邻接顶点开始DFS的
	showCriticalPathByDFS(queue[front]);
}

int main(void)
{
    
    
	int vexnum, arcnum;
	printf("请输入顶点数:\n");
	scanf("%d", &vexnum);
	printf("请输入弧数:\n");
	scanf("%d", &arcnum);
	initMap(vexnum); //初始化图 (weight)
	//结点信息初始化 
	for (int i = 0; i < vexnum; i++)
	{
    
    
		arrNode[i].index = i;
		arrNode[i].inDegree = 0;
		arrNode[i].next = NULL;
	}
	
	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 (TopologicalOrderByStack(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;
}



猜你喜欢

转载自blog.csdn.net/qq_40463117/article/details/112472674