一、问题描述
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
*/