大话数据结构-图的拓扑排序和关键路径

279 阅读9分钟

7 拓扑排序

7.1 概述

  在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网(Activity On Vertex Network),如下有一个AOV网的示例:

image.png

  设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1_1、v2_2、......vn_n,满足若从顶点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为例:

image.png

  由于需要删除顶点,我们使用邻接表的存储结构,为了便于计算入度,我们在邻接表的顶点中添加in表示入度,于是上述DAG的邻接表结构如下所示:

image.png

  首先我们找到邻接表中入度为0的顶点,将它放入栈中:

image.png

  然后遍历栈:删除并输出栈中的顶点,删除顶点后与该顶点连通的弧的入度减少1,若连通的顶点的入度为0的话,将其入栈,于是顶点D被从栈中删除并输出,D连通的顶点F、C、A、B的入度减1,由于F、C的入度减1后入度为0,因此将它们入栈,如下:

image.png

  再遍历栈,取出顶点C,将其连通的顶点E和B的入度减1,发出B和E的入度都变为0了,于是令它们入栈:

image.png

  遍历栈,取出顶点B,将其连通的顶点A的入度减1:

image.png

  遍历栈,取出顶点E,将其连通的顶点A的入度减1,顶点A的入度变为0,将其入栈:

image.png

  遍历栈,取出顶点A,A没有指向的顶点,直接取出并删除即可:

image.png

  遍历栈,取出顶点F,同A:

image.png

  栈为空,处理结束,得到拓扑排序:D->C->B->E->A->F,如图在侧为原有向图,右侧为拓扑排序结果:

image.png

  代码实现如下所示:

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网:

image.png

  我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。

  很明显,缩短关键路径上的关键活动时间可以减少整个工期长度,因此找出AOE网中的关键路径就变得很重要。

8.2 关键路径算法

  首先我们得基于拓扑排序进行关键路径的计算,因此可用于计算关键路径的图肯定也是DAG,所以仍然以下有向无环网为例:

image.png

  在拓扑排序的基础上,我们进行进一步调整:
  (1) 新增一个栈stack2,存储在拓扑计算时从栈中弹出的顶点;
  (2) 添加数组etv,即事件的最早发生时间,earliest time of vertex,长度与顶点长度相同,初始值均为0,计算方法是etv[i]=max(etv[i],etv[popVertexIndex] + weight),即若弹出顶点的最早发生时间+弹出顶点到当前顶点的权之和,大于当前顶点的最早发生时间时,令当前顶点的最早发生时间为权值和;
  (3) 我们有一个变量maxWeight来记录最早发生时间最晚的那个顶点的最早发生时间;

  因此初始化情况如下所示:

image.png

  然后把顶点D弹出stack1,压入stack2,然后计算etv,如下:

image.png

  可见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]};

  后续处理也类似:

image.png

image.png

image.png

image.png

image.png

  接下来是计算关键路径的重要步骤,我们先初始化几个数组:
  (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]来判断活动是否是关键活动。

  首先来计算所有事件的最晚发生时间,现在我们有如下数据:

image.png

  注意,假设有邻接表i->j->k,那么:
  ltv[i] = min{ltv[j]-weight(i->j),ltv[i],ltv[k]-weight(i->k)}

  我们将栈stack2中的元素依次出栈,其中F和A顶点并没有指向的顶点,因此ltv数组不进行任何变更:

image.png

image.png

  顶点E出栈时,ltv[0]被重新设置:

image.png

  后续顶点的出栈也类似处理:

image.png

image.png

image.png

  接下来我们开始计算ete和lte,首先,令ete直接等于etv,于是有以下:

image.png

  然后规则,如果有邻接表i->j->k,那么:
  如果lte[i]=lte[j]-weight(i->j)与ete[i]相等,则表示<i,j>是关键路径。

  于是,迭代网的顶点: image.png

image.png

image.png

image.png

image.png

image.png

  于是求得该有向无环网的关键路径为:

image.png

  代码类似如下所示:

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;
}

  注意,代码肯定还有优化空间,此处仅用于示例。