一、拓扑排序
1. 有向无环图
- 如果一个有向图的任意顶点都无法通过一些有向边回到自身,那么称这个有向图为有向无环图(Directed Acyclic Graph, DAG).
2. 拓扑排序
- 拓扑排序是将有向无环图G的所有顶点排成一个线性序列,使得图中任意两个顶点u, v,如果存在u->v,那么序列中u一点在v前面。这个序列又被称为拓扑序列。
- 定义一个队列Q,并把所有入度为0的结点加入队列
- 取队首结点,输出。然后,删去所有从它出发的边,并令这些边到达的顶点的入度减一,如果某个顶点的入度减为0,则将其加入队列。
- 反复进行2操作,直到队列为空。如果队列为空时入过队列的结点数目恰好为N,说明拓扑排序成功,图G为有向无环图,否则拓扑排序失败,图G中有环。
- 拓扑排序的一个很重要的应用及时判断图是否是有向无环图。
- 如果要求有多个入度为0的顶点,选择编号最小的顶点,那么把queue改成priority_queue,并保持队首元素(堆顶元素)是优先队列中的最小元素即可。
- 可以使用邻接表实现拓扑排序, 由于需要记录每个结点的入度,额外需要定义数组inDegree[MAXN],并在程序一开始读入图时就记录好每个结点的入度。
- 代码实现:
vector<int> G[MAXN];
int n, m, inDegree[MAXN];
bool topologicalSort(){
int num = 0;//记录未加入拓扑排序的顶点数
queue<int> q;
for(int i = 0; i < n; i++){
if(inDegree[i] == 0){
q.push(i);//将所有入度为0的加入队列
}
}
while(!q.empty()){
int u =q.front();
//printf("%d", u);//此处可输出顶点u,作为拓扑排序中的顶点
q.pop();
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i];
inDegree[v]--;
if(inDegree[v] == 0){
q.push(v);
}
}
G[u].clear();//清空顶点u的所有出边
num++;
}
if(num == 0) return true;
else return false;
}
二、关键路径
1. AOV网和AOE网
- 顶点活动(Activity On Vertex,AOV)网是用顶点表示活动,而使用边集表示活动期间优先关系的有向图。
- 边活动(Activity On Edge,AOE)网是用带权的边集表示活动,而用顶点表示事件的有向图,其中边权表示完成活动需要的时间。
- AOE网中的最长路径被称为关键路径(关键路径就是AOE网的最长路径),而把关键路径上的活动称为关键活动。
2. 最长路径
- 对于一个没有正环的图,如果需要求解最长路径长度,则可以把所有边的边权乘以-1,令其变为相反数,然后使用Bellman-Ford算法或SPFA算法求最短路径长度,将所得结果取反即可。不能使用Djikstra
- 对于有环的情况,每个顶点经过一次,这样是没有办法使用上面的算法来求解的。
3. 关键路径
- AOE网实际上是一种有向无环图,而关键路径是图中的最长路径,因此本节实际上给出了一种求解有向无环图的(DAG)中最长路径的方法。
- 求解当前结点的初始时间代码:
- 在访问某个结点时保证它的前驱结点都已经访问完毕,使用拓扑排序。
//拓扑序列
stack<int> topOrder;
//拓扑排序,顺便求ve数组
bool topologicalSort(){
queue<int> q;
for(int i = 0; i < n; i++){
if(inDegree[i] == 0){
q.push(i);
}
}
while(!q.empty()){
int u = q.front();
q.pop();
topOrder.push(u);//将u加入拓扑序列
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v;
inDegree[v]--;
if(inDegree[v] == 0){
q.push(v);
}
//用ve[u]l来更新u的所有后继结点v
if(ve[u] + G[u][i].w > ve[v]){
ve[v] = ve[u] + G[u][i].w;
}
}
}
if(topOrder.size() == n) return true;
else return false;
}
- 访问当前结点的最迟时间代码:
- 在访问某个结点时保证它的后继结点都已经访问完毕。
- 可以通过逆拓扑序列来实现,因为在前面有拓扑排序的序列了,因此倒着访问即可得到,也就是按顺序出栈就是。
fill(vl, vl + n, ve[n - 1]);//vl数组初始化,初始值为终点的ve值
//直接使用topOrder出栈即为逆序拓扑序列,求解vl数组
while(!topOrder.empty()){
int u = topOrder.top();
topOrder.pop();
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v;//u的后继结点v
//用u的所有后继结点v的vl值来更新vl[u]
if(vl[v] - G[u][i].w < vl[u]){
vl = vl[v] - G[u][i].w;
}
}
}
核心思路
- 按拓扑排序和逆拓扑排序分别计算各顶点的最早发生时间和最迟发生时间:
-
最早 :ve[j] = max{ve[i]+length[i->j]};
-
最迟 :vl[i] = min{vl[j]-length[i->j]};
- 用上面结果计算各边(活动)的最早开始时间和最迟开始时间:
-
最早:e[i->j] = ve[i];
-
最迟:l[i->j] = vl[j]-length[i->j];
- e[i->j] = l[i->j]的活动即为活动的关键活动。
- 主体代码:
//适用于汇点确定且唯一的情况,以n-1号顶点为汇点为例
int CriticalPath(){
memset(ve, 0, sizeof(ve));ve数组初始化
if(topologicalSort() == false){
return -1;
}
fill(vl, vl+n, ve[n-1]);//vl数组初始化,初始值为汇点的ve值
//直接使用topOrder出栈即为逆拓扑序列,求解vl数组
while(!topOrder.empty()){
int u = topOrder.top();
topOrder.pop();
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v;
if(vl[v] - G[u][i].w < vl[u]){
vl[u] = vl[v] - G[u][i].w;
}
}
}
//遍历邻接表的所有边,计算活动的最早开始时间e和最迟开始时间l
for(int u = 0; u < n; u++){
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v, w = G[u][i].w;
//活动的最早开始时间e和最迟开始时间l
int e = ve[u], l = vl[v] - w;
//如果e == l,说明该路径为关键路径
if(e == l){
printf("%d->%d\n", u, v);
}
}
}
return ve[n-1];//返回关键路径的长度
}