假定一个工程项目由一组子任务构成,子任务之间有的可以并行执行,有的必须在完成了其它一些子任务后才能执行。“任务调度”包括一组子任务、以及每个子任务可以执行所依赖的子任务集。
比如完成一个专业的所有课程学习和毕业设计可以看成一个本科生要完成的一项工程,各门课程可以看成是子任务。有些课程可以同时开设,比如英语和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为该任务开始和完成涉及的交接点编号。关键活动输出的顺序规则是:任务开始的交接点编号小者优先,起点编号相同时,与输入时任务的顺序相反。
输入样例:
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 <bits/stdc++.h>
#define INF 0x3f3f3f3f
#define MAXN 101
using namespace std;
struct Node{
int v; // 顶点
int t; // 时间(权重)
};
int N, M;
vector<Node> G[MAXN]; // 邻接表表示图
stack<int> topOrder; // 逆拓扑堆栈出栈排序
//int e[MAXN]; // 边上活动最早开始时间
//int l[MAXN]; // 边上活动最晚开始时间
int ve[MAXN] = {0}; // 顶点上活动最早开始时间
int vl[MAXN]; // 顶点上活动最晚开始时间
int inDegree[MAXN] = {0}; // 每个结点的入度 为0时入队
bool topologicalSort(){
queue<int> q;
// 先找出所有初始时入度为0的结点 编号从1开始到N
// 邻接表列表顶点u 没有++inDegree[v] 所以会最先入队
for(int i = 1; i <= N; i++){
if(inDegree[i] == 0){
q.push(i);
}
}
// 每次将所有入度为0的结点入栈 拓扑排序
while(!q.empty()){
int u = q.front();
q.pop();
topOrder.push(u);
for(int i = 0; i < G[u].size(); i++){
// u的i号后继结点编号为v
int v = G[u][i].v;
//入度减为0加入拓扑排序
if(--inDegree[v] == 0)
q.push(v);
// 用ve[u]来更新u的所有后继结点v
if(ve[u] + G[u][i].t > ve[v])
ve[v] = ve[u] + G[u][i].t;
}
}
// 加入拓扑序列N个顶点 与给定顶点数相同 则给定图为有向无环图 排序成功
if(topOrder.size() == N) return true;
else return false;
}
void criticalPath(){
if(topologicalSort() == false){
cout << 0 << endl;
return;
}
// 数据集存在随机序号 N号编号不一定是结束任务(汇点)
// 需要寻找拓扑序列最后一个结点 即结束任务结点 以找到总工程时间
// distance(int&, int*) 该函数第一个参数取容器地址 第二个参数指向需要查找元素的地址
// max_element() 函数返回容器最大值地址
int u = distance(ve, max_element(ve, ve + MAXN));
// vl数组初始化 初始值为汇点的ve值
fill(vl, vl + MAXN, ve[u]);
// 打印第一行 整个工程项目需要的时间
cout << ve[u] << endl;
// 直接使用 toporder 出栈即为逆拓扑序列 求解 v1 数组
while(!topOrder.empty()){
// 栈顶元素为u
int u = topOrder.top();
topOrder.pop();
for(int i = 0; i < G[u].size(); i++){
// u的后继结点编号v
int v = G[u][i].v;
// 用u的所有后继结点v的vl值来更新 vl[u]
if(vl[v] - G[u][i].t < vl[u])
vl[u] = vl[v] - G[u][i].t;
}
}
// 遍历邻接表的所有边 计算活动的最早开始时间e和最迟开始时间l
for(int u = 1; u < N; ++u){
// 同起点u时 以相反顺序输出
for(int i = G[u].size() - 1; i >= 0; i--){
// 得到u后继结点v 与u->v时间t(边权)
int v = G[u][i].v, t = G[u][i].t;
// 活动的最早开始时间(持续时间)e 和结点u最迟开始时间(机动时间)l
// 活动即u->v最早开始时间 没有拖延直接赋值
// 后继结点v的最迟开始时间逆推 减去时间t可得u的最迟开始时间
int e = ve[u], l = vl[v] - t;
// 如果 e==l 没有拖延时间 说明活动u->v是关键活动
if(e == l) cout << u << "->" << v << endl;
}
}
}
main(){
cin >> N >> M;
Node tmp;
int u, v, time;
while(cin >> u >> v >> time){
tmp.v = v;
tmp.t = time;
// 有向图只添加单向边
G[u].push_back(tmp);
// 对u的后继结点v增加入度
++inDegree[v];
}
criticalPath();
}
#include<bits/stdc++.h>
#define INF 0x3ffffff
#define MAXN 101
using namespace std;
int G[MAXN][MAXN];
int n,m,Max;
int in[MAXN];
int ear[MAXN];
int late[MAXN];
int route[MAXN][MAXN];
int pri[MAXN][MAXN];
int cnt[MAXN];
stack<int> Turn;
void init(){
for(int i = 1;i <= n;i++){
for(int j = 1;j <= n;j++){
G[i][j] = -INF;
}
ear[i] = -INF;
}
Max = -INF;
}
bool topologicalsort(){
stack<int> topOrder;
for(int i = 1;i <= n;i++)
if(in[i] == 0){
topOrder.push(i);
ear[i] = 0;
}
while(!topOrder.empty()){
int index = topOrder.top();
topOrder.pop();
Turn.push(index);
for(int i = 1;i <= n;i++)
if(G[index][i] != -INF){
in[i]--;
if(in[i] == 0) topOrder.push(i);
if(ear[index] + G[index][i] > ear[i]) ear[i] = ear[index] + G[index][i];
if(Max < ear[i]) Max = ear[i];
}
}
for(int i = 1;i <= n;i++)
if(in[i] > 0)return false;
return true;
}
void sort_turn(){
for(int i = 1;i <= n;i++)
for(int j = 0;j < cnt[i];j++)
for(int k = j + 1;k < cnt[i];k++)
if(pri[i][route[i][k]] > pri[i][route[i][j]]){
int t = route[i][j];
route[i][j] = route[i][k];
route[i][k] = t;
}
}
void Criticalpath(){
for(int i = 1;i <= n;i++)
late[i] = Max;
while(!Turn.empty()){
int index = Turn.top();
Turn.pop();
for(int i = 1;i <= n;i++)
if(G[i][index] != -INF){
if(late[index] - G[i][index] <= late[i]){
late[i] = late[index] - G[i][index];
if(late[i] == ear[i]){
route[i][cnt[i]++] = index;
}
}
}
}
sort_turn();
cout << Max <<endl;
for(int i = 1; i <= n; i++)
if(ear[i] == late[i]){
for(int j = 0;j < cnt[i];j++)
cout << i << "->" << route[i][j] << endl;
}
}
int main(){
cin>>n>>m;
init();
for(int i = 1; i <= m; i++)
{
int st,ed,val;
cin>>st>>ed>>val;
G[st][ed] = val;
pri[st][ed] = i;
in[ed]++;
}
if(topologicalsort()) Criticalpath();
else cout << 0 << endl;
}
向量vector增加元素push_back(),队列queue和栈stack增加元素用函数push()。
if(--inDegree[v] == 0)
q.push(v);
遍历邻接点顶点u的邻接点v,判断邻接点c入度是否为0使v入队的代码,与下面的更新ve最早发生时间代码为平行判断关系。
即如果入度为零则加入队列,如果u的最早发生时间加上权重可以取更大值,则更新后继结点v时间,两者达成各自条件可以分别执行代码。
if(ve[u] + G[u][i].t > ve[v])
ve[v] = ve[u] + G[u][i].t ;
拓扑排序更新后继结点时间时,边权G[u][i].t的行下标是 i 而不是 v。此处遍历的是邻接表顶点u的子结点,子结点的下标从 0 到 G[u].size() 。而前面 v = G[u][i].v 结点v是实际工程的编号,编号v可能超过 0 到 G[u].size() 的下标范围,所以编号 v 与下标 i 不能当成一个变量。
在逆拓扑排序更新最小值,最迟发生时间时,边权G[u][i].t道理相同,下标为 i 而不是 v。
while(!topOrder.empty())
无论是队列非空循环还是堆栈,循环判断条件使用函数都是empty(),而不是size()。