拓扑排序主要是为解决一个工程能否顺序进行的问题,但有时我们还需要解决工程完成需要的最短时间问题。
如果要对一个流程图获得最短时间,就必须要分析它们的拓扑关系,并且找到当中最关键的流程,这个流程的时间就是最短时间。
在前面讲了AOV网的基础上,我们来介绍一个新的概念。
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE网(Activity On Edge Network)。
把AOE网中没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点。
由于一个工程,总有一个开始,一个结束,所以正常情况下,AOE网只有一个源点一个汇点。
我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。
显然图7-9-3的AOE网而言,开始->发动机完成->部件集中到位->组装完成就是关键路径,路径长度5.5。
关键路径算法原理:
找到所有活动的最早开始时间和最晚开始时间,并且比较它们,如果相等就意味着此活动是关键活动,活动间的路径为关键路径,如果不等就不是。
关键路径算法:
总结:
1.先进行拓扑排序,得出顶点事件的最早开始时间(基于源点计算而来,从前往后)。
2.初始化顶点事件最晚开始时间(拓扑排序后的最后顶点事件的最早开始时间,都是最大值),计算顶点事件的最晚开始时间(基于终点计算而来,从后往前),。
3.活动最早开始时间:就是弧尾的顶点事件的最早开始时间
4.活动最晚开始时间:就是弧头的顶点事件的最晚开始时间 - 该活动的时间
5.判断活动最早开始时间 是否等于 活动最晚开始时间
具体详细看书吧,关键路径在代码最后。
//测试数据:上图构造而来,内部类VertexNode、EdgeNode可以自行放到AdjacencyList类外面,构造就方便了,不用嵌套。
AdjacencyList<int>.VertexNode<int>[] vertexArr = new AdjacencyList<int>.VertexNode<int>[]
{
new AdjacencyList<int>.VertexNode<int>() { data = 0,inDegree=0, firstEdge=new AdjacencyList<int>.EdgeNode(){ adjvex=1,weight=3, next=new AdjacencyList<int>.EdgeNode(){adjvex=2,weight=4 } } },
new AdjacencyList<int>.VertexNode<int>() { data = 1,inDegree=1, firstEdge=new AdjacencyList<int>.EdgeNode(){ adjvex=3,weight=5,next=new AdjacencyList<int>.EdgeNode(){adjvex=4,weight=6 } } },
new AdjacencyList<int>.VertexNode<int>() { data = 2,inDegree=1, firstEdge=new AdjacencyList<int>.EdgeNode(){ adjvex=3,weight=8,next=new AdjacencyList<int>.EdgeNode(){adjvex=5,weight=7} } },
new AdjacencyList<int>.VertexNode<int>() { data = 3,inDegree=2, firstEdge=new AdjacencyList<int>.EdgeNode(){ adjvex=4,weight=3 } },
new AdjacencyList<int>.VertexNode<int>() { data = 4,inDegree=2, firstEdge=new AdjacencyList<int>.EdgeNode(){ adjvex=6,weight=9,next=new AdjacencyList<int>.EdgeNode(){ adjvex=7,weight=4} } },
new AdjacencyList<int>.VertexNode<int>() { data = 5,inDegree=1, firstEdge=new AdjacencyList<int>.EdgeNode(){ adjvex=7,weight=6 } },
new AdjacencyList<int>.VertexNode<int>() { data = 6,inDegree=1, firstEdge=new AdjacencyList<int>.EdgeNode(){ adjvex=9,weight=2} },
new AdjacencyList<int>.VertexNode<int>() { data = 7,inDegree=2, firstEdge=new AdjacencyList<int>.EdgeNode(){ adjvex=8,weight=5} },
new AdjacencyList<int>.VertexNode<int>() { data = 8,inDegree=1, firstEdge=new AdjacencyList<int>.EdgeNode(){ adjvex=9,weight=3} },
new AdjacencyList<int>.VertexNode<int>() { data = 9,inDegree=2 }
};
AdjacencyList<int> al = new AdjacencyList<int>(vertexArr);
al.CriticalPath();
/// <summary>
/// 泛型邻接表:一维数组 + 链表(数组和链表自行提供)
/// </summary>
public class AdjacencyList<T>
{
/// <summary>
/// 边表结点
/// </summary>
public class EdgeNode
{
public int adjvex; //邻接点域,存储该顶点对应的下标
public int weight; //用于存储权值,对于非网图可以不需要(关键路径需要权值)
public EdgeNode next; //链域,指向下一个邻接点
}
/// <summary>
/// 顶点表结点
/// </summary>
public class VertexNode<T>
{
public int inDegree; //入度:拓扑排序用
public T data; //顶点域,存储顶点信息
public EdgeNode firstEdge; //边表头指针
}
private VertexNode<T>[] adjacencyArr; //顶点表
/// <summary>
/// 说明:没有按书上写,需要的数据自行提供
/// </summary>
/// <param name="vertexData">顶点数据</param>
public AdjacencyList(VertexNode<T>[] vertexData)
{
adjacencyArr = vertexData;
}
private bool[] visited;
#region 操作
/// <summary>
/// 邻接表的深度优先遍历
/// </summary>
public void DFSTraverse()
{
visited = new bool[adjacencyArr.Length];
for (int i = 0; i < adjacencyArr.Length; i++)
{
visited[i] = false; //初始化所有顶点都是未访问的状态
}
for (int i = 0; i < adjacencyArr.Length; i++)
{
//对未访问过的顶点调用DFS,若是连通图,只会执行一次
if (!visited[i])
{
DFS(i);
}
}
}
/// <summary>
/// 邻接表的深度优先递归算法
/// </summary>
/// <param name="gl"></param>
/// <param name="i"></param>
private void DFS(int i)
{
visited[i] = true;
//这里对顶点的操作,这里简单的打印
Debug.Log(adjacencyArr[i].data);
EdgeNode temp = adjacencyArr[i].firstEdge;
while (temp != null)
{
if (!visited[temp.adjvex])
DFS(temp.adjvex);
temp = temp.next;
}
}
/// <summary>
/// 邻接表:广度优先遍历
/// </summary>
/// <param name="gl"></param>
public void BFSTraverse()
{
visited = new bool[adjacencyArr.Length];
Queue<int> queue = new Queue<int>(); //初始化辅助队列
EdgeNode p;
for (int i = 0; i < adjacencyArr.Length; i++)
{
visited[i] = false;
}
for (int i = 0; i < adjacencyArr.Length; i++) //对每一个顶点做循环
{
if (!visited[i]) //若是未访问过就处理
{
visited[i] = true; //设置当前顶点访问过
//这里对顶点的操作,这里简单的打印
Debug.Log(adjacencyArr[i].data);
queue.Enqueue(i); //将此顶点入队列
while (queue.Count > 0) //当前队列有元素
{
i = queue.Dequeue(); //出队列
p = adjacencyArr[i].firstEdge; //找到当前顶点边表链表头指针
while (p != null)
{
//此顶点未访问过
if (!visited[p.adjvex])
{
visited[p.adjvex] = true;
//这里对顶点的操作,这里简单的打印
Debug.Log(adjacencyArr[p.adjvex].data);
queue.Enqueue(p.adjvex); //将此顶点入队列
}
p = p.next; //指针指向下一个邻接点
}
}
}
}
}
/// <summary>
/// 拓扑排序:需要一个栈辅助,存储处理过程中入度为0的顶点
/// 目的是为了避免么个查找时都要去遍历顶点表找有没有入度为0的顶点
/// </summary>
/// <returns>无回路返回true,有回路返回false</returns>
public bool TopologicalSort()
{
EdgeNode node;
int k, topIndex;
int count = 0; //用于统计输出顶点的个数
Stack<int> stack = new Stack<int>(adjacencyArr.Length); //建栈存储入度为0的顶点
for (int i = 0; i < adjacencyArr.Length; i++)
{
if (adjacencyArr[i].inDegree == 0)
{
stack.Push(i);
}
}
string result = string.Empty;
while (stack.Count != 0)
{
topIndex = stack.Pop(); //出栈
result += "->" + adjacencyArr[topIndex].data; //打印此顶点
count++; //统计输出顶点数
for (node = adjacencyArr[topIndex].firstEdge; node != null; node = node.next)
{
//对此顶点弧表遍历
k = node.adjvex;
if ((--adjacencyArr[k].inDegree) == 0) //将k号顶点邻接点的入度减1
{
stack.Push(k); //若为0则入栈,以便下次循环输出
}
}
}
Debug.Log(result); //打印结果
if (count < adjacencyArr.Length) return false; //如果count小于顶点数,说明存在环
else return true;
}
int[] etv; //事件最早发生的时间数组
int[] ltv; //事件最迟发生的时间数组
Stack<int> stack2; //存储拓扑序列的栈
/// <summary>
/// 关键路径的拓扑排序:需要一个栈辅助,存储处理过程中入度为0的顶点
/// 目的是为了避免每个查找时都要去遍历顶点表找有没有入度为0的顶点
/// </summary>
/// <returns>无回路返回true,有回路返回false</returns>
private bool TopologicalSortForCriticalPath()
{
EdgeNode node;
int k, topIndex;
int count = 0; //用于统计输出顶点的个数
Stack<int> stack = new Stack<int>(adjacencyArr.Length); //建栈存储入度为0的顶点
for (int i = 0; i < adjacencyArr.Length; i++)
{
if (adjacencyArr[i].inDegree == 0)
{
stack.Push(i);
}
}
//为求关键路径添加的
etv = new int[adjacencyArr.Length];
for (int i = 0; i < adjacencyArr.Length; i++)
{
etv[i] = 0;
}
stack2 = new Stack<int>(adjacencyArr.Length);
string result = "拓扑序列:";
while (stack.Count != 0)
{
topIndex = stack.Pop(); //出栈
result += "->" + adjacencyArr[topIndex].data; //打印此顶点
count++; //统计输出顶点数
//为求关键路径添加的
stack2.Push(topIndex); //将弹出的顶点序列号压入拓扑序列的栈
for (node = adjacencyArr[topIndex].firstEdge; node != null; node = node.next)
{
//对此顶点弧表遍历
k = node.adjvex;
if ((--adjacencyArr[k].inDegree) == 0) //将k号顶点邻接点的入度减1
{
stack.Push(k); //若为0则入栈,以便下次循环输出
}
//顶点事件的最早开始时间:从源点开始计算(源点的开始时间为0,以源点为基准)
if ((etv[topIndex] + node.weight) > etv[k]) //求各顶点事件最早发生时间值
{
etv[k] = etv[topIndex] + node.weight;
}
}
}
Debug.Log(result); //打印结果
if (count < adjacencyArr.Length) return false; //如果count小于顶点数,说明存在环
else return true;
}
/// <summary>
/// 关键路径:gl为有向网,输出gl的各项关键活动
/// </summary>
public void CriticalPath()
{
EdgeNode node;
int i, topIndex, k, j;
int ete, lte; //声明活动最早发生时间和最迟发生时间变量
bool sortSuccess = TopologicalSortForCriticalPath(); //求拓扑序列,计算数组etv和stack2的值
if (!sortSuccess)
{
Debug.Log("拓扑排序失败,工程存在问题!");
return;
}
ltv = new int[adjacencyArr.Length];
for (i = 0; i < adjacencyArr.Length; i++)
{
ltv[i] = etv[adjacencyArr.Length - 1]; //初始化ltv:拓扑排序得到的终点事件的时间
}
while (stack2.Count != 0) //计算ltv
{
topIndex = stack2.Pop(); //将拓扑序列出栈,后进先出
for (node = adjacencyArr[topIndex].firstEdge; node != null; node = node.next)
{
//顶点事件的最晚开始时间:从终点开始计算(以终点的开始时间为基准)
k = node.adjvex;
if (ltv[k] - node.weight < ltv[topIndex]) //求各顶点事件最晚发生时间ltv
{
ltv[topIndex] = ltv[k] - node.weight;
}
}
}
Debug.Log("关键路径:");
for (j = 0; j < adjacencyArr.Length; j++) //求ete,lte和关键活动
{
for (node = adjacencyArr[j].firstEdge; node != null; node = node.next)
{
k = node.adjvex;
ete = etv[j]; //活动最早发生时间:弧尾事件的最早开始时间
lte = ltv[k] - node.weight; //活动最迟发生时间:弧头事件的最晚开始时间 - 活动需要的时间
if (ete == lte) //两者相等即在关键路径上
{
Debug.Log($"<v{adjacencyArr[j].data},v{adjacencyArr[k].data}> length:{node.weight}");
}
}
}
}
#endregion
}