考研408--数据结构--day11--最小生成树&最短路径问题&拓扑排序&关键路径

(以下内容全部出自上述课程)

目录

  • 最小生成树
    • [1. 分类](#1. 分类)
      • [1.1 广度优先生成树](#1.1 广度优先生成树)
      • [1.2 深度优先生成树](#1.2 深度优先生成树)
      • [1.3 最小生成树](#1.3 最小生成树)
    • [2. 算法实现](#2. 算法实现)
      • [2.1 Prim算法](#2.1 Prim算法)
      • [2.2 Kruskal算法](#2.2 Kruskal算法)
      • [2.3 对比](#2.3 对比)
    • [3. 实现思想](#3. 实现思想)
      • [3.1 Prim算法](#3.1 Prim算法)
      • [3.2 Kruskal算法](#3.2 Kruskal算法)
    • [4. 小结](#4. 小结)
  • 最短路径问题
    • [1. BFS算法](#1. BFS算法)
      • [1.1 概念](#1.1 概念)
      • [1.2 代码实现](#1.2 代码实现)
      • [1.3 小结](#1.3 小结)
    • [2. Dijkstra算法](#2. Dijkstra算法)
      • [2.1 优化](#2.1 优化)
      • [2.2 实现过程](#2.2 实现过程)
      • [2.3 时间复杂度](#2.3 时间复杂度)
    • [3. Floyd算法](#3. Floyd算法)
      • [3.1 概念](#3.1 概念)
      • [3.2 实现过程](#3.2 实现过程)
      • [3.3 算法实例](#3.3 算法实例)
      • [3.4 练习及小结](#3.4 练习及小结)
  • 有向无环图(DAG图)
    • [1. DAG描述表达式](#1. DAG描述表达式)
    • [2. 解题方法](#2. 解题方法)
  • 拓扑排序
    • [1. AOV网](#1. AOV网)
    • [2. 拓扑排序](#2. 拓扑排序)
    • [3. 代码实现](#3. 代码实现)
    • [4. 逆拓扑排序](#4. 逆拓扑排序)
    • [5. 代码实现](#5. 代码实现)
    • [6. 小结](#6. 小结)
  • 关键路径
    • [1. AOE网](#1. AOE网)
    • [2. 关键路径](#2. 关键路径)
    • [3. 实现步骤](#3. 实现步骤)
    • [4. 特性](#4. 特性)
    • [5. 小结](#5. 小结)

最小生成树


1. 分类

1.1 广度优先生成树

广度优先遍历可见:广度优先遍历(BFS)

1.2 深度优先生成树

深度优先遍历可见:深度优先遍历(DFS)

1.3 最小生成树

情景 :比如我们是一个铁路承包商,我们想在这几个地点中间修路,从一个地点可以随意到达另一个地点,那么怎么修我们的成本最低?
道路规划要求 :所有地方都连通,且成本尽可能的低。

如果我们随意地将所有地点与中间连通,如左图,我们就会付出19的代价-->权值

但如果是像右图一样,我们就可以减少需要付出的代价

付出代价最小的树就是最小生成树

  • 最小生成树可能有很多个,但边的权值之和总是唯一且最小的。
  • 最小生成树的边数=顶点数-1.砍掉一条则不连通,增加一条则会出现回路。
  • 如果一个连通图本身就是一棵树,则其最小生成树就是它本身。
  • 只有连通图才有生成树,非连通图只能生成森林。

    小总结:

2. 算法实现

2.1 Prim算法

顶点 为主要影响因素。

每纳入一个新的顶点,就把它加入之前的集合,然后一步一步找最小的权值的边。

如右图就是最小生成树。

当然,最小生成树不唯一,下面的两个图也是最小生成树。

从学校开始:

  • 学校、P城 最短:1
  • 学校--1--P城、渔村 最短:4
  • 学校--1--P城--4--渔村、矿场 最短:2
  • 学校--1--P城--4--渔村--2--矿场、农场 最短:5
  • 学校--1--P城--4--渔村--2--矿场--5--农场、电站 最短:3
  • 总代价:1 + 4 + 2 + 5 + 3 = 15

2.2 Kruskal算法

为主要影响因素。

先挑边,再连的顶点。

  • 学校--P城(1)
  • 渔村--矿场(2)
  • 农场--电站(3)
  • P城--矿场(4)
  • 农场--P城(5)
  • 总代价 = 1 + 2 + 3 + 4 + 5 = 15

2.3 对比

  • V:顶点
  • E:边
  • Prim算法先定顶点,顶点肯定是重要影响因素,因为边不影响什么,所以适合边多的稠密图。
  • Kruskal算法先定边,边肯定是重要影响因素,所以适合边少的稀疏图。

3. 实现思想

3.1 Prim算法

  • v0√:v0已加入树
  • ∞:v0没办法直接到v4、v5
  • 找到代价最低的1,则与v3相连
  • 更新此时能够连通其他点的最低代价
  • 找到代价最低的2,则与v5相连
  • 更新此时能够连通其他点的最低代价
  • 找到代价最低的5,则与v1相连
  • 更新此时能够连通其他点的最低代价
  • 找到代价最低的3,则与v4相连
  • 更新此时能够连通其他点的最低代价
  • 每轮时间复杂度:第一次遍历找最低代价,第二次遍历更新表格-->2n
  • 总时间复杂度:需要重复n-1轮

3.2 Kruskal算法

  • 最开始就按照权值大小把表排好
  • 从最小的开始,看两个顶点连通么
  • 不连通就直接相连
  • 依次往后,看两个顶点连通么
  • 不连通就直接相连
  • 依次往后,看两个顶点连通么
  • 不连通就直接相连
  • 依次往后,看两个顶点连通么
  • 不连通就直接相连
  • 依次往后,看两个顶点连通么
  • 已连通就直接跳过
  • 依次往后,看两个顶点连通么
  • 不连通就直接相连

4. 小结

最短路径问题

1. BFS算法

1.1 概念

  • 单源最短路径问题:只针对一个顶点,看其他顶点到它的最短路径。
  • 每对顶点间的最短路径:谁到谁最方便不费力。

    BFS,具体可见广度优先遍历

1.2 代码实现

java 复制代码
// 访问标记数组:记录每个顶点是否已被访问过
// 初始时所有值都为 false(未访问)
bool visited[MAX_VERTEX_NUM];   // 访问标记数组

// 从顶点 v 出发,进行广度优先遍历的函数
void BFS(Graph G, int v) {
    visit(v);                    // 访问起始顶点 v(如打印或处理数据)

    visited[v] = TRUE;           // 将顶点 v 标记为已访问,防止重复访问

    Enqueue(Q, v);               // 将顶点 v 入队列 Q,准备开始层序遍历

    while (!IsEmpty(Q)) {        // 当队列不为空时,继续遍历
        DeQueue(Q, v);           // 从队列头取出一个顶点 v(先进先出)

        // 遍历当前顶点 v 的所有邻接点 w
        for (w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)) {
            // 如果邻接点 w 还未被访问
            if (!visited[w]) {
                visit(w);          // 访问该邻接点 w

                visited[w] = TRUE; // 将 w 标记为已访问

                Enqueue(Q, w);     // 将 w 入队列,等待后续处理
            }
        }
    }
}
java 复制代码
// 求顶点 u 到其他所有顶点的最短路径(适用于无权图)
void BFS_MIN_Distance(Graph G, int u) {
    // d[i] 表示从起点 u 到顶点 i 的最短路径长度
    int d[MAX_VERTEX_NUM];        // 路径长度数组

    // path[i] 表示从起点 u 到顶点 i 的最短路径中,i 的前驱顶点
    int path[MAX_VERTEX_NUM];     // 前驱数组(用于回溯路径)

    // 初始化:所有顶点的最短路径长度设为 -1(表示未访问)
    for (int i = 0; i < G.vexnum; ++i) {
        d[i] = -1;                 // 初始距离为 -1(表示不可达或未访问)
        path[i] = -1;              // 初始前驱为 -1(表示无前驱)
    }

    // 起点 u 到自身的距离为 0
    d[u] = 0;

    // 标记起点 u 已访问
    visited[u] = TRUE;

    // 将起点 u 入队列,开始 BFS
    Enqueue(Q, u);

    // BFS 主循环:只要队列不空,就继续处理
    while (!IsEmpty(Q)) {
        DeQueue(Q, u);             // 从队列头取出当前顶点 u

        // 遍历 u 的所有邻接点 w
        for (w = FirstNeighbor(G, u); w >= 0; w = NextNeighbor(G, u, w)) {
            // 如果邻接点 w 尚未被访问
            if (!visited[w]) {
                // 更新 w 的最短路径长度:比 u 多走一步
                d[w] = d[u] + 1;

                // 记录 w 的前驱是 u(用于后续回溯路径)
                path[w] = u;

                // 标记 w 为已访问
                visited[w] = TRUE;

                // 将 w 入队列,等待后续处理
                Enqueue(Q, w);
            }
        }
    }
}
步骤 操作 d[] path[] 队列 Q
1 初始化 d[2]=0,其余=-1 path[-1] [2]
2 DeQueue(2)
3 w=1,6 → 未访问 d[1]=1, d[6]=1 path[1]=2, path[6]=2 [1,6]
4 DeQueue(1)
5 w=5 → 未访问 d[5]=2 path[5]=1 [6,5]
6 DeQueue(6)
7 w=3,7 → 未访问 d[3]=2, d[7]=2 path[3]=6, path[7]=6 [5,3,7]
... 继续 ... ... ...

1.3 小结

2. Dijkstra算法

2.1 优化

不限制有向无向,因为无向就相当于双向,比如下面两个图的右边是同一个图:

2.2 实现过程

  • v0√:从v0开始连通
  • ∞:v0到v2、v3还没有直接连通的道路
  • -1:还没办法到达v2、v3,因为v0是自己本身所以也无法到达
  • 对比最短路径长度,选择最小的,可以直接到达v4
  • 引入v4,更新能够连通的顶点和最短路径长度
  • 对比最短路径长度,选择最小的,可以直接到达v3
  • 引入v3,更新能够连通的顶点和最短路径长度
  • 对比最短路径长度,选择最小的,可以直接到达v1
  • 引入v1,更新能够连通的顶点和最短路径长度
  • 对比最短路径长度,选择最小的,可以直接到达v2
  • v2是最后一个顶点,所以就不需要更新了

    比如我们现在想从v0到达v2:
  • 观察v2的那一列
  • 9:v0到v2的最短路径长度是9
  • 1:v2需要经过v1;4:还要经过v4;0:最后成功到达v0

2.3 时间复杂度

因为每轮都需要检查更新n个顶点,需要检查更新n轮,所以时间复杂度是O(n2)

对比项 Prim 算法 Dijkstra 算法
目的 最小生成树 单源最短路径
图类型 无向图 有向/无向图(非负权)
核心思想 连接最近的未连顶点 扩展最短的已知路径
更新规则 dist[v] = min(dist[v], w(u,v)) dist[v] = min(dist[v], dist[u] + w(u,v))
结果依赖起点? 否(MST 总权唯一)
能否处理负权? 可以(无向图) 不可以

不适用负权值带权图!!!

3. Floyd算法

3.1 概念

3.2 实现过程

初始:记录图上的无中转的真实数据

  • 允许v0中转
  • 更新两个表
  • 允许v1中转
  • 更新两个表
  • 允许v2中转
  • 更新两个表
  • n轮之后
  • 更新两个表
java 复制代码
// Floyd 算法:求任意两点之间的最短路径(适用于带权有向图,允许负权边,但不能有负环)

// A[i][j] 表示从顶点 i 到顶点 j 的当前最短路径长度
// path[i][j] 表示从 i 到 j 的最短路径中,j 的前驱顶点(用于回溯路径)

// 初始化:根据图的信息构建初始距离矩阵 A 和路径矩阵 path
// 例如:A[i][j] = 边 (i,j) 的权重;若无边,则为 ∞(用一个大数表示)
// path[i][j] = -1 表示无直接路径或未定义

for (int k = 0; k < n; k++) {        // 外层循环:枚举中间顶点 vk 作为"转点"
    for (int i = 0; i < n; i++) {     // 中层循环:遍历起点 i
        for (int j = 0; j < n; j++) { // 内层循环:遍历终点 j
            // 核心判断:是否通过顶点 k 能使 i→j 的路径更短?
            if (A[i][j] > A[i][k] + A[k][j]) {
                // 如果绕道 k 更短,则更新最短路径长度
                A[i][j] = A[i][k] + A[k][j];   // 更新距离

                // 同时更新路径记录:从 i 到 j 的路径现在经过 k
                path[i][j] = k;                // 记录 k 是 i→j 路径中的中转点
            }
            // 否则保持原值不变
        }
    }
}

3.3 算法实例








3.4 练习及小结

可以用于负权图

但是不可以用于带有负权回路的图。

有向无环图(DAG图)

1. DAG描述表达式

两个重复的部分:

就可以合并成一个,由上面的顶点全部指向保留的这一个

红绿两个部分同样重复了,

也可以保留其中一个,将另一边的指像这个重复的部分:

这两个b也重复了,

也可以合并成一个,然后上面的共同指向这一个:

真题:

2. 解题方法

经过无数尝试及总结得出一个结论

顶点中不可能出现重复的操作数

根据这一条结论,我们就可以得到如下的步骤:

  • 最底层就是我们表达式中的所有元素
  • 将表达式中的运算符标上序号,以防遗漏
  • 按顺序把运算符加入到树中,注意分层
  • 分层 :看清楚是谁与谁的运算,如果是与一个式子的运算,自然这个运算符就高人一等了
  • 添加完所有的运算符,我们就可以来进行优化了
  • 从底层向上逐层检查同层的运算符是否可以合体
  • 比如c+d有3个,就可以合成一个
  • 合体之后就是这个样子了:

小练习:

拓扑排序

1. AOV网

用顶点表示活动

  • A-->B:必须做完A才能执行B
  • 被两个箭头指:必须完成前两个事件才能做接下来的事件

    如果有环了,就不是AOV网了,因为AOV网本质上就是有向无环图(DAG)

2. 拓扑排序

就是按照顺序把顶点写下来,先完成前提分支,然后切回主线继续进行

拓扑排序条件:

  • 每个顶点出现且只出现一次
  • 序列中A在B前-->图中无B到A的路径

3. 代码实现

定义:

java 复制代码
#define MaxVertexNum 100            // 图中顶点数目的最大值

// 边(弧)结点结构体:用于存储有向边的信息
typedef struct ArcNode {
    int adjvex;                     // 该弧指向的顶点在数组中的下标(即邻接点)
    struct ArcNode *nextarc;        // 指向下一条弧的指针(形成链表)
    // InfoType info;               // 可选:存储边的权值或其他信息
} ArcNode;

// 顶点结点结构体:用于存储顶点及其出边链表
typedef struct VNode {
    VertexType data;                // 存储顶点的数据(如字符、数字等)
    ArcNode *firstarc;              // 指向第一条依附于该顶点的弧(即邻接表头指针)
} VNode, AdjList[MaxVertexNum];     // VNode 是顶点类型,AdjList 是顶点数组

// 图的整体结构体:Graph(以邻接表存储)
typedef struct {
    AdjList vertices;               // 顶点数组:存储所有顶点和它们的邻接表
    int vexnum, arcnum;             // 图的当前实际顶点数和弧数
} Graph;

拓扑排序函数实现:

java 复制代码
bool TopologicalSort(Graph G) {
    InitStack(S);                   // 初始化栈 S,用于存储入度为 0 的顶点

    // 将所有入度为 0 的顶点压入栈中
    for (int i = 0; i < G.vexnum; i++) {
        if (indegree[i] == 0)       // 如果顶点 i 的入度为 0
            Push(S, i);             // 将其压入栈 S
    }

    int count = 0;                  // 计数器:记录已经输出的顶点个数

    while (!IsEmpty(S)) {           // 当栈不为空时,继续处理
        Pop(S, i);                  // 弹出栈顶元素 i(一个入度为 0 的顶点)

        print[count++] = i;         // 输出顶点 i,并计数

        // 遍历顶点 i 的所有出边(即它指向的所有顶点)
        for (p = G.vertices[i].firstarc; p != NULL; p = p->nextarc) {
            v = p->adjvex;          // 获取 i 指向的顶点 v

            // 将 v 的入度减 1(因为 i 被移除,不再指向 v)
            if (!(--indegree[v]))   // 如果 v 的入度变为 0
                Push(S, v);         // 将 v 压入栈(准备输出)
        }
    }

    // 判断是否成功完成拓扑排序
    if (count < G.vexnum)           // 如果输出的顶点数小于总顶点数
        return false;               // 说明图中有环,拓扑排序失败
    else
        return true;                // 成功完成拓扑排序
}

栈的变化:

步骤 操作 栈 S 内容(从底 → 顶) 入度数组 indegree[] 输出 print[] 说明
1 初始化 [] [0,1,0,1,2] [-1,-1,-1,-1,-1] 初始状态:入度已知,栈为空
2 Push(0) [0] [0,1,0,1,2] [-1,-1,-1,-1,-1] 顶点 0 入度为 0 → 压栈
3 Push(2) [0, 2] [0,1,0,1,2] [-1,-1,-1,-1,-1] 顶点 2 入度为 0 → 压栈
4 Pop(S,i) → i=2 [0] [0,1,0,1,2] [2] 弹出栈顶 2,输出 2
5 处理边 2→4 [0] [0,1,0,1,1] [2] in[4] -=1 → 变为 1,不压栈
6 Pop(S,i) → i=0 [] [0,1,0,1,1] [2,0] 弹出栈顶 0,输出 0
7 处理边 0→1 [] [0,0,0,1,1] [2,0] in[1] -=1 → 变为 0 → 压栈
8 Push(1) [1] [0,0,0,1,1] [2,0] 顶点 1 入度变为 0 → 压栈
9 Pop(S,i) → i=1 [] [0,0,0,1,1] [2,0,1] 弹出栈顶 1,输出 1
10 处理边 1→3 [] [0,0,0,0,1] [2,0,1] in[3] -=1 → 变为 0 → 压栈
11 Push(3) [3] [0,0,0,0,1] [2,0,1] 顶点 3 入度变为 0 → 压栈
12 Pop(S,i) → i=3 [] [0,0,0,0,1] [2,0,1,3] 弹出栈顶 3,输出 3
13 处理边 3→4 [] [0,0,0,0,0] [2,0,1,3] in[4] -=1 → 变为 0 → 压栈
14 Push(4) [4] [0,0,0,0,0] [2,0,1,3] 顶点 4 入度变为 0 → 压栈
15 Pop(S,i) → i=4 [] [0,0,0,0,0] [2,0,1,3,4] 弹出栈顶 4,输出 4
  • 时间复杂度:因为每个顶点和边都需要处理一次。

4. 逆拓扑排序

就是把每个顶点反方向写出来。

灰色就是逆拓扑排序后的样子:

5. 代码实现

java 复制代码
// 访问标记数组:记录每个顶点是否已被访问过
// 初始时所有值都为 false(未访问)
bool visited[MAX_VERTEX_NUM];   // 访问标记数组

// 对图 G 进行深度优先遍历的主函数
void DFSTraverse(Graph G) {
    // 初始化访问标记数组:将所有顶点标记为"未访问"
    for (v = 0; v < G.vexnum; ++v) {
        visited[v] = FALSE;     // 所有顶点初始状态为未访问
    }

    // 遍历图中的每一个顶点(处理非连通图)
    for (v = 0; v < G.vexnum; ++v) {
        // 如果当前顶点 v 尚未被访问
        if (!visited[v]) {
            // 从该顶点出发,进行一次 DFS 遍历
            DFS(G, v);           // 调用 DFS 函数,遍历以 v 为起点的连通分量
        }
    }
}

// 深度优先遍历函数(从顶点 v 开始)
void DFS(Graph G, int v) {
    visited[v] = TRUE;           // 将顶点 v 标记为已访问,防止重复访问

    // 遍历当前顶点 v 的所有邻接点 w
    // 使用 FirstNeighbor 和 NextNeighbor 枚举所有邻接点
    for (w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)) {
        // 如果邻接点 w 还未被访问
        if (!visited[w]) {
            // 递归调用 DFS,从 w 开始继续深入遍历
            DFS(G, w);
        }
    }

    // ⭐ 关键:在顶点退栈前输出(即后序遍历)
    print(v);                    // 输出当前顶点 v
}

栈变化:

步骤 操作(函数调用/返回) 递归栈内容(从底 → 顶) 输出(print) 说明
1 DFS(0) 被调用 [0] --- 访问顶点 0
2 DFS(1) 被调用 [0, 1] --- 0→1,访问 1
3 DFS(3) 被调用 [0, 1, 3] --- 1→3,访问 3
4 DFS(4) 被调用 [0, 1, 3, 4] --- 3→4,访问 4
5 DFS(4) 返回 [0, 1, 3] 4 4 无出边,输出 4
6 DFS(3) 返回 [0, 1] 4, 3 3 的邻接点处理完,输出 3
7 DFS(1) 返回 [0] 4, 3, 1 1 的邻接点处理完,输出 1
8 DFS(0) 返回 [] 4, 3, 1, 0 0 的邻接点处理完,输出 0
9 DFS(2) 被调用 [2] 4, 3, 1, 0 主循环继续,访问未访问的 2
10 DFS(2) 返回 [] 4, 3, 1, 0, 2 2→4(但 4 已访问),输出 2

6. 小结

关键路径

1. AOE网

来表示活动。

  • 圈发生后-->边才能发生
  • 边结束后-->圈才能发生

2. 关键路径

  • 关键路径 :具有最大路径长度的路径
    比如a2+a3+a4的路径长度为6,大于a1+a4的路径长度4,所以a2+a3+a4为关键路径
  • 关键活动 :关键路径上的活动
    a2+a3+a4对应的活动就是关键活动
  • 最早发生时间 :指顶点 ,活动的最早开始时间
    比如:v3需要v1、v2连通的活动都必须结束,a1=2、a2=1 + a3=3 -->4;所以只能4的时候才能开工
  • 最早开始时间 :指 ,事件的最早开始时间
    比如:0时v1发生了,a1和a2就可以紧跟着发生,所以它俩的最早发生时间是0

    反向推,我已经知道6分钟可以做完番茄炒蛋,那我就要求6分钟后立马吃到:
  • 最迟发生时间 :指顶点,最晚6分钟就必须结束炒菜,然后依次向前推其他顶点的最迟发生时间
  • 最迟开始时间 :指 ,大部分都依赖于前一个顶点的最迟发生时间,但!
    这里注意a1,因为a2和a3一共需要4分钟才能结束,而a2只需要两分钟,所以可以推迟在2时刻再开始a2
  • 时间余量:就是看你能忙里偷闲偷多少
  • 比如a1,最早可以0时刻开始,但是最晚可以2时刻开始,相减就代表你可以偷懒两分钟

3. 实现步骤

就是上述介绍的概念全部算出来√

有两条路径的,就取值更大的那个

从上一步得到最后一个事件的最迟发生时间(就是它的最早发生时间),

然后逆推其余时间的最迟发生时间

看每个边的最早开始时间

每个边最晚的发生时间

边的最晚发生时间-边的最早发生时间

4. 特性


5. 小结


相关推荐
Java面试题总结2 小时前
浅析二叉树、B树、B+树和MySQL索引底层原理
数据结构·b树·mysql
小比特_蓝光2 小时前
string类的模拟实现
数据结构·c++·算法
404未精通的狗2 小时前
(数据结构)AVL树
数据结构
云深处@2 小时前
【数据结构】顺序表
数据结构
Дерек的学习记录10 小时前
C++:入门基础(下)
开发语言·数据结构·c++·学习·算法·visualstudio
程序员酥皮蛋14 小时前
hot 100 第二十四题 24.回文链表
数据结构·链表
仟濹16 小时前
【算法打卡day7(2026-02-12 周四)算法:BFS and BFS】 3_卡码网107_寻找存在的路线_并查集
数据结构·算法·图论·宽度优先
数智工坊17 小时前
【数据结构-排序】8.2 冒泡排序-快速排序
数据结构
芝士爱知识a17 小时前
[2026深度测评] AI期权交易平台推荐榜单:AlphaGBM领跑,量化交易的新范式
开发语言·数据结构·人工智能·python·alphagbm·ai期权工具