一、问题描述
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;
}