算法 - 图的实例 - 拓扑排序与关键路径 (Topological Sort and Critical Path)

本文详细介绍了拓扑排序和关键路径的概念,并提供了C++代码实现。拓扑排序是针对有向无环图(AOV)的线性排序,而关键路径则是寻找有向加权图中从源点到汇点的最长路径,用于确定项目完成的最短时间。文章通过实例阐述了如何计算活动的最早和最迟开始时间,以及如何识别关键活动和关键路径。

算法 - 图的实例 - 拓扑排序与关键路径 (Topological Sort and Critical Path)

返回分类:全部文章 >> 基础知识

返回上级:编程基础 - 图 (Graph)

本文将介绍活动网络的基础知识,并用C++实现拓扑排序(Topological Sort)和关键路径(Critical Path)。

在查看本文之前,需要一些数据结构和程序语言的基础。

尤其是“矩阵”、“矩阵的压缩(matrix)”“图(graph)”等的知识。



1 拓扑排序与关键路径简述 (Introduction)

  • AOV网络(Activity on Vertices):用有向图表示一个工程,每一个顶点表示活动,用边表示活动方向,边开始的顶点是结束顶点的前置条件。

    • AOV网络不能有有向回路,即不能有有向环;
    • 将各个顶点排列成一个线性有序序列的运算,称为拓扑排序;
    • 如果网络存在有向环,则表示此工程不可行。
  • AOE网络(Activity on Edges):用有向图表示一个工程,每一条边表示活动,用边上权值表示活动时间,顶点表示事件。

    • AOE网络不能有有向回路,即不能有有向环;
    • 完成整个工程所需的时间取决于从源点到汇点的最长路径长度,这条最长路径称为关键路径;
    • 关键路径上的活动称为关键活动。

2 拓扑排序 (Topological Sort)

拓扑排序方法:

  • (1)输入AOV网络;

  • (2)从AOV网路中选择一个没有直接前驱的顶点,输出;

  • (3)从图中删除该点,同时删除它所有的边;

  • (4)重复步骤(2)和(3),直到所有顶点均输出;或者还剩下顶点,表明此图存在有向环。

例如下图:没有前驱的顶点,只能是 V2 和 V4

V4
V0
V1
V5
V2
V3

所以拓扑排序顺序:

  • V2,V4,V0,……

  • V4,V2或V0, ……


3 拓扑排序C++代码 (Topological Sort C++ Code)

// Author: https://blog.youkuaiyun.com/DarkRabbit
// Activity Network
// 获取首批没有直接前驱的顶点和计算所有入度
// params:
//      graph:          图
//      vertexStack:    起始顶点栈
//      indegrees:      输出的入度
// return:
//      bool:           是否有起始点,图是否不是环
bool GetBeginVertexesAndIndegrees(AMGraphInt* graph,
                                  std::stack<int>& vertexStack,
                                  std::vector<int>& indegrees)
{
   
   
    double infinity = graph->GetDefaultWeight(); // 无边权值,即正无穷
    int size = graph->GetVertexCount(); // 顶点数量
    indegrees.assign(size, 0);
    
    double weight;
    for (int c = 0; c < size; c++) // 对列循环,即终点
    {
   
   
        bool hasEdge = false;

        for (int r = 0; r < size; r++) // 对行循环,即起始点
        {
   
   
            graph->TryGetWeight(r, c, weight); // 获取权重
            if (weight != infinity) // 如果有边
            {
   
   
                hasEdge = true; 
                indegrees[c]++; // 入度+1
            }
        }

        if (!hasEdge) // 如果顶点没有直接前驱
        {
   
   
            vertexStack.push(c); // 加入起始顶点
        }
    }

    return !vertexStack.empty(); // 没有起始点,说明图是个环
}

// 拓扑排序
// params:
//      graph:      需要排序的图
//      paths:      输出的顺序
// return:
//      bool:       是否出错
bool TopologicalSort(AMGraphInt* graph, 
                     std::vector<int>& paths)
{
   
   
    if (graph == nullptr || !graph->IsOriented()) // 无向图返回
    {
   
   
        return false;
    }

    paths.clear();
    int size = graph->GetVertexCount(); // 顶点数量
    if (size == 0) // 没有顶点
    {
   
   
        return true;
    }

    double infinity = graph->GetDefaultWeight(); // 无边权值,即正无穷
    
    std::stack<int> vertexStack; // 顶点栈
    std::vector<int> indegrees; // 顶点入度

    // 获取首批没有直接前驱的顶点和计算所有入度
    if (!GetBeginVertexesAndIndegrees(graph, vertexStack, indegrees))
    {
   
   
        return false; // 没有顶点,说明起始图就是个环
    }

    double weight;
    for (int i = 0; i < size; i++)
    {
   
   
        if (vertexStack.empty()) // 没有入度为0的顶点了
        {
   
   
            return false; // 有环
        }
        else
        {
   
   
            int vertex = vertexStack.top();
            vertexStack.pop();

            paths.push_back(vertex); // 输出路径

            // 将此顶点连接的顶点入度-1
            for (int c = 0; c < size; c++)
            {
   
   
                graph->TryGetWeight(vertex, c, weight);
                
                // 入度-1,如果没有入度了入栈
                if (weight != infinity && --indegrees[c] == 0)
                {
   
   
                    vertexStack.push(c);
                }
            }
        }
    }

    return true;
}

4 关键路径 (Critical Path)

关键路径:从源点到汇点具有最大长度的路径;

关键活动:关键路径上的活动;

我们假设带权有向图中,顶点为 { v0, v1, …, vi, …, vn-1 } ,边为 { e0, e1, …, ej,… } 。

  • 事件最早开始时间:顶点 vi 最早发生的时间;

  • 事件最迟开始时间:顶点 vi 最迟发生的时间,如果超过这个时间,工程将延误;

  • 活动的最早开始时间:边 ej 最早发生的时间;

  • 活动的最迟开始时间:边 ej 最迟发生的时间,如果超过这个时间,工程将延误。

举例说明:

e0 = 9
e1 = 13
e2 = 15
e3 = 9
e4 = 29
e6 = 6
e5 = 7
e7 = 18
e8 = 6
e9 = 12
v0
v1
v2
v3
v5
v4
v6
v7

此图已经按拓扑排序编号。

  • 事件最早活动时间 VE (Vertex Earliest Time) :

    V E ( v 0 ) = 0 V E ( v 1 ) = v 0 + e 0 = 0 + 9 = 9 V E ( v 2 ) = v 0 + e 1 = 0 + 13 = 13 V E ( v 3 ) = max ⁡ ( v 1 + e 2 , v 2 + e 3 ) = max ⁡ ( 9 + 15 , 13 + 9 ) = 24 V E ( v 4 ) = v 3 + e 6 = 24 + 6 = 30 V E ( v 5 ) = max ⁡ ( v 2 + e 4 , v 3 + e 5 ) = max ⁡ ( 13 + 29 , 24 + 7 ) = 42 V E ( v 6 ) = max ⁡ ( v 4 + e 7 , v 5 + e 8 ) = max ⁡ ( 30 + 18 , 42 + 6 ) = 48 V E ( v 7 ) = v 6 + e 9 = 48 + 12 = 60 \begin{array}{rlll} VE(v_0) &amp;&amp;&amp;= 0 \\ VE(v_1) &amp;= v_0 + e_0 &amp;= 0 + 9 &amp;= 9 \\ VE(v_2) &amp;= v_0 + e_1 &amp;= 0 + 13 &amp;= 13 \\ VE(v_3) &amp;= \max(v_1 + e_2, v_2 + e_3) &amp;= \max(9 + 15, 13 + 9) &amp;= 24 \\ VE(v_4) &amp;= v_3 + e_6 &amp;= 24 + 6 &amp;= 30 \\ VE(v_5) &amp;= \max(v_2 + e_4, v_3 + e_5) &amp;= \max(13 + 29, 24 + 7) &amp;= 42 \\ VE(v_6) &amp;= \max(v_4 + e_7, v_5 + e_8) &amp;= \max(30 + 18, 42 + 6) &amp;= 48 \\ VE(v_7) &amp;= v_6 + e_9 &amp;= 48 + 12 &amp;= 60 \end{array} VE(v0)VE(v1)VE(v2)VE(v3)VE(v4)VE(v5)VE(v6)VE(v7)=v

&lt;think&gt;我们在之前的对话中已经讨论了顶点事件的定义和特性,现在用户想要了解如何使用C语言实现AOE网关键路径的计算算法。根据用户提供的引用内容,尤其是引用[3]和[4],我们可以知道关键路径的计算需要拓扑排序和事件的最早/最迟发生时间计算。下面我们将详细描述C语言实现的关键步骤。 我们将按照以下步骤进行: 1. 的存储结构(邻接表) 2. 拓扑排序 3. 计算事件的最早发生时间ve 4. 计算事件的最迟发生时间vl 5. 计算活动的最早开始时间ee和最迟开始时间el 6. 根据ee和el的差值(时间余量)确定关键活动,从而得到关键路径 注意:在AOE网中,顶点表示事件,边表示活动,边上的权值表示活动的持续时间。 下面我们逐步展开: ### 1. 的存储结构 我们使用邻接表来存储AOE网。每个顶点对应一个事件,每个边节点包含该边所代表的活动信息,包括活动的持续时间(权值)以及指向的下一个邻接点的指针。 数据结构定义如下: ```c #include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; #define MAX_VERTEX_NUM 20 // 边表节点 typedef struct ArcNode { int adjvex; // 该边指向的顶点位置 int weight; // 边的权值,即活动持续时间 struct ArcNode *nextarc; // 指向下一条边的指针 } ArcNode; // 顶点表节点 typedef struct VNode { int data; // 顶点信息(如事件编号) int in; // 顶点的入度 ArcNode *firstarc; // 指向第一条依附该顶点的边 } VNode, AdjList[MAX_VERTEX_NUM]; // 的结构 typedef struct { AdjList vertices; int vexnum, arcnum; // 顶点数和边数 } ALGraph; ``` ### 2. 拓扑排序 拓扑排序用于确定事件发生的顺序,确保在计算事件最早发生时间时,其前驱事件都已计算完毕。 算法步骤: - 将所有入度为0的顶点入栈 - 当栈不为空时,弹出栈顶顶点,并输出 - 将该顶点的所有邻接点的入度减1,若减为0则入栈 - 重复直到栈空 拓扑排序过程中,我们同时记录拓扑序列,并计算每个事件的最早发生时间ve。 ### 3. 计算事件最早发生时间ve - 初始化:源点(入度为0)的ve=0,其他事件ve=0(实际应该是负无穷,但初始化0,通过后续计算更新) - 按拓扑顺序遍历每个顶点,对于每个顶点v: - 遍历v的所有邻接点w(即v-&gt;w的有向边): - 若ve[v] + weight(v-&gt;w) &gt; ve[w],则更新ve[w] = ve[v] + weight(v-&gt;w) ### 4. 计算事件最迟发生时间vl - 初始化:汇点(出度为0)的vl值等于它的ve值(因为关键路径长度就是汇点的ve),其他事件vl初始化为一个很大的数(如INF,但实际可以用汇点的ve值初始化,因为vl不能大于汇点的ve) -拓扑排序的逆序(即从汇点往前推)遍历每个顶点: - 遍历v的所有邻接点w(即v-&gt;w的边),实际上这里我们需要逆邻接表?或者另一种方法:遍历每个顶点时,我们查找其邻接点,然后更新当前顶点的vl值。但是注意,我们需要的是从后继节点更新当前节点。因此,我们使用逆邻接表会更方便。如果没有逆邻接表,可以在原上遍历所有边,对于每条边&lt;u, v&gt;(表示u-&gt;v),用v的vl值减去边的权值来更新u的vl值(取最小值)。但逆拓扑顺序下,每个顶点的后继节点的vl值已经计算完毕。 - 因此,具体步骤: - 初始化:vl[i] = ve[汇点](对于所有i,但后续会更新) - 按拓扑序列的逆序处理每个顶点v: - 对于每一个以v为弧尾的边(即v-&gt;w),更新vl[v] = min(vl[v], vl[w] - weight(v-&gt;w)) 注意:由于我们可能没有建逆邻接表,所以可以在原邻接表上,对于每个顶点v,遍历其邻接点w(即v-&gt;w),然后更新v的vl值?但这样不行,因为更新v的vl值需要用到所有后继节点的vl值,所以我们可以在原邻接表基础上,再建一个逆邻接表,或者在计算vl时,我们遍历整个的所有边来更新前驱节点的vl值。但更高效的方法是建立逆邻接表,然后对每个顶点v,遍历其逆邻接表(即所有指向v的边,这些边的起点是v的前驱),然后更新前驱节点的vl值?实际上,在计算vl时,我们是用后继节点的vl值来更新当前节点的vl值,所以我们需要当前节点的后继节点信息(也就是原邻接表)。所以,对于每个顶点v,我们遍历v的邻接点w(即v的后继),然后更新v的vl值:vl[v] = min(vl[v], vl[w] - weight(v-&gt;w))。注意,在逆拓扑排序中,w的vl值已经计算过了。 ### 5. 计算活动的最早开始时间ee和最迟开始时间el - 活动(边)的最早开始时间ee:等于该活动的起点所表示的事件的最早发生时间。即对于边&lt;v, w&gt;,有ee(&lt;v,w&gt;) = ve[v] - 活动的最迟开始时间el:等于该活动的终点所表示的事件的最迟发生时间减去该活动所需时间。即对于边&lt;v, w&gt;,有el(&lt;v,w&gt;) = vl[w] - weight(v-&gt;w) ### 6. 确定关键活动 - 对于每个活动(边),计算其时间余量:slack = el - ee(或el-ee,由于ee=ve[v],而el=vl[w]-weight,所以slack= (vl[w]-weight) - ve[v] = vl[w] - ve[v] - weight) - 若时间余量等于0(即slack==0),则该活动为关键活动,所有关键活动组成的路径即为关键路径。 ### 代码框架 下面我们给出关键路径计算的代码框架(不包含的构建和拓扑排序的具体实现细节,只给出关键步骤): ```c // 拓扑排序并计算ve int TopologicalSort(ALGraph G, int ve[]) { // 初始化栈,将入度为0的顶点入栈 int stack[MAX_VERTEX_NUM], top = 0; for (int i = 0; i &lt; G.vexnum; i++) { if (G.vertices[i].in == 0) { stack[top++] = i; } } int count = 0; // 记录已输出的顶点数 int topo[MAX_VERTEX_NUM]; // 存储拓扑序列 // 初始化ve数组为0(假设事件的最早发生时间从0开始) for (int i = 0; i &lt; G.vexnum; i++) { ve[i] = 0; } while (top &gt; 0) { int v = stack[--top]; topo[count++] = v; // 将v保存到拓扑序列中 // 遍历v的所有邻接点 ArcNode *p = G.vertices[v].firstarc; while (p != NULL) { int w = p-&gt;adjvex; // 后继节点 // 更新w的入度 G.vertices[w].in--; if (G.vertices[w].in == 0) { stack[top++] = w; } // 更新w的最早发生时间 if (ve[v] + p-&gt;weight &gt; ve[w]) { ve[w] = ve[v] + p-&gt;weight; } p = p-&gt;nextarc; } } if (count &lt; G.vexnum) { return 0; // 有环,拓扑排序失败 } else { return 1; // 成功 } } // 关键路径算法 void CriticalPath(ALGraph G) { int ve[MAX_VERTEX_NUM]; // 事件最早发生时间 int vl[MAX_VERTEX_NUM]; // 事件最迟发生时间 // 步骤1:拓扑排序并计算ve if (!TopologicalSort(G, ve)) { printf(&quot;网中有环,无法计算关键路径\n&quot;); return; } // 步骤2:初始化vl数组 // 找到汇点:在拓扑序列的最后一个顶点(因为拓扑序列中汇点一定是最后一个,因为汇点出度为0,所以拓扑序列最后出现) int max_length = 0; for (int i = 0; i &lt; G.vexnum; i++) { if (ve[i] &gt; max_length) { max_length = ve[i]; } } // 初始化vl数组为最大值(汇点的ve值,即整个工程的最短工期) for (int i = 0; i &lt; G.vexnum; i++) { vl[i] = max_length; } // 步骤3:按照拓扑序列的逆序计算vl // 注意:在拓扑排序函数中,我们得到了拓扑序列topo数组,但是我们在TopologicalSort函数中没有返回topo数组,这里假设已经保存了拓扑序列 // 由于上面的TopologicalSort函数没有返回topo数组,我们这里重新写一个拓扑排序函数来获取拓扑序列?或者我们修改TopologicalSort函数使其返回拓扑序列。为了简化,我们假设已经有一个拓扑序列数组topo,并且count等于顶点数(存储了拓扑序列) // 修改:在TopologicalSort函数中,我们将topo数组作为参数传入,并返回成功否。这里我们重新写一个函数,或者将上面的函数修改为返回拓扑序列。我们这里为了简洁,重新写一个包含拓扑序列的版本,但为了不重复,我们假设已经得到了拓扑序列topo[]和顶点总数count(即G.vexnum) // 由于上面的TopologicalSort函数没有保存拓扑序列,我们修改一下,这里省略修改步骤,假设已经得到拓扑序列topo,并且我们知道拓扑序列中最后一个顶点就是汇点(但可能有多个汇点?通常只有一个汇点,如果有多个,那么vl初始化时用最大值,然后每个汇点的vl都等于其ve值。但我们上面的vl初始化全部为max_length(即所有ve的最大值),然后按照逆拓扑序更新。 // 重新获取拓扑序列(这里省略,假设已经得到topo数组,且长度为G.vexnum) // 逆序遍历拓扑序列 for (int i = G.vexnum - 1; i &gt;= 0; i--) { int v = topo[i]; // 当前顶点 ArcNode *p = G.vertices[v].firstarc; while (p != NULL) { int w = p-&gt;adjvex; // v的后继节点 // 用w的vl值更新v的vl值 if (vl[w] - p-&gt;weight &lt; vl[v]) { vl[v] = vl[w] - p-&gt;weight; } p = p-&gt;nextarc; } } // 步骤4:计算活动的最早开始时间ee和最迟开始时间el,并判断关键活动 printf(&quot;关键活动:\n&quot;); for (int u = 0; u &lt; G.vexnum; u++) { ArcNode *p = G.vertices[u].firstarc; while (p != NULL) { int v = p-&gt;adjvex; int ee = ve[u]; // 活动&lt;u,v&gt;的最早开始时间 int el = vl[v] - p-&gt;weight; // 活动&lt;u,v&gt;的最迟开始时间 if (ee == el) { // 时间余量为0,是关键活动 printf(&quot;活动&lt;%d,%d&gt;,权值:%d\n&quot;, u, v, p-&gt;weight); } p = p-&gt;nextarc; } } } ``` ### 注意事项 - 上述代码是简化版本,实际实现时需要处理的构建、拓扑序列的存储等问题。 - 在计算vl时,遍历邻接点更新vl[v]时,由于是逆拓扑排序,所以每个后继节点w的vl值已经计算好了。 -拓扑排序中,我们修改了中顶点的入度,所以如果需要多次使用该,应该备份入度信息。 ### 实例验证 引用[4]中示的AOE网(顶点0到8),通过上述算法应该能找出关键路径上的关键活动。 ### 参考文献 以上实现参考了数据结构经典算法[^3][^4]。 &sect;&sect;相关问题&sect;&sect; 1. 如何处理有多个源点或汇点的AOE网关键路径计算? 2. 关键路径算法的时间复杂度和空间复杂度是多少? 3. 如何输出所有关键路径(可能存在多条)? &lt;/think&gt;以下是使用C语言计算AOE网关键路径的完整算法实现,结合拓扑排序和动态规划技术逐步求解关键路径。核心算法基于事件的最早/最迟发生时间计算($ve$/$vl$)和关键活动判定。 ```c #include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; #define MAX_VERTEX 20 #define INF 0x3f3f3f3f // 边表节点(表示活动) typedef struct ArcNode { int adjvex; // 指向的顶点下标 int weight; // 活动持续时间 struct ArcNode* next; // 下一条边 } ArcNode; // 顶点表节点(表示事件) typedef struct { int data; // 事件信息 int inDegree; // 入度 ArcNode* firstEdge; // 第一条邻接边 } VertexNode; // AOE网结构 typedef struct { VertexNode vertices[MAX_VERTEX]; int vexNum, arcNum; // 顶点数、边数 int source, sink; // 源点(入度=0)、汇点(出度=0) } AOE_Graph; // 拓扑排序并计算ve(事件最早发生时间) int TopoSort(AOE_Graph* G, int ve[]) { int stack[MAX_VERTEX], top = -1; int count = 0; // 已排序顶点数 // 初始化ve数组 for (int i = 0; i &lt; G-&gt;vexNum; i++) { if (G-&gt;vertices[i].inDegree == 0) { stack[++top] = i; // 入度为0的顶点入栈 ve[i] = 0; // 源点ve=0 } else { ve[i] = 0; } } while (top != -1) { int v = stack[top--]; count++; // 更新所有后继顶点的ve值 ArcNode* p = G-&gt;vertices[v].firstEdge; while (p != NULL) { int w = p-&gt;adjvex; if (--G-&gt;vertices[w].inDegree == 0) { stack[++top] = w; } if (ve[v] + p-&gt;weight &gt; ve[w]) { ve[w] = ve[v] + p-&gt;weight; // 更新最早时间 } p = p-&gt;next; } } return (count == G-&gt;vexNum); // 返回是否有环(1=无环) } // 计算关键路径 void CriticalPath(AOE_Graph* G) { int ve[MAX_VERTEX]; // 事件最早发生时间 int vl[MAX_VERTEX]; // 事件最迟发生时间 // 步骤1: 拓扑排序并计算ve if (!TopoSort(G, ve)) { printf(&quot;存在环,无法计算关键路径\n&quot;); return; } // 步骤2: 初始化vl数组 for (int i = 0; i &lt; G-&gt;vexNum; i++) { vl[i] = ve[G-&gt;sink]; // 初始化为汇点时间 } // 步骤3: 逆拓扑序计算vl(从汇点回溯) int stack[MAX_VERTEX], top = -1; for (int i = 0; i &lt; G-&gt;vexNum; i++) { if (G-&gt;vertices[i].inDegree == 0) { stack[++top] = i; // 重新入栈源点 } } while (top != -1) { int v = stack[top--]; ArcNode* p = G-&gt;vertices[v].firstEdge; while (p != NULL) { int w = p-&gt;adjvex; if (vl[w] - p-&gt;weight &lt; vl[v]) { vl[v] = vl[w] - p-&gt;weight; // 更新最迟时间 } p = p-&gt;next; } } // 步骤4: 识别关键活动 printf(&quot;关键路径活动: \n&quot;); for (int u = 0; u &lt; G-&gt;vexNum; u++) { ArcNode* p = G-&gt;vertices[u].firstEdge; while (p != NULL) { int v = p-&gt;adjvex; int ee = ve[u]; // 活动最早开始时间 int el = vl[v] - p-&gt;weight; // 活动最迟开始时间 // 时间余量=0即为关键活动 if (ee == el) { printf(&quot;⚡ [%d]&rarr;[%d] 耗时:%d\n&quot;, G-&gt;vertices[u].data, G-&gt;vertices[v].data, p-&gt;weight); } p = p-&gt;next; } } printf(&quot;项目总工期: %d\n&quot;, ve[G-&gt;sink]); } /* 创建AOE网示例 */ void CreateAOE(AOE_Graph* G) { // 初始化顶点和边(此处省略具体构建代码) // 需设置: // G-&gt;vertices[i].inDegree // G-&gt;vertices[i].firstEdge // G-&gt;source, G-&gt;sink } ``` ### 算法核心原理(四步法) 1. **拓扑排序$ve$计算** 按拓扑顺序递推事件最早发生时间: $$ ve[j] = \max_{\forall i \in \text{前驱}(j)} \{ ve[i] + w(i,j) \} $$ 源点$ve[\text{source}] = 0$[^3]。 2. **逆拓扑序计算$vl$** 从汇点回溯事件最迟发生时间: $$ vl[i] = \min_{\forall j \in \text{后继}(i)} \{ vl[j] - w(i,j) \} $$ 汇点$vl[\text{sink}] = ve[\text{sink}]$[^4]。 3. **关键活动判定** 对活动$a_{i,j}$满足: ``` ee(i,j) = ve[i] // 活动最早开始时间 el(i,j) = vl[j] - w(i,j) // 活动最迟开始时间 if (ee == el) &rarr; 关键活动 ``` 4. **关键路径生成** 所有关键活动构成关键路径,总工期$= ve[\text{sink}]$。 ### 时间复杂度分析 | 步骤 | 时间复杂度 | 空间复杂度 | |----------------|------------|------------| | 拓扑排序 | $O(V+E)$ | $O(V)$ | | $ve/vl$计算 | $O(V+E)$ | $O(V)$ | | 关键活动识别 | $O(E)$ | $O(1)$ | | **总计** | $O(V+E)$ | $O(V)$ | &gt; **提示**:实际应用中需添加邻接表构建函数(`CreateAOE()`)和栈操作细节。关键路径反映了项目的最短工期,任何关键活动的延迟都会导致整体延期[^4]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值