【问题描述】给定一个项目,项目的各个子任务的完成时间以及子任务之间的依赖关系已给出,采用有向图表示。请编写程序判定一个给定的工程项目的任务调度是否可行;如果该调度方案可行,则计算完成整个工程项目需要的最短时间,并输出所有的关键活动。
【输入形式】
7 8 //结点数(结点编号从1开始连续编码),边的数量 1 2 4 //每个活动的起点和终点,活动完成所需时间 1 3 3 2 4 5 3 4 3 4 5 1 4 6 6 5 7 5 6 7 2
【输出形式】
如果任务可调度,输出形式如下:
17 //完成整个工程项目需要的最短时间 1 2 //每一条关键路径 2 4 4 6 6 7 如果任务不可调度,则输出: 0
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
//
#define MAX_VERTEX_NUM 100 // 最大顶点数
// 定义邻接表结点
typedef struct ArcNode {
int adjvex; // 弧头结点的位置
int weight; // 边的权重,即活动所需时间
struct ArcNode* nextarc; // 指向下一条弧的指针
} ArcNode;
// 定义顶点表结点
typedef struct VNode {
int indegree; // 结点入度
int earliest; // 该活动最早开始时间
int latest; // 该活动最晚开始时间
ArcNode* firstarc; // 指向第一条依附该结点的弧
} VNode, AdjList[MAX_VERTEX_NUM];
// 定义图结构
typedef struct {
AdjList vertices; // 邻接表
int vexnum, arcnum; // 图的顶点数和弧数
} ALGraph;
// 初始化图
void InitGraph(ALGraph* G, int n) {
G->vexnum = n;
G->arcnum = 0;
for (int i = 1; i <= n; i++) {
G->vertices[i].indegree = 0;
G->vertices[i].earliest = 0;
G->vertices[i].latest = INT_MAX; // 初始化最晚开始时间为无穷大
G->vertices[i].firstarc = NULL;
}
}
// 插入边(使用头插法)
void InsertArc(ALGraph* G, int tail, int head, int weight) {
ArcNode* arcNode = (ArcNode*)malloc(sizeof(ArcNode));
arcNode->adjvex = head;
arcNode->weight = weight;
arcNode->nextarc = G->vertices[tail].firstarc;
G->vertices[tail].firstarc = arcNode;
G->vertices[head].indegree++; // 增加终点的入度
G->arcnum++;
}
// 拓扑排序
int TopologicalSort(ALGraph* G, int* etv) {
int count = 0; // 记录已经输出的顶点数
int stack[MAX_VERTEX_NUM]; // 存储入度为0的顶点
int top = -1; // 栈顶指针
for (int i = 1; i <= G->vexnum; i++) {
if (G->vertices[i].indegree == 0) { // 将入度为0的顶点入栈
stack[++top] = i;
}
}
while (top != -1) {
int v = stack[top--]; // 出栈一个顶点
count++;
ArcNode* p = G->vertices[v].firstarc;
while (p != NULL) {
int w = p->adjvex; // 弧头结点
if (--G->vertices[w].indegree == 0) { // 入度减1后变为0的顶点入栈
stack[++top] = w;
}
if (etv[v] + p->weight > etv[w]) {
etv[w] = etv[v] + p->weight; // 更新最早开始时间
}
p = p->nextarc;
}
}
if (count != G->vexnum) { // 如果存在环路,说明任务不可调度
return 0;
} else {
return 1;
}
}
// 关键路径计算
void CriticalPath(ALGraph* G) {
int etv[MAX_VERTEX_NUM]; // 事件最早发生时间
int ltv[MAX_VERTEX_NUM]; // 事件最晚发生时间
for (int i = 1; i <= G->vexnum; i++) {
etv[i] = 0;
}
if (!TopologicalSort(G, etv)) { // 执行拓扑排序,判断任务是否可调度
printf("0\n"); // 任务不可调度
return;
}
// 初始化ltv数组
for (int i = 1; i <= G->vexnum; i++) {
ltv[i] = etv[G->vexnum];
}
int stack[MAX_VERTEX_NUM]; // 存储关键路径上的顶点
int top = -1; // 栈顶指针
for (int i = G->vexnum; i >= 1; i--) {
ArcNode* p = G->vertices[i].firstarc;
while (p != NULL) {
int k = p->adjvex; // 弧头结点
if (ltv[k] - p->weight < ltv[i]) {
ltv[i] = ltv[k] - p->weight; // 更新最晚开始时间
}
p = p->nextarc;
}
if (etv[i] == ltv[i]) { // 找到关键活动,将顶点入栈
stack[++top] = i;
}
}
printf("%d\n", etv[G->vexnum]); // 输出完成整个工程项目所需最短时间
while (top != -1) { // 输出关键活动
int v = stack[top--];
ArcNode* p = G->vertices[v].firstarc;
while (p != NULL) {
int k = p->adjvex; // 弧头结点
if (etv[v] == ltv[k] - p->weight) {
printf("%d %d\n", v, k); // 输出关键路径上的边
}
p = p->nextarc;
}
}
}
int main() {
int n, m; // 结点数和边的数量
scanf("%d %d", &n, &m);
ALGraph G;
InitGraph(&G, n);
for (int i = 0; i < m; i++) {
int tail, head, weight;
scanf("%d %d %d", &tail, &head, &weight);
InsertArc(&G, tail, head, weight);
}
CriticalPath(&G);
return 0;
}
以上代码使用了关键路径算法(Critical Path Method,CPM)来计算项目的关键路径。关键路径算法基于拓扑排序和动态规划的思想。
具体算法步骤如下:
-
构建有向无环图:根据输入的边信息,构建有向无环图(DAG),其中每个结点表示一个任务,边表示任务之间的依赖关系。
-
拓扑排序:通过拓扑排序,确定每个任务的最早开始时间(Earliest Start Time,etv)。拓扑排序从入度为0的结点开始,逐层移除入度为0的结点,并更新其后继结点的最早开始时间。
-
计算最晚开始时间:逆向遍历图,计算每个任务的最晚开始时间(Latest Start Time,ltv)。从最后一个任务开始,根据后继任务的最晚开始时间和边的权重,逐层向前计算每个任务的最晚开始时间。
-
判断可调度性:如果拓扑排序完成后,仍存在入度不为0的结点,说明图中存在环路,任务不可调度。
-
输出关键路径:根据最早开始时间和最晚开始时间的比较,找到关键活动(即etv[i] == ltv[i]的任务),输出关键路径上的边。
该算法的时间复杂度为O(n + m),其中n为顶点数,m为边数。
拓扑排序的方法:
1.在有向图中选择一个没有前驱的顶点输出;
2.从图中删除该顶点和所以以它为尾的弧。
重复以上两步,直至全部顶点均已输出。
注:
拓扑排序是一种对有向无环图(DAG)进行排序的算法。它通过分析图中各个结点之间的依赖关系,确定结点的执行顺序,使得所有的依赖关系得以满足。
拓扑排序主要用来解决以下问题:
-
任务调度:在项目管理中,任务之间存在不同的先后关系和依赖关系,拓扑排序可以确定任务的执行顺序,从而合理安排任务的时间表,保证项目按计划进行。
-
课程安排:在学校或大学的课程安排中,课程之间存在先修课程和依赖关系,拓扑排序可以确定学习课程的顺序,避免学生学习未掌握的前置知识。
-
编译顺序:在编译程序时,源代码文件之间可能存在相互引用和依赖关系,拓扑排序可以确定编译的顺序,保证依赖的文件先编译,避免出现错误。
-
任务优先级:在任务管理中,不同任务可能具有不同的优先级和依赖关系,拓扑排序可以确定任务的优先级顺序,帮助选择最重要或最紧急的任务。
总而言之,拓扑排序适用于有向无环图,并且在任务之间存在依赖关系的场景中,可以帮助确定结点的执行顺序和解决相关问题。