7 拓扑排序
7.1 概述
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网(Activity On Vertex Network),如下有一个AOV网的示例:
设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v、v、......v,满足若从顶点Vi到Vj有一条路径,则在顶点序列中顶点Vi必在顶点Vj之前,这样的顶点序列称为一个拓扑排序。
上图这样的AOV网的拓扑排序不止一条,序列V0、V1、V2、V3、V4、V5、V6、V7、V8、V9、V10、V11、V12、V13、V14、V15、V16是一条拓扑序列,V0、V1、V4、V3、V2、V7、V6、V5、V8、V10、V9、V12、V11、V14、V13、V15、V16也是一条拓扑序列。
所谓的拓扑排序,其实就是对一个有向图构造拓扑序列的过程。构造时会有两个结果,如果此网的全部顶点都被输出,则说明它是不存在环(回路)的AOV网;如果输出顶点数少了,哪怕是少了一个,也说明这个网存在环(回路),不是AOV网。
一个不存在回路的AOV网,我们可以将它应用在各种各样的工程或项目的流程图中,满足各种应用场景的需求,所以实现拓扑排序的算法就很有价值了。
7.2 拓扑排序算法
需要注意,拓扑排序针对的是有向无环图(DAG,Directed Acyclic Graph),对其他类型的图不适用。
对AOV网进行拓扑排序的基本思路是:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止。
以以下DAG为例:
由于需要删除顶点,我们使用邻接表的存储结构,为了便于计算入度,我们在邻接表的顶点中添加in表示入度,于是上述DAG的邻接表结构如下所示:
首先我们找到邻接表中入度为0的顶点,将它放入栈中:
然后遍历栈:删除并输出栈中的顶点,删除顶点后与该顶点连通的弧的入度减少1,若连通的顶点的入度为0的话,将其入栈,于是顶点D被从栈中删除并输出,D连通的顶点F、C、A、B的入度减1,由于F、C的入度减1后入度为0,因此将它们入栈,如下:
再遍历栈,取出顶点C,将其连通的顶点E和B的入度减1,发出B和E的入度都变为0了,于是令它们入栈:
遍历栈,取出顶点B,将其连通的顶点A的入度减1:
遍历栈,取出顶点E,将其连通的顶点A的入度减1,顶点A的入度变为0,将其入栈:
遍历栈,取出顶点A,A没有指向的顶点,直接取出并删除即可:
遍历栈,取出顶点F,同A:
栈为空,处理结束,得到拓扑排序:D->C->B->E->A->F,如图在侧为原有向图,右侧为拓扑排序结果:
代码实现如下所示:
import lombok.Data;
/**
* 顶点表结点
* <p>
* T表示顶点类型
* <p>
* W表示权值类型
*
* @author Korbin
* @date 2023-01-31 16:05:38
**/
@Data
public class VertexNode<T, W> {
/**
* 边表头指针,也即第一个邻接点的位置
**/
private EdgeNode<W> firstEdge;
/**
* 入度的数量
**/
private int in;
/**
* 顶点
**/
private T vertex;
}
/**
* 按照顶点集值vertexValues和边或弧链表结点数组构造图
* <p>
* 无向图和无向网图的边的数量,是所有单链表结点的总数的一半
* <p>
* 有向图和有向网图的弧的数量,是所有单链表结点的总数的一半
*
* @param vertexValues 顶点集
* @param edges 边或弧矩阵,不存储next
* @param type 图类型
* @param reverseAdjacency 是否为逆邻接表,true为是,false为否
* @author Korbin
* @date 2023-01-31 14:23:07
**/
public void create(T[] vertexValues, EdgeNode<W>[][] edges, BusinessConstants.GRAPH_TYPE type,
boolean reverseAdjacency) {
this.type = type;
this.vertexNum = vertexValues.length;
this.reverseAdjacency = reverseAdjacency;
int edgeNumTmp = 0;
// 入度数组
int[] ins = new int[vertexNum];
for (int i = 0; i < vertexValues.length; i++) {
VertexNode<T, W> vertexNode = null;
if (null != vertexValues[i]) {
vertexNode = new VertexNode<>();
vertexNode.setVertex(vertexValues[i]);
if (null != edges && null != edges[i]) {
edgeNumTmp += edges[i].length;
// 处理第一个邻接顶点
if (edges[i].length > 0) {
EdgeNode<W> firstEdge = edges[i][0];
vertexNode.setFirstEdge(firstEdge);
// 设置入度
ins[firstEdge.getIndex()] = ins[firstEdge.getIndex()] + 1;
}
for (int j = 1; j < edges[i].length && null != edges[i][j]; j++) {
// 处理后续邻接顶点
EdgeNode<W> edgeNode = edges[i][j];
EdgeNode<W> lastNode = edges[i][j - 1];
lastNode.setNext(edgeNode);
// 设置入度
ins[edgeNode.getIndex()] = ins[edgeNode.getIndex()] + 1;
}
}
}
vertexes[i] = vertexNode;
}
// 设置入度
for (int i = 0; i < vertexNum; i++) {
vertexes[i].setIn(ins[i]);
}
switch (type) {
case UNDIRECTED:
case UNDIRECTED_NETWORK: {
// 无向图和无向网图的边的数量,是所有单链表结点的总数的一半
this.edgeNum = edgeNumTmp / 2;
break;
}
case DIRECTED:
case DIRECTED_NETWORK: {
// 有向图和有向网图的弧的数量,是所有单链表结点的总数的一半
this.edgeNum = edgeNumTmp;
break;
}
}
}
/**
* 对有向无环图进行拓扑排序后返回排序结果
*
* @return 拓扑排序结果
* @author Korbin
* @date 2023-02-28 14:57:25
**/
public List<T> topologicSort() {
List<T> result = new ArrayList<>();
LinkStack<VertexNode<T, W>> stack = new LinkStack<>();
stack.init();
// 初始化,把入度为0的顶点入栈
for (int i = 0; i < vertexNum; i++) {
VertexNode<T, W> vertexNode = vertexes[i];
int in = vertexNode.getIn();
if (in == 0) {
stack.push(vertexNode);
}
}
while (!stack.isEmpty()) {
StackNode<VertexNode<T, W>> stackNode = stack.pop();
VertexNode<T, W> vertexNode = stackNode.getData();
System.out.println("\r\npop " + vertexNode.getVertex());
// 加入结果列表
result.add(vertexNode.getVertex());
// 削减连通弧的入度
EdgeNode<W> edgeNode = vertexNode.getFirstEdge();
while (null != edgeNode) {
int index = edgeNode.getIndex();
VertexNode<T, W> refVertex = vertexes[index];
int refIn = refVertex.getIn();
if (refIn > 0) {
refIn--;
refVertex.setIn(refIn);
vertexes[index] = refVertex;
if (refIn == 0) {
// 入度减1后若为0,则入栈
stack.push(refVertex);
System.out.println("push " + refVertex.getVertex());
}
}
edgeNode = edgeNode.getNext();
}
}
return result;
}
算法的时间复杂度为O(n+k)。
8 关键路径
8.1 概述
拓扑排序解决的是一个工程能否顺序进行的问题,但有时候我们还需要解决工程完成需求的最短时间问题。
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称为AOE网(Activity On Edge Network)。
我们把AOE网中没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点,如下所示是一个AOE网:
我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。
很明显,缩短关键路径上的关键活动时间可以减少整个工期长度,因此找出AOE网中的关键路径就变得很重要。
8.2 关键路径算法
首先我们得基于拓扑排序进行关键路径的计算,因此可用于计算关键路径的图肯定也是DAG,所以仍然以下有向无环网为例:
在拓扑排序的基础上,我们进行进一步调整:
(1) 新增一个栈stack2,存储在拓扑计算时从栈中弹出的顶点;
(2) 添加数组etv,即事件的最早发生时间,earliest time of vertex,长度与顶点长度相同,初始值均为0,计算方法是etv[i]=max(etv[i],etv[popVertexIndex] + weight),即若弹出顶点的最早发生时间+弹出顶点到当前顶点的权之和,大于当前顶点的最早发生时间时,令当前顶点的最早发生时间为权值和;
(3) 我们有一个变量maxWeight来记录最早发生时间最晚的那个顶点的最早发生时间;
因此初始化情况如下所示:
然后把顶点D弹出stack1,压入stack2,然后计算etv,如下:
可见etv[1]和etv[4]有变更,这里注意,etv的计算公式是:
如果有弧邻接表i->j->k,那么:
(1) etv[j]=max{etv[i]+weight(i->j), etv[j]};
(2) etv[k]=max{etv[i]+weight(i->k), etv[k]};
后续处理也类似:
接下来是计算关键路径的重要步骤,我们先初始化几个数组:
(1) ltv,lastest time of vertex,事件最晚发生时间,长度为顶点数量,初始值为maxWeight;
(2) ete,lastest time of edge,活动的最早开工时间,长度为顶点数量,初始值为0;
(3) lte,lastest time of edge,活动的最晚开工时间,长度为顶点数量,初始值为0;
有一个重要概念是:如果活动的最早开工时间等于最晚开工时间,那么该活动为关键活动,因此我们需要分别计算ltv、ete、lte,然后通过判断ete[i]是否等于lte[i]来判断活动是否是关键活动。
首先来计算所有事件的最晚发生时间,现在我们有如下数据:
注意,假设有邻接表i->j->k,那么:
ltv[i] = min{ltv[j]-weight(i->j),ltv[i],ltv[k]-weight(i->k)}。
我们将栈stack2中的元素依次出栈,其中F和A顶点并没有指向的顶点,因此ltv数组不进行任何变更:
顶点E出栈时,ltv[0]被重新设置:
后续顶点的出栈也类似处理:
接下来我们开始计算ete和lte,首先,令ete直接等于etv,于是有以下:
然后规则,如果有邻接表i->j->k,那么:
如果lte[i]=lte[j]-weight(i->j)与ete[i]相等,则表示<i,j>是关键路径。
于是,迭代网的顶点:
于是求得该有向无环网的关键路径为:
代码类似如下所示:
import lombok.Data;
/**
* 顶点表结点
* <p>
* T表示顶点类型
* <p>
* W表示权值类型
*
* @author Korbin
* @date 2023-01-31 16:05:38
**/
@Data
public class VertexNode<T, W> {
/**
* 边表头指针,也即第一个邻接点的位置
**/
private EdgeNode<W> firstEdge;
/**
* 入度的数量
**/
private int in;
/**
* 索引下标
**/
private int index;
/**
* 顶点
**/
private T vertex;
}
/**
* 对有向无环图进行拓扑排序后返回排序结果
* <p>
* 仅用于示范,因此默认所有权值均大于0
*
* @return 拓扑排序结果
* @author Korbin
* @date 2023-02-28 14:57:25
**/
@SuppressWarnings("unchecked")
public Object[] topologicSort() {
Object[] result = new Object[4];
List<T> vertexList = new ArrayList<>();
LinkStack<VertexNode<T, W>> stack = new LinkStack<>();
stack.init();
// 弹出顶点栈
LinkStack<VertexNode<T, W>> stack2 = new LinkStack<>();
stack2.init();
// 最大权值和
W maxWeight = null;
// 最早发生时间
W[] etv = (W[]) Array.newInstance(infinity.getClass(), vertexNum);
// 以权值是int的情况举例,其他类型类似
for (int i = 0; i < vertexNum; i++) {
if (infinity instanceof Integer) {
etv[i] = (W) Integer.valueOf(0);
maxWeight = (W) Integer.valueOf(0);
}
}
// 初始化,把入度为0的顶点入栈
for (int i = 0; i < vertexNum; i++) {
VertexNode<T, W> vertexNode = vertexes[i];
int in = vertexNode.getIn();
if (in == 0) {
stack.push(vertexNode);
}
}
while (!stack.isEmpty()) {
StackNode<VertexNode<T, W>> stackNode = stack.pop();
// 压入栈2
stack2.push(stackNode.getData());
VertexNode<T, W> vertexNode = stackNode.getData();
System.out.println("\r\npop " + vertexNode.getVertex());
// 加入结果列表
vertexList.add(vertexNode.getVertex());
W vertexEtv = etv[vertexNode.getIndex()];
// 削减连通弧的入度
EdgeNode<W> edgeNode = vertexNode.getFirstEdge();
while (null != edgeNode) {
int index = edgeNode.getIndex();
VertexNode<T, W> refVertex = vertexes[index];
int refIn = refVertex.getIn();
if (refIn > 0) {
refIn--;
refVertex.setIn(refIn);
vertexes[index] = refVertex;
if (refIn == 0) {
// 入度减1后若为0,则入栈
stack.push(refVertex);
System.out.println("push " + refVertex.getVertex());
}
}
// 求etv
W weight = edgeNode.getWeight();
W oldEtv = etv[index];
if (infinity instanceof Integer) {
// 以整型举例
W newEtv = (W) Integer.valueOf((Integer) vertexEtv + (Integer) weight);
// 如果当前顶点的Etv+weight大于连通顶点的Etv
// 则令连通顶点的Etv为当前顶点的Etv+weigh
if (newEtv.compareTo(oldEtv) > 0) {
etv[index] = newEtv;
// 求取最大权值
if (maxWeight == null || maxWeight.compareTo(newEtv) < 0) {
maxWeight = newEtv;
}
}
}
edgeNode = edgeNode.getNext();
}
}
result[0] = vertexList;
result[1] = etv;
result[2] = maxWeight;
result[3] = stack2;
return result;
}
/**
* 生成关键路径
*
* @return 关键路径
* @author Korbin
* @date 2023-03-02 10:23:21
**/
@SuppressWarnings("unchecked")
public List<String> circlePath() {
List<String> result = new ArrayList<>();
// 进行拓扑排序,获得etv、stack2和maxWeight
Object[] topologic = topologicSort();
W[] etv = (W[]) topologic[1];
W maxWeight = (W) topologic[2];
LinkStack<VertexNode<T, W>> stack2 = (LinkStack<VertexNode<T, W>>) topologic[3];
// 定义并初始化ltv
W[] ltv = (W[]) Array.newInstance(infinity.getClass(), vertexNum);
// 定义并初始化ete
W[] ete = (W[]) Array.newInstance(infinity.getClass(), vertexNum);
for (int i = 0; i < vertexNum; i++) {
ltv[i] = maxWeight;
ete[i] = etv[i];
}
// 遍历栈,重置ltv
while (!stack2.isEmpty()) {
VertexNode<T, W> vertexNode = stack2.pop().getData();
int vertexNodeIndex = vertexNode.getIndex();
W vertexLtv = ltv[vertexNodeIndex];
EdgeNode<W> edgeNode = vertexNode.getFirstEdge();
while (null != edgeNode) {
W weight = edgeNode.getWeight();
int index = edgeNode.getIndex();
if (infinity instanceof Integer) {
W newLtv = (W) (Integer.valueOf((Integer) ltv[index] - (Integer) weight));
if (newLtv.compareTo(vertexLtv) < 0) {
// 若连通顶点的ltv-weight小于当前顶点的ltv
// 则令当前顶点的ltv等于连通顶点的ltv-weight
ltv[vertexNodeIndex] = newLtv;
vertexLtv = newLtv;
}
}
edgeNode = edgeNode.getNext();
}
}
// 求取关键路径
for (int i = 0; i < vertexNum; i++) {
VertexNode<T, W> vertexNode = vertexes[i];
T vertex = vertexNode.getVertex();
EdgeNode<W> edgeNode = vertexNode.getFirstEdge();
while (null != edgeNode) {
W weight = edgeNode.getWeight();
int index = edgeNode.getIndex();
if (infinity instanceof Integer) {
W lte = (W) (Integer.valueOf((Integer) ltv[index] - (Integer) weight));
if (lte.equals(ete[i])) {
// 如果指向的顶点的ltv-weight等于当前顶点的ete,则表示这条路径为关键路径
String circlePath = "<" + vertex + "," + vertexes[index].getVertex() + "> " + weight;
result.add(circlePath);
}
}
edgeNode = edgeNode.getNext();
}
}
return result;
}
注意,代码肯定还有优化空间,此处仅用于示例。