目录
题目描述
问题分析
拓扑排序
关键路径
问题解决
拓扑排序
关键路径
题目描述
说明: AOE 网络是有向无环加权图,其中顶点表示事件,弧表示活动,权表示活动持续的时间,通常可以用来估算工程完成的时间,即图中从开始点到结束点之间最长的路径对应的时间。请完成一个程序,完成下列任务:
1 、计算 AOE 网络对应的拓扑排序。如果排序结果不唯一,请输出按照从小到大的顺序排列的结果。从小到大的顺序就是输入的节点序列顺序(参见下面关于输入格式的说明)。如图1中满足要求的拓扑排序是: a-b-c-d-e-f-g-h-k ,图2中满足要求的拓扑排序是:v1-v3-v5-v2-v6-v4-v7-v8-v9
2 、计算 AOE 网络的关键路径。注意关键路径可能不唯一,要求输出所有的关键路径。同样,按照是按照从小到大的顺序输出。例,如果得到两条关键路径,分别是0-1-3-6-8-9和0-1-3-4-5-8-9,那么先输出后一条路径,因为两条路径中前三个节点相同,而后一条路径第四个节点的编号小。
测试用例的输入输出格式说明:
输入:
节点的个数,边的条数;
各个节点的名称序列
边: < 起点 , 终点 , 权值 > 。说明起点和终点是在各个点在输入序列中的位置,如图1中边 <a,b> 表示为 <0,1,6> 。
输出:
拓扑排序;
关键路径
测试用例0是与图1相对应的,测试用例1是与图2相对应的。
期待的输出时间限制内存限制额外进程
测试用例 1 |
|
| 1秒 | 64M | 0 |
测试用例 2 |
|
| 1秒 | 64M | 0 |
测试用例 3 |
|
| 1秒 | 64M | 0 |
问题分析
这道题可以分成两部分求解。
一是输出拓扑序列,而是输出关键路径。
拓扑排序
首先讨论拓扑排序。
我们需要先找到入度为0的结点,将其输出,接着将其所有边都删去(也就是将与它所连的所有顶点的入度都-1),然后再次寻找入度为0的结点。直到所有顶点都被找到为止。
由以上分析可知,我们需要一个邻接表存储各个顶点所连接的边。在顶点里需要存储入度信息。在边里需要存储所连的另一个顶点及边的权重。
另外我们需要注意,输出顶点的顺序也是有要求的,所以我们需要保证每个顶点所连的边要按输入顺序存储。
关键路径
当查找关键路径时,我们可以借助关键路径上活动的特点,即e==l(e为活动的最早开始时间,l为活动的最晚开始时间)。而e和l的求法如下:
假设我们有一个由j指向k的路径活动,ve为j开始的最早状态,vl为k开始的最晚状态,weight为活动i的权重。则有
e[i]=ve[j],l[i]=vl[k]-weight[i]
由此我们可以找出关键路径。
需要注意的是,题目要求找出所有关键路径。我们可以用回溯法解决。从第一个点开始遍历,找e==l的连接点,顺次往后,当找到最后一个点时证明次路径可以作为一条关键路径。
保存所有关键路径,按顺序输出即可。
问题解决
拓扑排序
首先做一些准备工作。
struct edge //存储边的结构体
{
int to; //存储所连另一顶点的输入下标
int weight; //边的权重
};
struct vec //顶点结构体
{
char id[5]; //顶点名字
int inlink=0; //顶点入度
int linknum=0; //顶点后面所连的边数(即出度)
edge link[300];//顶点所连边构成的数组
int ve=0; //顶点的最早开始时间
int vl; //顶点的最晚开始时间
};
bool cmp(const edge& a,const edge& b) //排序依据函数,按顶点的输入顺序排序
{
return a.to<b.to;
}
vec V[100]; //顶点构成的数组
int n,m; //顶点数和边数
其中weight,ve和vl是求关键路径的时候用的,现在可以先不用关注。
然后进行读入数据处理
void Initial()
{
scanf("%d,%d",&n,&m);
char str[200] = { '\0' }; // 初始化存储整行读入数据的数组
scanf("%s",str);
int len = strlen(str), q = 0, k = 0;
for (int i = 0; i < len; i++)
{
if (str[i] != ',') //如果不是逗号就继续读入
V[k].id[q++] = str[i];
else
{
V[k].id[q] = '\0'; //如果是逗号就跳过,另开一个V[k]存储
k++;
q = 0; //q重新置0(从新名字的第一位开始保存)
}
}
getchar();
for(int i=0;i<m;i++)
{
int a,b,c;
scanf("<%d,%d,%d>",&a,&b,&c); //读入边的数据
getchar();
V[b].inlink++; //b位置的结点入度++
V[a].link[V[a].linknum].to=b; //把b位置存到a位置的结点连接的点的位置数组里
V[a].link[V[a].linknum].weight=c; //保存相应权重
V[a].linknum++; //a所在位置的结点所连接的边数++
}
for(int i=0;i<n;i++)
{
sort(V[i].link, V[i].link + V[i].linknum,cmp); //对a位置处的结点所连的各个结点位置下标从小到大排序,即按输入顺序对其排序
}
}
然后进行拓扑排序
int topu(int n)
{
priority_queue<int, vector<int>,greater<int>> q; //定义优先队列
vector<int> T; //保存拓扑序列
for(int i=0;i<n;i++) //按下标从小到大遍历各个结点
{
if(V[i].inlink==0) //找到入度为0的结点,加入优先队列
{
q.push(i);
}
}
while(!q.empty()) //如果优先队列非空
{
int u=q.top(); //取出队首元素
q.pop(); //弹出队首元素
T.push_back(u); //把它加入拓扑序列
for(int i=0;i<V[u].linknum;i++) //按队首元素存储的下标信息找到结点,
并将它所连的其他节点入度-1
{
int v=V[u].link[i].to;
V[v].inlink--;
if(V[v].inlink==0) 若入度刚-1的结点入度变为0,立即将其加入优先队列
{
q.push(v);
}
if(V[v].ve<V[u].ve+V[u].link[i].weight) V[v].ve=V[u].ve+V[u].link[i].weight;
这句是为了关键路径用的,更新各个结点最早开始时间
}
}
if(T.size()<n) //如果拓扑序列中的顶点数小于总顶点数说明有环,拓扑序列不存在
{
cout<<"NO TOPOLOGICAL PATH"<<endl;
}
else
{
for(int i=0;i<n;i++) //否则拓扑序列存在,按顺序输出vector T中的下标对应的结点名称即可
{
V[i].vl=V[n-1].ve; //这句是为了关键路径,把所有结点的最晚开始时间都先初始化为
最后一个结点的最早开始时间。
if(i>0) cout<<"-";
cout<<V[T[i]].id;
}
cout<<endl;
}
可以先写个main函数看看对错
int main()
{
Initial();
topu(n);
return 0;
}
关键路径
首先定义查找关键路径函数。
vector<vector<string>> paths; // 存储所有的关键路径
void findCriticalPath(int node, vector<string>& path)
{
path.push_back(V[node].id); // 将当前结点加入路径
if (node == n-1) // 达到终点事件
{
paths.push_back(path); // 将完整路径加入结果
}
else
{
for (int i = 0; i < V[node].linknum; i++) //遍历这个点所连的所有边
{
int nextNode = V[node].link[i].to;//记当前边关联到的点为nextNode
int e = V[node].ve; // 当前事件的最早发生时间
int l = V[nextNode].vl - V[node].link[i].weight; // 下一个事件的最迟发生时间减去边的权值
if (e == l) //证明可能在关键路径上
{
findCriticalPath(nextNode, path); // 递归进入下一个事件
}
}
}
path.pop_back(); // 回溯时,将当前结点从路径中移除
}
然后输出关键路径
vector<string> pathing;//定义一个数组用来存储路径结点
void Putout()
{
// 计算事件的最迟发生时间 vl
for (int i = n - 2; i >= 0; i--)
{
int u=T[i]; //一定要注意!!从拓扑序列的倒数第二个元素开始遍历,而不是从V[]的倒数第二个元素
//开始遍历,否则会导致遍历的结点不按连接顺序从后往前,而是按下标大小从后往前
for (int j = 0; j < V[u].linknum; j++)
{
int v = V[u].link[j].to;
V[u].vl = min(V[u].vl, V[v].vl - V[u].link[j].weight); //更新结点的vl,便于关键路
//径结点的计算
}
}
findCriticalPath(0,pathing);
for(int i=0;i<paths.size();i++) //输出关键路径
{
for(int j=0;j<paths[i].size();j++)
{
if(j!=paths[i].size()-1){
cout<<paths[i][j]<<"-";
}
else cout<<paths[i][j]<<endl;
}
}
}
这里一定要注意, 要从拓扑序列数组的倒数第二个元素开始遍历,而不是从V[]的倒数第二个元素开始遍历,否则会导致遍历的结点不按连接顺序从后往前,而是按下标大小从后往前遍历。
因为如果按V里存的下标遍历的话,如果下标大的活动在图中连接很靠前的地方,那么从后往前遍历的时候,它后面连接的比它下标小的点的vl还没来得及更新,所以计算出的vl不是最晚的活动开始时间,可能会偏小。
拿题图2举例
若按下标从倒数第二个往前遍历,那么轮到v6的时候要用v4的最晚开始时间减去v6到v4的权重,但是此时v4的最晚开始时间还未更新,所以计算出的v6的最晚开始时间也是不准确的。
所以我们要用拓扑序列,因为拓扑序列的顺序就是结点连接顺序,从v9的前一个结点开始往前遍历,每次都是当前结点连接的上一个结点。
所以我们可以把上面写的int topu(int n)函数中定义的T拿到全局变量里来,方便关键路径函数使用
最后赋完整代码:
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <cstring>
using namespace std;
struct edge
{
int to;
int weight;
};
struct vec
{
char id[5];
int inlink=0;
int outlink=0;
int linknum=0;
edge link[300];
int ve=0;
int vl;
};
bool cmp(const edge& a,const edge& b)
{
return a.to<b.to;
}
vec V[100];
int n,m;
void Initial()
{
scanf("%d,%d",&n,&m);
char str[200] = { '\0' };
scanf("%s",str);
int len = strlen(str), q = 0, k = 0;
for (int i = 0; i < len; i++)
{
if (str[i] != ',')
V[k].id[q++] = str[i];
else
{
V[k].id[q] = '\0';
k++;
q = 0;
}
}
getchar();
for(int i=0;i<m;i++)
{
int a,b,c;
scanf("<%d,%d,%d>",&a,&b,&c);
getchar();
V[b].inlink++;
V[a].link[V[a].linknum].to=b;
V[a].link[V[a].linknum].weight=c;
V[a].linknum++;
V[a].outlink++;
}
for(int i=0;i<n;i++)
{
sort(V[i].link, V[i].link + V[i].linknum,cmp);
}
}
vector<int> T;
int topu(int n)
{
priority_queue<int, vector<int>,greater<int>> q;
for(int i=0;i<n;i++)
{
if(V[i].inlink==0)
{
q.push(i);
}
}
while(!q.empty())
{
int u=q.top();
q.pop();
T.push_back(u);
for(int i=0;i<V[u].linknum;i++)
{
int v=V[u].link[i].to;
V[v].inlink--;
if(V[v].inlink==0)
{
q.push(v);
}
if(V[v].ve<V[u].ve+V[u].link[i].weight) V[v].ve=V[u].ve+V[u].link[i].weight;
}
}
if(T.size()<n)
{
cout<<"NO TOPOLOGICAL PATH"<<endl;
}
else
{
for(int i=0;i<n;i++)
{
V[i].vl=V[n-1].ve;
if(i>0) cout<<"-";
cout<<V[T[i]].id;
}
cout<<endl;
}
}
vector<vector<string>> paths; // 存储所有的关键路径
void findCriticalPath(int node, vector<string>& path)
{
path.push_back(V[node].id); // 将当前结点加入路径
if (node == n-1) // 达到终点事件
{
paths.push_back(path); // 将完整路径加入结果
}
else
{
for (int i = 0; i < V[node].linknum; i++)
{
int nextNode = V[node].link[i].to;
int e = V[node].ve; // 当前事件的最早发生时间
int l = V[nextNode].vl - V[node].link[i].weight; // 下一个事件的最迟发生时间减去边的权值
if (e == l)
{
findCriticalPath(nextNode, path); // 递归进入下一个事件
}
}
}
path.pop_back(); // 回溯时,将当前结点从路径中移除
}
vector<string> pathing;
void Putout()
{
// 计算事件的最迟发生时间 vl
for (int i = n - 2; i >= 0; i--)
{
int u=T[i];
for (int j = 0; j < V[u].linknum; j++)
{
int v = V[u].link[j].to;
V[u].vl = min(V[u].vl, V[v].vl - V[u].link[j].weight);
}
}
findCriticalPath(0,pathing);
for(int i=0;i<paths.size();i++)
{
for(int j=0;j<paths[i].size();j++)
{
if(j!=paths[i].size()-1){
cout<<paths[i][j]<<"-";
}
else cout<<paths[i][j]<<endl;
}
}
}
int main()
{
Initial();
topu(n);
Putout();
return 0;
}