写这篇的时候,因为网络波动,导致写了5000+的文章没了,心态崩了,还是坚持写了
如有不对,不吝赐教
下面进入正题:
假定一个工程项目由一组子任务构成,子任务之间有的可以并行执行,有的必须在完成了其它一些子任务后才能执行。“任务调度”包括一组子任务、以及每个子任务可以执行所依赖的子任务集。
比如完成一个专业的所有课程学习和毕业设计可以看成一个本科生要完成的一项工程,各门课程可以看成是子任务。有些课程可以同时开设,比如英语和C程序设计,它们没有必须先修哪门的约束;有些课程则不可以同时开设,因为它们有先后的依赖关系,比如C程序设计和数据结构两门课,必须先学习前者。
但是需要注意的是,对一组子任务,并不是任意的任务调度都是一个可行的方案。比如方案中存在“子任务A依赖于子任务B,子任务B依赖于子任务C,子任务C又依赖于子任务A”,那么这三个任务哪个都不能先执行,这就是一个不可行的方案。
任务调度问题中,如果还给出了完成每个子任务需要的时间,则我们可以算出完成整个工程需要的最短时间。在这些子任务中,有些任务即使推迟几天完成,也不会影响全局的工期;但是有些任务必须准时完成,否则整个项目的工期就要因此延误,这种任务就叫“关键活动”。
请编写程序判定一个给定的工程项目的任务调度是否可行;如果该调度方案可行,则计算完成整个工程项目需要的最短时间,并输出所有的关键活动。
输入格式:
输入第1行给出两个正整数N(≤100)和M,其中N是任务交接点(即衔接相互依赖的两个子任务的节点,例如:若任务2要在任务1完成后才开始,则两任务之间必有一个交接点)的数量。交接点按1N编号,M是子任务的数量,依次编号为1M。随后M行,每行给出了3个正整数,分别是该任务开始和完成涉及的交接点编号以及该任务所需的时间,整数间用空格分隔。
输出格式:
如果任务调度不可行,则输出0;否则第1行输出完成整个工程项目需要的时间,第2行开始输出所有关键活动,每个关键活动占一行,按格式“V->W”输出,其中V和W为该任务开始和完成涉及的交接点编号。关键活动输出的顺序规则是:任务开始的交接点编号小者优先,起点编号相同时,与输入时任务的顺序相反。
输入样例:
7 8
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
从这个题目的名字就可以看出我们应该如何去做这道题目了,关键就是如何去实现它
下面上我的代码,在逐步解释
#include<stdio.h>
#include<stdbool.h>
int main(void)
{
int N,M; //任务数目
int i,j;
int task1,task2,time; //输入时的两个任务以及第一个任务所需时间
fscanf(stdin,"%d %d",&N,&M);
int graph[N+1][N+1]; //任务图
int dependence[N+1],re_dependence[N+1]; //顺向依赖和逆向依赖的任务数量
int ae[N+1],al[N+1]; //一项活动最先开始的时间 和 一项活动最晚开始的时间
bool vis[N+1]; //该任务是否已经完成过了
bool flag=true; //表示该轮中有任务完成
int count; //当前完成的任务的数量
int s[N+1][N+1]; //表示输入的边的顺序
int queue[N]; //表示关键路径某个任务中下一个任务编号
int tail; //表示queue中最后一个元素的位置
for(i=0;i<=N;i++){
for(j=0;j<=N;j++){
graph[i][j]=-1; //初始化表
s[i][j]=-1; //初始化输入
}
dependence[i]=0;
re_dependence[i]=0; //初始化依赖关系
ae[i]=0;
vis[i]=false;
queue[i]=-1;
}
for(i=0;i<M;i++){
fscanf(stdin,"%d %d %d",&task1,&task2,&time);
graph[task1][task2]=graph[task2][task1]=time; //填充图
s[task1][task2]=i; //填入顺序
dependence[task2]++; //task2在正向顺序中依赖task1
re_dependence[task1]++; //task1在逆向顺序中依赖task2
}
count=0;
while(flag&&count!=N){
i=1;
while(i<=N&&(dependence[i]||vis[i]))
i++; //找到依赖的任务都完成并且没有被添加到已完成任务表中的任务
if(i<=N){
j=1;
while(j<=N){
if(-1!=graph[i][j]&&!vis[j]){
dependence[j]--;
if(ae[j]<ae[i]+graph[i][j]) //选择最长的时间线路
ae[j]=ae[i]+graph[i][j];
} //说明两个任务之间有依赖关系
j++;
}
vis[i]=true;
flag=1;
count++;
if(time<ae[i])
time=ae[i]; //记录下任务可以完成的最早时间
}
else
flag=0;
}
for(i=0;i<=N;i++){
al[i]=time; //初始化一项任务的最晚开始时间
vis[i]=false;
}
count=0;
while(flag&&count!=N){
i=1;
while(i<=N&&(re_dependence[i]||vis[i]))
i++; //找到依赖的任务都完成并且没有被添加到已完成任务表中的任务
if(i<=N){
j=1;
while(j<=N){
if(-1!=graph[i][j]&&!vis[j]){
re_dependence[j]--;
if(al[j]>al[i]-graph[i][j]) //选择最长的时间线路
al[j]=al[i]-graph[i][j];
} //说明两个任务之间有依赖关系
j++;
}
vis[i]=true;
flag=1;
count++;
}
else
flag=0;
}
if(!flag)
printf("0\n");
else{
printf("%d\n",time);
for(i=1;i<=N;i++){
if(ae[i]!=al[i])
continue; //说明任务i不在关键路径上
tail=0;
for(j=1;j<=N;j++){
if(-1!=graph[i][j]&&ae[j]==al[j]&&ae[i]+graph[i][j]==ae[j]&&j!=i)
queue[tail++]=j;
} //找到任务i在关键路径上的下一个任务
int k;
for(j=0;j<tail;j++)
for(k=0;k<tail;k++){
int temp;
if(s[i][queue[k]]<s[i][queue[j]]){
temp=queue[k];
queue[k]=queue[j];
queue[j]=temp;
}
} //冒泡排序 将输入顺序在后的排到前面
for(j=0;j<tail;j++)
printf("%d->%d\n",i,queue[j]);
}
}
return 0;
}
1.先是存储数据的几个变量
int graph[N+1][N+1]; //任务图
int dependence[N+1],re_dependence[N+1]; //顺向依赖和逆向依赖的任务数量
int ae[N+1],al[N+1]; //一项活动最先开始的时间 和 一项活动最晚开始的时间
bool vis[N+1]; //该任务是否已经完成过了
bool flag=true; //表示该轮中有任务完成
int count; //当前完成的任务的数量
int s[N+1][N+1]; //表示输入的边的顺序
int queue[N]; //表示关键路径某个任务中下一个任务编号
int tail; //表示queue中最后一个元素的位置
我使用邻接矩阵来存储数据(如果想要节省空间,可以使用邻接表,不过操作稍稍复杂一点,一节访问的平均时间复杂度变为O(M\N))。
然后就是dependence和re_dependence两个数组,这是用来计算关键路径的辅助变量,从前和从后来计算时间。
s是用来记录下来输入边的顺序(为了满足后面那个鬼畜的输出)。
queue数组是在输出的时候记录该任务的下一个任务的编号。
2.然后是部分数据的初始化:
for(j=0;j<=N;j++){
graph[i][j]=-1; //初始化表
s[i][j]=-1; //初始化输入
}
这里使用-1来标记没有两任务之间没有关系,这也是为什么后面把关键路径的有向图化为无向图的原因。
for(i=0;i<M;i++){
fscanf(stdin,"%d %d %d",&task1,&task2,&time);
graph[task1][task2]=graph[task2][task1]=time; //填充图
s[task1][task2]=i; //填入顺序
dependence[task2]++; //task2在正向顺序中依赖task1
re_dependence[task1]++; //task1在逆向顺序中依赖task2
}
严格说,因为关键路径应该是有向图,这里对边的填充应该有正负,但是我为了简单,就把它简化成了无向图。
3.然后是计算最早完成任务时间和最迟完成任务的时间
count=0;
while(flag&&count!=N){
i=1;
while(i<=N&&(dependence[i]||vis[i]))
i++; //找到依赖的任务都完成并且没有被添加到已完成任务表中的任务
if(i<=N){
j=1;
while(j<=N){
if(-1!=graph[i][j]&&!vis[j]){
dependence[j]--;
if(ae[j]<ae[i]+graph[i][j]) //选择最长的时间线路
ae[j]=ae[i]+graph[i][j];
} //说明两个任务之间有依赖关系
j++;
}
vis[i]=true;
flag=1;
count++;
if(time<ae[i])
time=ae[i]; //记录下任务可以完成的最早时间
}
else
flag=0;
}
这相当与从第一个入度为0的节点开始进行BFS。
先是最外层的判断开始
while(flag&&count!=N)
这里flag的取值由该轮循环中是否有入度为0的任务决定,这很好理解,因为没了入度为0的任务只能说明两种情况:任务链成环或者是所有任务都已经完成。而count!=N则判断第二种情况,因此如果flag=0就说明了有环的存在,那么显然没有关键活。
然后就是每个任务最早开始时间的更新:
if(ae[j]<ae[i]+graph[i][j]) //选择最长的时间线路
ae[j]=ae[i]+graph[i][j];
这里要选择时间最长的一次(原因显而易见,如果A依赖B和C,C在5分钟结束,B在10分钟结束,但是我们先检验B,然后在检验C,如果没有这个比较,那么时间就选取了5分钟,这显然不对)。
还有就是time的改变,和上面一样,我们得取较大的值。
if(time<ae[i])
time=ae[i]; //记录下任务可以完成的最早时间
对于求最迟的开工时间,只要将所有时间初始化为time,然后反向进行相同操作就可以了。
4.满足鬼畜的输出条件:
注意这个输出条件:任务开始的交接点编号小者优先,起点编号相同时,与输入时任务的顺序相反。
这句话读起来特别的拗口,是这样理解的:在关键路径上的任务都有一个编号(就是输入中的1到N),从最小的编号开始输出,后输入的边先输出(也就是我们存储边的顺序的s数组中的值更大的先输出)。
所以我们得先从1->N进行检查
if(ae[i]!=al[i])
这就是关键路径的精髓,只有最早开始的时间和最晚开始的时间的任务才是关键路径,简单理解就是:当一个任务没有选择时间的余地,那么这个任务肯定在关键路径上。
然后就是将里面的边先选出来
for(j=1;j<=N;j++){
if(-1!=graph[i][j]&&ae[j]==al[j]&&ae[i]+graph[i][j]==ae[j]&&j!=i)
queue[tail++]=j;
}
条件的判断有一下几点:首先这两个任务之间得有关系(也就是边有权值),然后就是你这个任务得在关键路径上,要不然咋叫关键路径上的边,然后就是这条边得是关键路径上的边:
ae[i]+graph[i][j]==ae[j]
这个关系显而易见
在然后就是将这些边按照输入的逆序进行排序:
for(j=0;j<tail;j++)
for(k=0;k<tail;k++){
int temp;
if(s[i][queue[k]]<s[i][queue[j]]){
temp=queue[k];
queue[k]=queue[j];
queue[j]=temp;
}
} //冒泡排序 将输入顺序在后的排到前面
这里因为数据小,所以直接冒泡排序了其实是因为懒。
然后就可以输出了。
下面给出测试结果和部分测试样例,部分样例来源别人blog,但好像找不到网址了,如有发现,望与我联系:
这个图的输入顺序应该是:
4 3
1 4 5
1 2 5
2 3 5
这个图要注意圆圈中的情况