# Lecture8 - 图

  • 考试重点

  • 概念:选择填空为主

  • 重点是理解

  • 连通性、各种性质

  • 强连通性、加权图、生成树 概念判断

  • 邻接表、邻接矩阵要求掌握

  • 邻接二重表不考

  • 遍历要求

  • 最小生成树算法 要求掌握重点

    • kruskal
    • prim
  • 最短路径 要求掌握重点

    • Dijkstra
    • BellmanFord
    • Floyed
  • 证明都不要求

  • 算法思想:除了动态规划不要求

    • 基本就是考考概念
  • BellmanFord 算法 Floyed 算法的算法流程要求

  • 活动网络常考

    • 包括拓扑排序

    • AOV 网络:概念多,爱出题

      关键路径算法

代码再仔细看看

# 图的定义

  1. Graph = (V, E)

    1. V: nonempty finite vertice set(顶点集) 一个非空确定顶点个数的集合
    2. E: edge set (边集)
  2. Undirected Graph 无向图

    if the tuple denoting an edge is unordered, then (v1, v2) and (v2, v1) are the same edge.

# 有向图

If the tuple (元组) denoting an edge is ordered, then <v1,v2> and <v2,v1> are different edges.

(如果表示的边的元组是有序的,也就是 <v1,v2> 和 < v2,v1 > 是不同的)

v1: 始点

v2: 终点

img

In a directed graph with n nodes, the number of edges <=n*(n-1). If “=” is satisfied, then it is called a complete directed graph*.

(一个有 n 个节点的有向图,其边的个数 <= n(n-1),如果相等,则为是一个完全有向图 *)

完全图 (有向完全图): 指有向图中每两个顶点都相互指向。

image-20221209141507512

# 无向图

If the tuple denoting an edge is unordered, then (v1,v2) and (v2,v1) are the same edge.

(如果表示边的元组是无序的,则 (v1,v2) 和 (v2,v1) 是相同的边。)

In an undirected graph with n nodes, the number of edges <= n*(n-1)/2. If “=” is satisfied, then it is called a complete undirect graph.

(在一个有 n 个顶点的无向图中,边的个数 <= n(n-1)/2,如果刚好相等,则被称为完全无向图)

完全图 (无向完全图): 就是指每两个顶点之间都有一条边。

img

# 其他图

以下两种图在我们的数据结构中不进行讨论

img

不考虑 自环 (ring) 和 多重边 的多重图。

# 概念 —— 顶点的度数(入度和出度)

image-20230214131905817

  1. 对于无向图只有度数,而对于有向图不仅仅有入度,还有出度。

  2. degree dv of vertex v, TD(v): is the number of edges incident on vertex v. In a directed graph :

    (顶点 v 的度数为 dv,TD (V) 是顶点 v 的度数,在有向图中)

    1. in-degree of vertex v is the number of edges incident to v, ID(v).

      (顶点 v 的入度是指向顶点 v 的边的个数)

    2. out-degree of vertex v is the number of edges incident from the v, OD(v).

      (顶点 v 的出度从 v 出发的边的个数)

  3. 性质:(度数)TD(v)=ID(v)+OD(v)

    度数可以理解为,这个顶点的边

img

# 图的性质

img

所有的度数加起来是边的个数的两倍。

# 子图

image-20230214132334890

Graph G=(V,E),G’=(V‘,E‘), if V’包含于 V, E’包含于 E, and the vertices incident on the edges in E’ are in V’, then G’ is the subgraph of G.

如果图 G 和图 G’,如果 V’包含于 V,E’包含于 E, 并且 E’中顶点的边也在 G’中,那么 G’是 G 的子图

img

# 路径 (path)

image-20230214132326704

A sequence of vertices P=i1,i2,……ik is an i1 to ik path in the graph of graph G=(V,E) iff the edge(ij,ij+1)is in E for every j, 1 <= j < k.

在图 G=(V,E) 中,如果每 j 的边 (ij,ij+1) 在 E 中,1<= j< k,则顶点序列 P=i1,i2,…,ik 是 i1 到 ik 的路径。

# 简单路径和环 (Simple path and cycle)

image-20230214132416033

  1. A Simple path is a path in which all vertices except possibly the first and last , are different.

    (简单路径:路径除了第一个和最后一个顶点中没有出现相同的顶点)

  2. A Simple cycle is a simple path with the same start and end vertex.

    (简单回路:起点和终点相同的时候的简单路径)

    image-20230214132549580

# 连通图和连通分量 (Connected graph & Connected component)

  1. In a undirected graph, if there is a path from vertex v1 to v2, then v1 and v2 are connected.

    (在无向图中,如果 v1 到 v2 之间有一条路径,那么 v1 和 v2 是连通的)

  2. In a undirected graph ,if two arbitrary vertices are connected, then the graph is a connected graph

    (在无向图中,如果任意两个顶点是连通的,则该图是连通图)

img

极大连通子图:就是结点个数最多的连通的子图。

# 强联通图和强联通分量 (Strong connected graph and strongly connected component)

image-20230214132823488

  1. 强连通图

    有向图 digraph 是强连通的,当它包含从 i 到 j 和从 j 到 i 的有向路径时,对于每对不同的顶点 i 和 j

    简单来说就是既要过的去,也要回得来

  2. 强连通分量

    The maximum strong connected subgraph (极大强连通子图) of a non-strongly connected graph is called strongly connected conponent (强连通分量).

    (一个非强连通图的最大强连通子图 (South-South-PosialSuth-Posiple Fug) 称为强连通构 (Suth-Posiple Stand))

# 加权图 (Network)

  1. When weights and costs are assigned to edges, the resulting data object is called weighted graph and weighted digraph.

    (当权值和代价分配给边时,得到的数据对象称为加权图加权有向图。)

  2. The term network refers to weighted connected graph and weighted connected digraph.

    (加权图是用来代指加权连通图和加权连通有向图)

img

# 生成树 (Spanning tree)

A spanning tree of a connected graph is its minimum connected subgraph(极小连通子图). An n-vertex spanning tree has n-1 edges.

(连通图的生成树是其极小连通子图。n 顶点生成树有 n-1 条边。)

保持联通的最小边数的图

img

# ADT Graph and Digraph

image-20221209142531480

# Representation of graphs and diagraphs

image-20230214133528703

# Adjacency Matrix 邻接矩阵

graph 无向图

digraph 有向图

# 无向图

image-20221209143030813

image-20221209143125763

  1. 无向图的邻接矩阵是一个对称矩阵
  2. 无向图的每个顶点的度数等于矩阵中每一行的和

# 有向图

image-20221209143144805

出度,一行的和;

入度,一列的和

# 加权图

image-20221209143821067

image-20221209143829081

# 代码实现 —— 数组

image-20221209143933283

dist 距离(权)

template<class NameType, class DistType> 是一种模板,意味着 class 用 NameType 和 DistType 进行定义

SeqList 是顺序表的意思

image-20221209143953116

image-20221209144000077

# 代码实现 —— 链表

邻接表

image-20221209144907924

image-20221209144916071

# 声明

image-20221209145022064

image-20221209145032715

image-20221209145043254

image-20221209145423763

# 构造函数

image-20221209145442706

# 找到在顶点表的位置

image-20221209145450950

# 给出顶点 V 的第一个邻接顶点的位置

image-20230214134749181

# 找到下一个邻居

image-20221209145509374

# 【不考】邻接多重表(adjacency multilist)

  1. 在无向图中,如果边数为 m, 则在邻接表表示中需 2m 个单位来存储。为了克服这一缺点,采用邻接多重表,每条边用一个结点表示.
    • 其中的两个结点号就是边的两个点。
    • path1 指向的就是同样始点 (vertex1),顺序终点的结果。
    • path2 执行的是以 vertex2 为始点顺序向下的。
  2. Eg. 使用正常的邻接表,则右边应该有 10 个点,但是多重表就是只有 5 个表
    • 默认情况下边的始点的编号要小于终点的编号大小。

image-20221209151914910

image-20221209151930870

img

  1. 邻接表和邻接多重表之间的区别在于有几个顶点,有几个边。
  2. data 部分只记录 first-in 和 first-out,也就是第一条出边和第一条入边

# 图的遍历与连通性

image-20221209144224188

# 算法思想

从图中某个顶点 V0 出发,访问它,然后选择一个 V0 邻接到的未被访问的一个邻接点 V1 出发深度优先遍历图,当遇到一个所有邻接于它的结点都被访问过了的结点 U 时,回退到前一次刚被访问过的拥有未被访问的邻接点 W, 再从 W 出发深度遍历,…… 直到连通图中的所有顶点都被访问过为止.

img

递归方法实现 算法中用一个辅助数组 visited []:

0: 未访问

1: 访问过了

我们假设图为连通图

image-20221209153919037

image-20221209153931254

算法分析

用邻接表表示 O (N+E)

用邻接矩阵表示 O (n2)

# 思想

从图中某顶点 V0 出发,在访问了 V0 之后依次访问 v0 的各个未曾访问过的邻接点,然后分别从这些邻接点出发广度优先遍历图,直至图中所有顶点都被访问到为止.

img

算法同样需要一个辅助数组 visited [] 表示顶点是否被访问过。还需要一个队列,记正在访问的这一层和上一层的顶点。算法显然是非递归的.

image-20221209154815385

image-20221209154826520

算法分析

用邻接表表示 O (N+E)

用邻接矩阵表示 O (n2)

# 连通分量

连通图:任意两个顶点是连通的。

以上讨论的是对一个无向的连通图或一个强连通图的有向图进行遍历,得到一棵深度优先或广度优先生成树。但当无向图(以无向图为例)为非连通图时,从图的某一顶点出发进行遍历(深度,广度)只能访问到该顶点所在的最大连通子图(即连通分量)的所有顶点。

image-20221209155103515

image-20221209160239621

加一个循环语句即可

# 最小生成树 minimum-cost spinning tree MST

# 生成树

# 生成树的定义

  1. G =(V,E) 是一个连通的无向图 (或是强连通有向图) 从图 G 中的任一顶点出发作遍历图的操作,把遍历走过的边的集合记为 TE (G),显然 **G‘=(V,TE)** 是 G 之子图, G‘被称为 G 的生成树 (spanning tree),也称为一个连通图.
  2. n 个结点的生成树有 n-1 条边。
  3. 生成树的代价 (cost):TE (G) 上诸边的代价之和
  4. 生成树不唯一

img

# 最小代价生成树

各边权的总和为最小的生成树

# 贪心 (Grandy) 求解最小代价生成树

6 个城市已固定,现从一个城市发出信息到每一个城市如何选择或铺设通信线路,使花费 (造价) 最低。

img

img

两个算法:Prim, Kruskal.

它们都使用了逐步求解(贪心算法)的策略。

# 贪心策略的具体内容

image-20230214163354394

Grandy 策略:

设:连通网络 N={V,E}, V 中有 n 个顶点。

  1. 先构造 n 个顶点,0 条边的森林 F =
  2. 每次向 F 中加入一条边。该边是一端在 F 的某棵树 Ti 上而另一端不在 Ti 上的所有边中具有最小权值的边。 这样使 F 中两棵树合并为一棵,树的棵数 - 1
  3. 重复上述操作 n-1 次

去掉所有边,每次加入的边是当前最小的边,并且保证这个边不是回边。

# 最小生成树的类声明

image-20221209163836664

image-20221209163925674

# Kruskal 算法 (对边进行排序,然后生成)

image-20230214163725826

把无向图的所有边排序

一开始的最小生成树为

img

在 E 中选一条代价最小的边 (u,v) 加入 T,一定要满足 (u,v) 不和 TE 中已有的边构成回路

img

一直到 TE 中加满 n-1 条边为止。

img

image-20221209164108267

邻接矩阵

# 代码实现

image-20221209164144977

image-20221209164151523

image-20221209164158409

排序:建立最小堆

出堆操作:找到最小值

find 操作:?

union 操作:添加到数集中

# Prim 算法(任何起点,选可通达的最小权重的边)

image-20230214165855967

设:原图的顶点集合 V (有 n 个) 生成树的顶点集合 U (最后也有 n 个),一开始为空 TE 集合为 {}

步骤:

  1. U={1} 任何起始顶点,TE={}
  2. 每次生成 (选择) 一条边。这条边是所有边 (u,v) 中代价 (权) 最小的边, u∈U,v∈V-U TE=TE+[(u,v)]; U=U+[v]
  3. 当 U≠V,返回上面一个步骤

# 例子

img
img

  1. 一开始只考虑从 1 号顶点到其他顶点之间的边。
    • 泛泛而言,考虑 u 和 v 之间的边

img
img

# 最小生成树不唯一

  1. 对于一般的图来讲,最小生成树不唯一。
  2. 所以相应的 Prime 算法和 Kruskal 算法也会出现多解的情况

image-20230214170520688

# prim 算法实例

image-20230214171319803

image-20230214171407861

image-20230214171416308

image-20230214171427973

image-20230214171433658

image-20230214171439317

image-20230214171444611

image-20230214171452042

# Prim 算法实现

image-20230214172406455

image-20230214172414957

image-20230214172423013

# 最短路径

  1. 设 G=(V,E) 是一个带权图 (有向,无向),如果从顶点 v 到顶点 w 的一条路径为 (v,v1,v2,…,w),其路径长度不大于从 v 到 w 的所有其它路径的长度,则该路径为从 v 到 w 的最短路径。
  2. 背景:在交通网络中,求各城镇间的最短路径。
  3. 三种算法:
    1. 边上权值为非负情况的从一个结点到其它各结点的最短路径 (单源最短路径)(Dijkstra 算法)
    2. 边上权值为任意值的单源最短路径【边上存在负权值,但是没有负环】【不能出现小于零的环(负环),此时最小路径没有意义】
    3. 边上权值为非负情况的所有顶点之间的最短路径

# 含非负权值的单源最短路径 (Dijkstra)

每次从 「未求出最短路径的点」中 取出 距离距离起点 最小路径的点,以这个点为桥梁 刷新「未求出最短路径的点」的距离

  1. 问题

img

# 贪心思想

起点 V0,首先直接连接,不管是否直接连接。

img

排好序后,V0-V1 10 已经是最小的了,不可能再找到更短的路径

img

接下来,尝试 V0-v2 通过 V1 绕会不会比原来的更短 (考虑 V1-V2 直连),V0-V4 从 V1 绕会不会比原来更短 (考虑 V2-V3 直连),如果短则更新,此时 V0-V3 是三者中最小值,所以选择 V0-V3。

img

尝试绕行 V3,计算直连,更新掉,然后重复

img

红色是已经选择好的,绿色是绕行选择。

img

贪心:当前新产生的一条最短路径能否使已有路径在一步以内变短。

进一步思考,就是只进行一步,不进行多步。

总体来讲:不可能走更长的路径,然后回来

img
img

数值更新,路径数组对应位置更新

img

# 代码实现

image-20230214173554247

image-20230214173607549

const int NumVertices = 6;// 大于所有边的权重的值
class graph {
    private:
        int Edge[NumVertices][NumVertices]; 
        int dist[NumVertices];
        int path[NumVertices];
        int S[NumVertices];
    public:
        void shortestpath(int,int);
};
void Graph::shortestpath(int n,int v) {  
    for( int i=0; i<n; i++) {
        //v 为当前节点,dist 数组是表示距离的数组
        // 遍历 n 次
        dist[i] = Edge[v][i];
        s[i] = 0;
        if( i!=v && dist[i]< MAXNUM )
            path[i]= v;// 如果可达,则用 path 数组记录下路径
        else
            path[i]=-1;// 如果不可达,则用 path 数组记录下不可达 (-1)
        }
        s[v]=1;
        dist[v]=0;
        // 表示访问过当前节点,并且距离为 0
        for( i=0; i<n-1; i++) {
            float min=MAXNUM;
            int u = v;
            for( int j = 0;  j < n;  j++)
                if( !s[j] && dist[j]<min ) {
                    // 如果结点 j 还没有访问过,并且 dist [j] 小于最小值
                    u = j;
                    min = dist[j];
                }
            s[u]=1;
            for ( int w=0; w<n; w++)
                if( !s[w] && Edge[u][w] < MAXNUM && dist[u]+Edge[u][w] < dist[w]) {
                    //dist [u] 就是起点到 u 的距离,下面是关键条件
                    dist[w]=dist[u]+Edge[u][w];
                    path[w]=u;
                }
        }//for
}

image-20230214173613313

# 边上权值为任意值的单源最短路径(贝尔曼 - 福特)BellemanFord

image-20230214174453911

distk 从源点 v 开始最多经过不构成带负长度边回路 k 条边的最短路径长度

image-20230214174554207

递推公式

img

  1. 更新的时候都是根据前面结果,遍历计算存储
  2. 所有第 k 步,只受第 k-1 步的影响

image-20230214175305102

void  Graph::BellmanFord(int n, int v) {
    // 动态规划
    for(int i=0;i<n;i++) {
        // 初始化 dist 距离数组
        dist[i]=Edge[v][i];
        if(i!=v && dist[i]<MAXNUM) path[i]=v;
        // 初始化路径数组 
        else path[i]=-1; }
    
    for (int k = 2;k < n;k++)
        for(int u = 0;u < n;u++)
            if(u!=v)
                for(i=0;i < n;i++)
                    // 一直算到 n-1 步
                    if (Edge[i][u]<>0 && Edge[i][u]<MAXNUM && dist[u]> dist[i]+Edge[i][u]){
                        dist[u]=dist[i]+Edge[i][u];
                        path[u]=i;
                    }
}
  1. 时间复杂度:O (n3)

# 所有顶点之间的最短路径(Floyed)

  1. 前提:各边权值均大于 0 的带权有向图
    • 每个顶点到自己的代价为 0
  2. 方法:
    1. 把有向图的每一个顶点作为源点,重复执行 Dijkstra 算法 n 次,执行时间为 O (n3)
    2. Floyed 方法,算法形式更简单些,但是时间仍然是 O (n3)

img
img
img

  1. 简单来说就是:每次都会选择一个中介点,然后遍历整个数组,更新相应的需要更新的数组。

# floyed 算法实现

image-20230214193445295

void Graph::Alllength(int n) {
    for(int i=0; i<n; i++)
        for(int j=0; j<n; j++) {
            a[i][j]=Edge[i][j];
            if(i!=j&&a[i][j]<MAXNUM) path[i][j] = i;// 路由表
            else path[i][j]=0;
            }
    for(int k=0; k<n; k++)
        for(int i=0; i<n; i++)
            for(int j=0; j<n; j++)
                if( a[i][k]+a[k][j]<a[i][j] ) {
                    a[i][j]=a[i][k]+a[k][j];
                    path[i][j]=path[k][j];
                }
}
矩阵S中的元素a[i][j]表示顶点i(第i个顶点)到顶点j(第j个顶点)的距离。矩阵P中的元素b[i][j],表示顶点i到顶点j经过了b[i][j]记录的值所表示的顶点。
  1. 算法复杂度:O (n3)
  2. 参考:Floyed 算法

# 6.4. Floyed 算法参考

  1. 最短路径问题

# 活动网络 Activity Network(常考)

  1. 用顶点表示活动的网络 (AOV 网络)
  2. 用边表示活动的网络 (AOE 网络)
  3. 用顶点表示活动的网络

# 7.1. AOV 网络 Activity On Vertex network

image-20230214200449558

img

# AOV 网络结构

  1. 图中表示课程 (活动),有向边 (弧) 表示先决条件。 若课程 i 是课程 j 的预修课程,则图中有弧 < i,j>
  2. AOV 网 (Activity On Vertex network)
    • 用顶点表示活动,用弧表示活动间的优先关系的有向图称为 AOV 网。
  3. 直接前驱,直接后继
    • <i,j> 是网中一条弧,则 i 是 j 的直接前驱,j 是 i 的直接后继。
  4. 前驱,后继
    • 从顶点 i-> 顶点 j 有一条有向路径,则称 i 是 j 的前驱,j 是 i 的后继。
  5. AOV 网中,不应该出现有向环

# AOV 图的拓扑排序

  1. 有向图 G=(V,E),V 里结点的线性序列 (vi1,vi2,…,vin), 如果满足:在 G 中从结点 vi 到 vj 有一条路径,则序列中结点 Vi 必先于结点 vj ,称这样的线性序列为一拓扑序列
  2. 不是任何有向图的结点都可以排成拓扑序列,有环图是显然没有拓扑排序的。

image-20230214200713804

# 拓扑算法思想

  1. 从图中选择一个入度为 0 的结点输出之。(如果一个图中,同时存在多个入度为 0 的结点,则随便输出任意一个结点)
  2. 从图中删掉此结点及其所有的出边
  3. 反复执行以上步骤
    1. 直到所有结点都输出了,则算法结束
    2. 如果图中还有结点,但入度不为 0,则说明有环路

# 拓扑算法实现

  1. 具体实现算法:AOV 网用邻接表来实现

    数组 count 存放各顶点的入度

  2. 并且为了避免每次从头到尾查找入度为 0 的顶点,建立入度为 0 的顶点栈,栈顶指针为 top,初始化时为 - 1.

img

img

image-20230214202307026

image-20230214203538043

没看懂,甚至可能是错的

//AOV 网的声明
class Graph {
    friend class <int,float> vertex;
    friend class <float> Edge;
    private:
        vertex <int, float>* nodeTable ; 
        int* count ;// 存放入度
        int n ;
    public:
        Graph ( const int vertices=0): n (vertices) {
            NodeTable=new vertex <int, float> [n];
            count=new int[n];
        }
        void topologicalorder() ;
};
// 拓扑排序
void Graph :: Topologicalsort () {
    int top=-1; //top 是当前找到的入度为 0 的点,top==-1 表示找不到入度为 0 的点
    // 初始化无入度顶点
    for ( int i=0; i<n ;i++ )
        if (count[i]==0){
            count[i]= top ;
            top = i;
        }
    // 进行正式排序
    for (int i=0 ; i<n ; i++)
        if (top == -1){
            // 如果 top 变为 - 1,那么显然存在回路
            cout <<"Network has a cycle"<< endl;
            return;
        }else{
            int j = top;
            top = count[top];
            cout<<j<<endl;
            Edge<float>* l = NodeTable[j].adj;
            while(l) {
                int k = l.dest;
                if ( --connt[k] == 0)
                    // 如果完成所有节点的删除
                    count[k] = top;
                    top = k;
            } 
            l = l->link;
        }
    }
}

https://blog.csdn.net/lisonglisonglisong/article/details/45543451

java 实现

image-20230214203659497

void topsort() throws CycleFound {
    Queue q;// 队列或者栈都可以
    int counter = 0;
    Vertex v, w;
    q = new Queue();
    for each vertex v
        if( v.indegree == 0 )
            q.enqueue(v);
    while(!q.isEmpty()) {
        v = q.dequeue();
        v.topNum = ++counter;//Assign next number 
        for each w adjacent to v
            if( --w.indegree == 0 ) 
                q.enqueue;
    }
    if( counter != NUM_VERTICES )
        throw new CycleFound();
}

# 算法复杂度分析

  1. 算法分析:n 个顶点,e 条边
  2. 建立链式栈 O (n),每个结点输出一次,每条边被检查一次 O (n+e),所以为:O (n+n+e)

image-20230214201617082

# 7.2. AOE 网络 Activity On Edge Network

  1. 用边表示活动的网络 (AOE 网络,Activity On Edge Network) 又称为事件顶点网络
  2. 顶点:
    • 表示事件 (event)
    • 事件 —— 状态。表示它的入边代表的活动已完成,它的出边代表的活动可以开始,如下图 v0 表示整个工程开始,v4 表示 a4,a5 活动已完成 a7,a8 活动可开始。
  3. 有向边:表示活动
    • 边上的权 —— 表示完成一项活动需要的时间

img
img

有唯一的入度为 0 的开始节点

有唯一的出度为 0 的完成结点

# 关键路径

  1. 目的:利用事件顶点网络,研究完成整个工程需要多少时间 加快那些活动的速度后,可使整个工程提前完成。
  2. 关键路径:具有从开始顶点 (源点)-> 完成顶点 (汇点) 的最长的路径

# 一些定义

image-20230214204156511

  1. 对于事件:

    1. Ve [i]-表示事件 Vi 的可能最早发生时间:定义为从源点 V0->Vi 的最长路径长度,如 Ve [4]=7 天

    2. Vl [i]-表示事件 Vi 的允许的最晚发生时间:是在保证汇点 Vn-1 在 Ve [n-1] 时刻 (18) 完成的前提下,事件 Vi 允许发生的最晚时间= Ve [n-1]- Vi->Vn-1 的最长路径长度。

      是从最后汇点时间长度 - 两者之间最长路径

img

  1. 解释:
    1. 计算到最后汇点的总共最短时间:找到从源点到汇点的最大路径
    2. 最早 12,因为之前不能做。
    3. 最晚 12,是因为如果这时候不开始,最后完成不了。

image-20230214204912475

对于活动:

  1. e [k]-表示活动 ak=<Vi,Vj > 的可能的最早开始时间。 即等于事件 Vi 的可能最早发生时间。 e [k]=Ve [i]
  2. l [k]-表示活动 ak= <Vi,Vj> 的允许的最迟开始时间 l [k]=Vl [j]-dur (<i,j>);
  3. l [k]-e [k]-表示活动 ak 的最早可能开始时间和最迟允许开始时间的时间余量。也称为松弛时间。 (slack time)
  4. l [k]==e [k]-表示活动 ak 是没有时间余量的关键活动
  5. 一开始的例子中
    1. a8 的最早可能开始时间 e [8]=Ve [4]=7
    2. 最迟允许开始时间 l [8]=Vl [7]-dur (<4,7>) =14-7=7, 所以 a8 是关键路径上的关键活动
    3. a9 的最早可能开始时间 e [9]=Ve [5]=7
    4. 最迟允许开始时间 l [9]=Vl [7]-dur (<5,7>) =14-4=10
  6. 所以 l [9]-e [9]=3, 该活动的时间余量为 3,即推迟 3 天或延迟 3 天完成都不 影响整个工程的完成,它不是关键活动

# 寻找关键路径的算法

image-20230214205249995

  1. 求各事件的可能最早发生时间 从 Ve [0]=0 开始,向前推进求其它事件的 Ve Ve [i]=max {Ve [j]+dur (< Vj,Vi >)}, <Vj,Vi > 属于 S2, i=1,2,…n-1 j S2 是所有指向顶点 Vi 的有向边 < Vj,Vi > 的集合
  2. 求各事件的允许最晚发生时间 从 Vl [n-1]=Ve [n-1] 开始,反向递推 Vl [i]=min {Vl [j]-dur (<Vi,Vj>)}, <Vi,Vj > 属于 S1, i=n-2,n-3,…0 j S1 是所有从顶点 Vi 出发的有向边 < Vi,Vj > 的集合
  3. 以上的计算必须在拓扑有序及逆拓扑有序的前提下进行,求 Ve [i] 必须使 Vi 的所有前驱结点的 Ve 都求得求 Vl [i] 必须使 Vi 的所有后继结点最晚发生时间都求得。
  4. 求每条边 (活动) ak= <Vi,Vj> 的 e [k], l [k] e [k]=Ve [i];l [k]=Vl [j]-dur (<Vi,Vj> ),k=1,2,…e
  5. 如果 e [k]==l [k],则 ak 是关键活动
  6. AOE 网用邻接表来表示,并且假设顶点序列已按拓扑有序与逆拓扑有序排好。如上例:
    • 先正向推,然后反向推回来。(分别计算最早时间和最晚时间)

image-20230214205442329

img
img

# 算法实现

image-20230214205814211

image-20230214205823058

image-20230214205833772

void Graph ::CriticalPath () {
    int i , j ;
    int p, k ;
    float e, l ;
    float * Ve=new float[n];
    float * Vl=new float[n];
    // 初始化 Ve 数组
    for (i=0; i<n ; i++)
        Ve[i]=0;
    // 开始正向拓扑计算
    for (i=0; i<n ; i++) {
        Edge <float> * p=NodeTable[i].adj; 
        while (p!=NULL) {
            k = p.dest;
            if (Ve[i]+p. cost > Ve[k])
                Ve[k]=Ve[i]+p.cost ;
                p=p.link;
        }
    } 
    // 反向 Ve 数组,初始化 Vl 数组
    for (i=0; i<n ; i++)
        Vl[i]=Ve[n-1];
    // 反向计算事件最迟开始时间
    for (i=n-2; i ; i--) {
        p=NodeTable[i].adj;
        while(p!=NULL) {
            k=p. dest;
            if (Vl[k]-p.cost<Vl[i])
                Vl[i]=Vl[k]-p.cost ; 
                p=p. link;
        }
    } 
    // 用来比较最早开始时间和最晚开始时间,确定是否是关键路径
    for (i=0; i<n ;i++) {
        p=NodeTable[i].adj;
        while (p!=NULL) {
            k= p. dest;
            e=Ve[i];
            l=Vl[k]-p. cost;
        if(l==e)
            cout<<"<"<<i<<","<<k<<">"<<"is critical Activity"<<endl;
            p=p.link;
        }
    } 
}

img

#