7-11 关键活动
假定一个工程项目由一组子任务构成,子任务之间有的可以并行执行,有的必须在完成了其它一些子任务后才能执行。“任务调度”包括一组子任务、以及每个子任务可以执行所依赖的子任务集。
比如完成一个专业的所有课程学习和毕业设计可以看成一个本科生要完成的一项工程,各门课程可以看成是子任务。有些课程可以同时开设,比如英语和C程序设计,它们没有必须先修哪门的约束;有些课程则不可以同时开设,因为它们有先后的依赖关系,比如C程序设计和数据结构两门课,必须先学习前者。
但是需要注意的是,对一组子任务,并不是任意的任务调度都是一个可行的方案。比如方案中存在“子任务A依赖于子任务B,子任务B依赖于子任务C,子任务C又依赖于子任务A”,那么这三个任务哪个都不能先执行,这就是一个不可行的方案。
任务调度问题中,如果还给出了完成每个子任务需要的时间,则我们可以算出完成整个工程需要的最短时间。在这些子任务中,有些任务即使推迟几天完成,也不会影响全局的工期;但是有些任务必须准时完成,否则整个项目的工期就要因此延误,这种任务就叫“关键活动”。
请编写程序判定一个给定的工程项目的任务调度是否可行;如果该调度方案可行,则计算完成整个工程项目需要的最短时间,并输出所有的关键活动。
输入格式:
输入第1行给出两个正整数N(≤100)和M,其中N是任务交接点(即衔接相互依赖的两个子任务的节点,例如:若任务2要在任务1完成后才开始,则两任务之间必有一个交接点)的数量。交接点按1~N编号,M是子任务的数量,依次编号为1~M。随后M行,每行给出了3个正整数,分别是该任务开始和完成涉及的交接点编号以及该任务所需的时间,整数间用空格分隔。
输出格式:
如果任务调度不可行,则输出0;否则第1行输出完成整个工程项目需要的时间,第2行开始输出所有关键活动,每个关键活动占一行,按格式“V->W”输出,其中V和W为该任务开始和完成涉及的交接点编号。关键活动输出的顺序规则是:任务开始的交接点编号小者优先,起点编号相同时,与输入时任务的顺序相反。
输入样例:
1 2 3 4 5 6 7 8 9 | 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 |
输出样例:
1 2 3 4 5 | 17 1->2 2->4 4->6 6->7 |
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | #include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=2e2; int in [N],ve[N],vl[N]; //in为点的入度,ve为事件最早开始时间,vl为事件最迟开始时间 vector<pair< int , int > >e[N]; //pair为后继结点,与后继结点的边权 stack< int >toporder; //按拓扑序列入栈,出栈即逆拓扑序列 bool topologicalsort( int n){ //拓扑排序,同时求Ve数组 queue< int >q; for ( int i=1;i<=n;i++){ //遍历所有结点,令入度为零的结点入队 if (! in [i])q.push(i); } while (!q.empty()){ int u=q.front(); q.pop();toporder.push(u); for ( int i=0;i<e[u].size();i++){ //遍历u所有后继结点 int v=e[u][i].first,w=e[u][i].second; //u的i号后继节点编号为v,边权为w if (-- in [v]==0)q.push(v); //v点前面的所有点都进入后,v点才可以进入(拓扑排序的原理) if (ve[u]+w>ve[v])ve[v]=ve[u]+w; //用ve[u]来跟新u的所有后继结点v } } if (toporder.size()==n) return true ; //判断是否存在回路 else return false ; } void criticalpath( int n){ memset(ve,0, sizeof (ve)); //ve数组初始化 if (!topologicalsort(n)){ cout<<0; //不是有向无环图,打印0 } int mx=-1; //关键路径长度 for ( int i=1;i<=n;i++){ if (ve[i]>mx)mx=ve[i]; }cout<<mx<<endl; fill(vl+1,vl+n+1,mx); //vl数组初始化 while (!toporder.empty()){ int u=toporder.top(); //按拓扑序列入栈,出栈即逆拓扑序列 toporder.pop(); for ( int i=0;i<e[u].size();i++){ int v=e[u][i].first,w=e[u][i].second; //u的i号后继节点编号为v,边权为w if (vl[v]-w<vl[u])vl[u]=vl[v]-w; //同时向前更新vl数组 } } for ( int i=1;i<=n;i++){ //任务开始的交接点编号小者优先 for ( int j=e[i].size()-1;j>=0;j--){ //起点编号相同时,与输入时任务的顺序相反。 int v=e[i][j].first,w=e[i][j].second; //i的j号后继节点编号为v,边权为w int e=ve[i],l=vl[v]-w; //遍历领接表的所有边,计算活动最早开始时间e与最晚开始时间l if (e==l)cout<<i<< "->" <<v<<endl; } } } int main(){ int n,m,x,y,z; cin>>n>>m; //n个点,m条有向边 while (m--){ cin>>x>>y>>z; //x到y的边权为z的有向边 in [y]++; e[x].push_back({y,z}); } criticalpath(n); return 0; } |
总结:
1、关键路径问题:
AOE网:在一个表示工程的带权有向图中,用顶点表示事件(如V0),用有向边表示活动(如<v0,v1> = a1),边上的权值表示活动的持续时间,称这样的有向图为边表示的活动的网,简称AOE网(activity on edge network)如果该有向图有环,则输出0
源点:
在AOE网中,没有入边的顶点称为源点;如顶点V0
终点:
在AOE网中,没有出边的顶点称为终点;如顶点V3
AOE网的性质:
【1】只有在进入某顶点的活动都已经结束,该顶点所代表的事件才发生;
例如:a1和a2活动都结束了,顶点V2所代表的事件才会发生。
【2】只有在某顶点所代表的事件发生后,从该顶点出发的各活动才开始;
例如:只有顶点V1所代表的事件结束之后,活动a2和a4才会开始。
在AOE网中,所有活动都完成才能到达终点,因此完成整个工程所必须花费的时间(即最短工期)应该为源点到终点的最大路径长度。具有最大路径长度的路径称为关键路径。关键路径上的活动称为关键活动。

一个AOE网的关键路径可能有多条。
2、如果该有向图有环,则输出0;
3、拓扑排序:
拓扑排序对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
4、fill 和 memset的区别

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通