复旦大学961-数据结构-第五章-图(五)拓扑排序

961全部内容链接

文章目录

  • 拓扑排序
  • AOV网
  • AOE网
    • 基本概念
    • 求关键路径的代码实现

拓扑排序

拓扑排序:在一个有向无环图中,把节点排成一个序列,当且仅当满足以下两个特性时,称该序列为该图的一个拓扑排序:

  • 每个顶点在序列中只出现一次
  • 若顶点A在序列中排在顶点B的前面,则在图中不存在从顶点B到顶点A的路径

拓扑排序的实际应用:
复旦大学961-数据结构-第五章-图(五)拓扑排序_第1张图片
比如该图是一个有向无环图。每个节点代表着一种活动。但是每个活动都需要一个前置的触发条件,比如,如果你不买菜,就没有鸡蛋,就没法打鸡蛋。所以打鸡蛋的前置条件是买菜。其他的也同理。 但是在这个所以,这些所有活动应该按照什么样的顺序来进行,最后排一下序,这个排序后的序列就是这个有向无环图的拓扑排序。比如,对于该图,它的拓扑排序不止一个,可以举例如下两个:

  1. 准备厨具 -> 买菜 -> 打鸡蛋 -> 洗番茄 -> 切番茄 -> 下锅炒 -> 吃
  2. 买菜 -> 洗番茄 -> 准备厨具 -> 切番茄 -> 打鸡蛋 -> 下锅炒 -> 吃

除了上述两种情况,还有很多种情况存在,所以拓扑排序是不唯一的

AOV网

AOV网(Activity On Vertex Network)是用顶点表示活动的网络。主要用于描述各个活动的优先级。所以它的边没有权值。如定义中举的例子,我们并不关心哪一项活动到底花费多长时间,我们只关心哪一步先做,哪一步后做。

拓扑排序的算法相对简单,主要思路为:

  1. 从有向无环图中找到一个入度为0的点,并输出
  2. 从图中删除该顶点的所有以它为起点的有向边(此时也不会有以它为终点的有向边,因为它的入度为0)。
  3. 重复1,2,直到图中不再有顶点。

算法伪代码:

  1. 申请一个栈(队列也行,其他集合也可),用于存储当前入度为0的点
  2. 初始化栈,将当前入度为0的点入栈。
  3. 开始while,如果栈不可为空,则
    3.1 出栈,然后输出顶点
    3.2 将该顶点的邻接节点的入度-1,如果-1后入度为0,则入栈
  4. 当栈空时,遍历结束。

Java代码如下:

    // AOV网拓扑排序
    public static List topologicalSort(DirectedGraph graph) {
     
        List sortResult = new ArrayList();
        Map<Object, Integer> vertexInDegree = new HashMap<>();  // 存储每个节点的入度
        Stack stack = new Stack(); // 存储入度为0的节点

        for (Object vertex : graph.getVertexes()) {
       // 遍历所有节点,初始化所有的入度
            int inDegree = graph.getInDegree(vertex);
            vertexInDegree.put(vertex, inDegree);  // 将节点的度存储到map中
            if (inDegree == 0) stack.push(vertex); // 如果节点的入度为0,则入栈
        } // 初始化完成

        while (!stack.isEmpty()) {
      // 如果栈中还有入度为0的点,则继续,当栈中没有入度为0的点,说明拓扑排序已经完成
            Object vertex = stack.pop();
            sortResult.add(vertex);

            for (Object neighbor : graph.neighbors(vertex)) {
      // 遍历当前节点的所有邻接节点(后继节点)
                // 将该节点的所有后继节点的入度减1,相当于删除了该节点以及该节点的边。
                vertexInDegree.put(neighbor, vertexInDegree.get(neighbor) - 1);
                // 若该节点的后继节点入度减1后为0,则将其入栈
                if (vertexInDegree.get(neighbor) == 0) stack.push(neighbor);
            }
        }
        return sortResult;
    }

这里使用到了一个之前没有用过的有向图接口和邻接矩阵有向图的实现,代码如下:

public interface DirectedGraph<VertexType> extends Graph<VertexType> {
     

    int getInDegree(VertexType vertex);  // 获取节点入度

    int getOutDegree(VertexType vertex);  // 获取节点出度

}

这个有向图接口继承自普通图,多了两个接口


// 邻接矩阵实现有向图
public class AdjacencyMatrixDirectedGraph<VertexType> extends AdjacencyMatrixUndirectedGraph<VertexType>
        implements DirectedGraph<VertexType> {
     

    public AdjacencyMatrixDirectedGraph(int maxVertexNum) {
     
        super(maxVertexNum);
    }

    @Override
    public void addEdge(VertexType x, VertexType y) {
     
        int xIndex = findIndex(x);
        int yIndex = findIndex(y);
        edges[xIndex][yIndex] = 1;  // 因为是有向图,所以只给边赋值
    }


    @Override
    public int getInDegree(VertexType vertex) {
     
        int inDegree = 0;
        int xIndex = findIndex(vertex);
        for (int i = 0; i < edges.length; i++) {
     
            if (edges[i][xIndex] > 0) inDegree++;
        }
        return inDegree;
    }

    @Override
    public int getOutDegree(VertexType vertex) {
     
        int outDegree = 0;
        int[] edge = edges[findIndex(vertex)];
        for (int i : edge) {
     
            if (i > 0) outDegree++;
        }
        return outDegree;
    }
}

这个是邻接矩阵有向图的实现

AOE网

基本概念

AOE(Activity On Edge Network)网和AOV网很像,从名字中基本可以看出,AOV的V是vertex,所以AOV网是顶点代表活动。而AOE网的E是Edge,也就是边代表活动。所以,用边表示活动的网,简称AOE网。而AOE网中的顶点代表着事件
复旦大学961-数据结构-第五章-图(五)拓扑排序_第2张图片
比如如图一张AOE网,事件就是顶点,比如V1(开始事件),V2(可以切了事件),V3(可以抄了事件)。而边代表活动,比如a1(打鸡蛋活动)等等。
如图所示,可以看出事件只代表一种状态,不消耗时间。而活动是需要消耗一定时间的,而这个时间一般用权值代表

AOE网具有如下性质:

  • 只有在某顶点的所代表的的时间发生后,从该顶点出发的各个有向边所代表的的活动才能开始。意思是,当你处于“可以炒了(V3)”这个事件后,你才可以进行下一项“炒菜(a4)”这个活动
  • 只有在进入某顶点的各个有向边所得代表的活动都已经结束时,该顶点所代表的的事件才能发生。意思就是,你只有打完鸡蛋(a1)且切完番茄(a3),才可以开始炒了(V3)
  • AOE网必须要有一个开始节点和一个结束节点。即一个入度为0的节点和一个出度为0的节点。

关键路径的概念
从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径。而把关键路径上的活动称为关键活动。注意,多条路径是可以并行执行的,比如打鸡蛋可以和洗番茄并行执行。
解释:汇点就是除开始之外的其他顶点。简单点说关键路径就是影响整个工期的路径。比如在上图中炒菜(a4)就是一个关键活动,因为在整个工期中,它快整个就快,它慢,整个就慢。而切番茄(a3)也是一个关键活动。因为切番茄的快慢也是可以直接影响整个进度的,因为打蛋只需要两分钟,所以并不会影响整体进度。

复旦大学961-数据结构-第五章-图(五)拓扑排序_第3张图片
下面这几个概念很重要,这几个概念涉及到算法实现,估计也会是出题重点。

1. 事件Vk的最早发生时间ve(k)
事件最早发生时间就是字面意思。比如:“开始(V1)”的最早发生时间是0。“可以切了(V2)”的最早发生时间是1(因为他前面需要花一分钟的时间切番茄)。“可以炒了(V3)”的最早发生时间是4(因为他前面要经历a2和a3,而打鸡蛋的时间小于它们俩之和,所以不影响)。结束的最早发生时间是6。

2.事件Vk最迟发生时间vl(k)
最迟发生时间是指在不推迟整个工程完成的前提下,即保证它的后继事件Vj在其最迟发生时间vl(j)能够发生时,该事件最迟必须发生的时间。
上面这个图不太能体现这个例子,我对其进行加工一下,如图

a1.5=1,拿鸡蛋
a2=2,打鸡蛋
a1=1,洗番茄
a3=5, 切番茄
a4=2, 炒菜
V1
V1.5
V3
V2
V4

假设对上述图进行改造,在打鸡蛋之前又加了拿鸡蛋,增加了事件1.5。切番茄的时间也相应增加一些。此时关键路径没有变,还是a1,a3,a4。
对上述例子中,V1.5事件的最晚发生时间为4。因为在第6分钟的时候,洗番茄和切番茄已经干完了,虽然打鸡蛋这件事不是关键路径,但是你不能影响别人的进度,所以你在第4分钟的时候,就必须开始打鸡蛋,要不然你就来不及了。

3.活动ai的最早开始时间e(i)
理解了上面两个时间,那么这个也就不难理解了。字面意思。还用上面的老图。
复旦大学961-数据结构-第五章-图(五)拓扑排序_第4张图片
“洗番茄(a2)”的最早发生时间是0,“打鸡蛋(a1)”的最早发生时间也是0。“炒菜(a4)”的最早发生时间是4。也就是说,活动的最早发生时间等于该边对应起点的最早发生时间。即 e(i)=ve(k)。

4.活动ai的最迟开始时间l(i)
这个概念与时间的最迟发生时间差不多。比如,“打鸡蛋(a1)”这个活动的最晚发生时间为2。因为打鸡蛋需要2分钟,而洗番茄+切番茄需要4分钟。如果在2分钟的时候还不开始打鸡蛋,就会耽误整体进度。注意区分事件最迟发生时间和活动最迟发生时间的区别。事件是顶点,活动是边

5.时间余量
一个活动的“最迟开始时间 - 最早开始时间”就是时间余量。简单点说,就是这个活动可以拖多长时间再开始。比如上图中,打鸡蛋这个活动可以在开始之后拖两分钟再去做,它的活动余量就是2。你决定考研到来不及必须学政治的时候,相差了100天,那学政治这个活动的时间余量就是100天。

求关键路径的代码实现

核心思想:那些时间余量为0的活动,就是关键活动,他们构成的路径就是关键路径。简单点说就是那些不能够推迟的活动组成的路径就是关键路径。

算法步骤:

  1. 根据拓扑排序的顺序,依次求出各个顶点的最早开始时间
  2. 根据逆拓扑排序(可以理解为将拓扑排序逆序)依次求出各个顶点的最迟开始时间
  3. 如果两个顶点是邻居,且他们的最早开始时间和最晚开始时间相等,则他们之间的活动(路径)就是关键活动。

算法步骤解释:

  • 关于第一步,因为是按照拓扑排序,所以当你求第n个顶点的最早发生时间时,那么前驱节点一定已经求过了,所以只需要用它前驱节点的最早发生时间加上这两个节点之间的活动消耗时间即可。如果一个节点有多个前驱节点,那么它的最早发生时间取合最大的那个。
  • 关于第二步,因为是求最迟发生时间,所以要考虑该事件(顶点)的后继节点的最晚发生时间。即一个事件的最晚发生时间是它后继事件的最晚发生时间减去他们之间活动的所消耗的时间。比如 =3,如果v的最晚发生时间是5,那么u的最晚发生时间是5-3=2。之所以采用逆拓扑排序,是为了从后往前求,这样当你求一个节点时,它的后继节点的最晚发生时间一定已经求过了。如果一个节点有多个后继节点,那么可以求出来多个最晚发生时间,去最小的那个。

关于王道书上的步骤的个人理解:王道书上是五步,王道书在求完节点后,又求了每条边(活动)的最早发生时间和最迟发生时间,然后使用的是边的两个时间之差判断是否为0。它这样的好处个人认为有两个:1. 可以直接求出关键活动,而不用根据两个节点夹出关键活动。 2. 可以适用于复杂图,如图所示

a1=1
a2=2
V1
V2

比如对于该图,很明显,该图是一个复杂图,假设用两个节点之间有多条活动,那么用第一种方法就会认为a1和a2都是关键路径,但实际上只有a2是关键路径。

Java代码如下:

    // 求关键路径
    public static List<WeightedGraph.Edge> criticalPath(DirectedWeightedGraph graph) {
     
        List topoVertexes = topologicalSort(graph); // 求出节点的拓扑排序
        List reverseTopoVertexes = new ArrayList(); // 求出逆拓扑排序
        for (int i = topoVertexes.size() - 1; i >= 0; i--) {
     
            reverseTopoVertexes.add(topoVertexes.get(i));
        }

        Map<Object, Integer> ve = new HashMap<>(); // 存储节点的最早发生时间
        Map<Object, Integer> vl = new HashMap<>(); // 存储节点的最迟发生时间

        for (Object vertex : topoVertexes) {
      // 根据拓扑排序顺序,求出各个节点的最早发生时间
            List predecessors = graph.predecessors(vertex); // 获取该节点的所有前驱节点
            int maxWeight = 0;  // vertex节点中,“前驱顶点最早发生时间+边活动的消耗时间”的最大值
            for (Object predecessor : predecessors) {
     
                // weight = 前驱顶点最早发生时间+边活动的消耗时间
                int weight = ve.get(predecessor) + graph.getEdgeWeight(predecessor, vertex);
                if (weight > maxWeight) maxWeight = weight;  // 从它所有的前驱节点中找出消耗时间最长的那个作为最早发生时间
            }
            ve.put(vertex, maxWeight);
        }


        vl.put(reverseTopoVertexes.get(0), ve.get(reverseTopoVertexes.get(0)));  // 初始化结束顶点的最晚开始时间(等于它的最早开始时间)
        for (int i = 1; i < reverseTopoVertexes.size(); i++) {
       // 按逆拓扑排序,遍历各个顶点
            Object vertex = reverseTopoVertexes.get(i);
            Object[] neighbors = graph.neighbors(vertex);  // 求出当前节点的所有后继节点
            int latestStartTime = Integer.MAX_VALUE;  // 用于存储当前节点的最小最晚开始时间
            for (Object neighbor : neighbors) {
       // 遍历当前节点的所有后继节点,找出最小的最晚开始时间
                int delay = vl.get(neighbor) - graph.getEdgeWeight(vertex, neighbor); // 求出这条路径的最晚开始时间
                if (delay < latestStartTime) latestStartTime = delay;  // 如果它比最晚开始时间小,则更新最晚开始时间
            }
            vl.put(vertex, latestStartTime);
        }

        List<WeightedGraph.Edge> result = new ArrayList<>();
        List<WeightedGraph.Edge> edges = graph.getEdges();
        for (WeightedGraph.Edge edge : edges) {
       // 遍历所有边,若该边的两个节点都满足“最晚开始时间=最早开始时间”,那么该边就是关键搞活动
            if (vl.get(edge.vertex1) == ve.get(edge.vertex1)
                    && vl.get(edge.vertex2) == ve.get(edge.vertex2
            )) {
     
                result.add(edge);
            }
        }
        return result;
    }

你可能感兴趣的:(961)