行至图深处,自有路标出

版权所有 © [2026] Vannik 未经作者授权,禁止转载
Doc Map
  1. BFS 求最短路径(无权图)
  2. Dijkstra 算法(单源带权图)
  3. Floyd 算法(多源带权图)
  4. 拓扑排序
  5. 关键路径

六月中旬了,时间快得让人心慌。

成都的夏天从来不跟你商量,说热就热。前几天天气还是比较阴,有时候还在下雨(伦敦的天气未必有成都阴郁😎),甚至感觉有点凉爽,结果今天就飙到28°。中午从食堂走回图书馆,大理石路面都被烤得发光,直晃眼睛,热浪直往脸上扑。回想这一个多月,过得像开了倍速 —— 写不完的思想汇报,翻来覆去改材料,一轮轮谈话、外调……直到前几天在党旗下宣誓,正式成为预备党员的那一刻,心里才踏实下来。那段忙碌总算有了着落。

等缓过神来,日历已经翻到六月。看看桌上的 408 复习资料,心里咯噔一下:一轮还没收尾,数学题堆成山,《数据结构》刚磕到第六章 —— 图。

身边有的研友早就开始二轮复习并刷真题了,说不慌是假的。每次闭馆往回走,脑子里都在盘算进度,越算越焦虑。但急也没用,落下的只能熬夜补。这几天总算静下来了,重新坐回那个固定位置,摸到熟悉的键盘和那支硬笔钢笔,状态慢慢回来了。

今天复习到图的最短路径、拓扑排序以及关键路径,这块是整个章节里最硬的部分。写篇笔记,把逻辑捋清楚,也给重新启航的自己扎个锚。

图论里最经典的问题之一:在一张带权图里,怎么找到从一个点到另一个点的最短路径?根据问题规模不同,分三种情况讨论 —— 单源无权图用 BFS,单源带权图用 Dijkstra,多源带权图用 Floyd。说到 Dijkstra,就不得不提它的发明者 Edsger W. Dijkstra,这位大佬不仅在最短路径上留了一笔,操作系统里那个经典的银行家算法也是他搞出来的。膜拜大佬🙌。

BFS 求最短路径(无权图)

如果图上每条边都没有权重(或者说权重都相等,默认为 1),那问题就简单了 —— BFS 天生就能找到最短路径。

BFS 是一层一层往外扩的,先被访问到的顶点,一定离起点更近。所以从起点开始跑一遍 BFS,记录每个顶点的”层数”,这层数就是最短路径长度。

#include <stdio.h>
#include <stdbool.h>

#define MAX_VERTEX_NUM 100

/**
 * @brief 使用广度优先搜索 (BFS) 计算无权图的单源最短路径
 * * @param graph 邻接矩阵表示的图,graph[v][w] != 0 表示 v 到 w 有边
 * @param n     图中顶点的总数
 * @param start 起始顶点的索引
 * @param dist  输出数组:存放起点到各顶点的最短距离(-1 表示不可达)
 * @param path  输出数组:存放最短路径上每个顶点的前驱节点(用于回溯路径,-1 表示无前驱)
 */
void BFS_ShortestPath(int graph[MAX_VERTEX_NUM][MAX_VERTEX_NUM], int n, int start, int dist[], int path[]) {
    // 1. 初始化访问标记数组,false 表示未访问,true 表示已访问
    bool visited[MAX_VERTEX_NUM] = {false};
    
    // 2. 初始化队列及头尾指针(采用顺序队列模拟)
    int queue[MAX_VERTEX_NUM];
    int front = 0, rear = 0;

    // 3. 初始化距离数组和路径数组
    for (int i = 0; i < n; i++) {
        dist[i] = -1; // 默认所有顶点不可达
        path[i] = -1; // 默认没有前驱节点
    }

    // 4. 起点入队处理
    visited[start] = true;  // 标记起点已访问
    dist[start] = 0;        // 起点到自己的距离为 0
    queue[rear++] = start;  // 起点入队

    // 5. 队列不为空时循环遍历
    while (front < rear) {
        int v = queue[front++]; // 出队当前顶点 v
        
        // 遍历所有可能的邻接点 w
        for (int w = 0; w < n; w++) {
            // 如果 v 和 w 之间有边,且 w 尚未被访问过
            if (graph[v][w] != 0 && !visited[w]) {
                visited[w] = true;              // 标记 w 已访问
                dist[w] = dist[v] + 1;          // w 的最短距离等于 v 的距离加 1
                path[w] = v;                    // 记录 w 的前驱节点是 v
                queue[rear++] = w;              // 将 w 入队,以便后续扩展
            }
        }
    }
}

BFS 的写法很干净,没什么坑。唯一的限制:只能处理无权图或每条边权值相等的情况。如果边权不一样,BFS 就不管用了 —— 它只看层数,不认权重。这时候就得请出 Dijkstra。

Dijkstra 算法(单源带权图)

最常见的问题:从一个起点出发,怎么走到图中任意顶点的距离最短?

Dijkstra 的解法有种一步一个脚印的稳健感。核心思想是贪心 —— 把顶点分成两个阵营:已经找到最短路径的“确定区”集合 S{S},和还在等待的“未知区”集合 VS{V − S}

每一步,在未知区里找一个离起点最近的顶点,拉进确定区。然后以这个新加入的点为跳板,刷新它周围所有邻接点的距离。这个不断“拉人进组、刷新认知”的过程,跟一轮复习挺像的 —— 先把最笃定的知识点啃下来,再以此为基础去摸索更远的领域。

408 选择题极其喜欢考辅助数组的变化,用 C 语言手写一遍 Dijkstra 的底层逻辑是逃不掉的。

#include <stdio.h>
#include <stdbool.h>

#define INFINITY 65535
#define MAX_VERTEX_NUM 100

/**
 * @brief Dijkstra 算法计算带权图的单源最短路径
 * * @param matrix  邻接矩阵,matrix[i][j] 表示 i 到 j 的边权值,不可达则为 INFINITY
 * @param vex_num 顶点总数
 * @param v0      源点(起点)的索引
 * @param dist    输出数组:存放 v0 到各个顶点的当前已知最短距离
 * @param path    输出数组:存放最短路径上每个顶点的前驱节点索引
 */
void Dijkstra(int matrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM], int vex_num, int v0, int dist[], int path[]) {
    // final[i] 为 true 表示已求得 v0 到顶点 i 的最短路径(即 i 已加入确定集合)
    bool final[MAX_VERTEX_NUM];

    // ==========================================
    // 阶段一:初始化阶段
    // ==========================================
    for (int i = 0; i < vex_num; i++) {
        final[i] = false;          // 初始时所有顶点都未确定最短路径
        dist[i] = matrix[v0][i];   // 将与源点 v0 直连的边权值赋给 dist

        // 如果 v0 与顶点 i 之间有直连边
        if (dist[i] < INFINITY) {
            path[i] = v0;          // i 的前驱节点暂时设为 v0
        } else {
            path[i] = -1;          // 不直连则暂无前驱
        }
    }

    dist[v0] = 0;      // 源点到自身的距离显然为 0
    final[v0] = true;  // 源点直接纳入已确定集合
    path[v0] = -1;     // 源点没有前驱节点

    // ==========================================
    // 阶段二:主循环,每次找出一个距离最近的顶点纳入集合
    // ==========================================
    // 只需要循环 vex_num - 1 次(剩余未确定的顶点数)
    for (int i = 1; i < vex_num; i++) {
        int min = INFINITY; // 记录当前未确定顶点中的最小距离
        int v = v0;         // 记录最小距离顶点的下标

        // 核心步骤 1:贪心选择
        // 在所有未确定的顶点中,找出距离源点 v0 最近的顶点 v
        for (int w = 0; w < vex_num; w++) {
            if (!final[w] && dist[w] < min) {
                v = w;
                min = dist[w];
            }
        }

        // 如果最小距离依然是 INFINITY,说明剩下的未确定顶点与源点完全不连通,直接退出
        if (min == INFINITY) break;
        
        // 将选出的顶点 v 标记为已确定
        final[v] = true;

        // 核心步骤 2:松弛操作 (Relaxation)
        // 借助刚刚新加入的顶点 v,尝试更新源点到其他未确定顶点 w 的最短距离
        for (int w = 0; w < vex_num; w++) {
            // 条件:w 未确定 && v 与 w 之间有边 && (通过 v 中转的距离 < 之前的已知距离)
            if (!final[w] && matrix[v][w] < INFINITY && (min + matrix[v][w] < dist[w])) {
                dist[w] = min + matrix[v][w]; // 更新更小的距离
                path[w] = v;                  // 修改 w 的前驱节点为 v
            }
        }
    }
}

408 踩坑点: 做选择题手算模拟时,死死记住一条 —— 每一次刷新数组,都是且仅是基于当前最新加入 final 集合的那一个顶点的延伸。另外,Dijkstra 不能处理负权边,这算是一个经典面试题了:为什么不能有负权?因为一旦某条边是负的,之前已经确定“最短”的路径,后面可能还能通过绕路变得更短,贪心的前提就崩了。

再用 C# 写一遍,换个视角看算法骨架:

using System;

public class GraphSolver
{
    // 定义正无穷大为整型的最大值
    private const int Infinity = int.MaxValue;

    /// <summary>
    /// 使用 Dijkstra 算法计算带权图的单源最短路径(C# 改进版)
    /// </summary>
    /// <param name="adjMatrix">邻接矩阵,adjMatrix[i, j] 表示边权值,不可达则为 int.MaxValue</param>
    /// <param name="vertexCount">顶点总数</param>
    /// <param name="source">源点(起点)索引</param>
    /// <param name="dist">输出参数:从源点到各个顶点的最短距离数组</param>
    /// <param name="path">输出参数:各个最短路径上节点的父节点(前驱)数组</param>
    public static void DijkstraSolve(int[,] adjMatrix, int vertexCount, int source, out int[] dist, out int[] path)
    {
        // 进行安全校验
        if (adjMatrix == null || vertexCount <= 0 || source < 0 || source >= vertexCount || adjMatrix.GetLength(0) < vertexCount || adjMatrix.GetLength(1) < vertexCount)
        {
            dist = Array.Empty<int>();
            path = Array.Empty<int>();
            return false;
        }

        // 1. 分配输出数组与状态数组的空间
        dist = new int[vertexCount];
        path = new int[vertexCount];
        bool[] isVisited = new bool[vertexCount];

        // 2. 初始化数据
        for (int i = 0; i < vertexCount; i++)
        {
            dist[i] = adjMatrix[source, i]; // 初始距离为源点直连的边权
            isVisited[i] = false;          // 初始所有节点均未访问
            
            // 如果源点与 i 直连且 i 不是源点本身,前驱设为 source,否则设为 -1
            path[i] = (dist[i] != Infinity && i != source) ? source : -1;
        }

        dist[source] = 0;          // 源点到自身的距离为 0
        isVisited[source] = true;  // 标记源点已访问并确定最短路径

        // 3. 主循环:对其余的 vertexCount - 1 个节点进行贪心扩展
        for (int count = 1; count < vertexCount; count++)
        {
            int u = -1;
            int minDistance = Infinity;

            // 核心步骤 1:寻找当前未访问节点中,距离源点最近的节点 u
            for (int v = 0; v < vertexCount; v++)
            {
                if (!isVisited[v] && dist[v] < minDistance)
                {
                    minDistance = dist[v];
                    u = v;
                }
            }

            // 如果找不到可达的节点(剩余节点与源点均不连通),提前跳出循环
            if (u == -1) break;
            
            // 将节点 u 标记为已访问(加入确定集合)
            isVisited[u] = true;

            // 核心步骤 2:松弛操作 (Relaxation)
            // 利用新加入的节点 u,尝试更新其所有邻接点 v 的最短路径
            for (int v = 0; v < vertexCount; v++)
            {
                // 条件:v 未访问 && u 可达 && u 到 v 有边直连
                if (!isVisited[v] && dist[u] != Infinity && adjMatrix[u, v] != Infinity)
                {
                    // 使用减法代替加法进行比较 (dist[u] < dist[v] - adjMatrix[u, v])
                    // 避免 dist[u] + adjMatrix[u, v] 导致的整型溢出回绕
                    if (dist[u] < dist[v] - adjMatrix[u, v])
                    {
                        dist[v] = dist[u] + adjMatrix[u, v]; // 更新更短的距离
                        path[v] = u;                         // 更新前驱节点
                    }
                }
            }
        }
    }
}
Floyd 算法(多源带权图)

Dijkstra 一次只能求一个起点到其他点的最短路径。如果需要知道任意两个顶点之间的最短路径,就要用 Floyd。

Floyd 不贪心,而是动态规划。核心思路是“借路”:对于每一对顶点 (i,j){(i, j)},依次尝试让顶点 0、1、2……作为中转节点 kk,看看 ikj{i→k→j} 是否比 ij{i→j} 更短。这个做法有个好听的名字叫“多阶段决策”,跟求传递闭包的 Warshall 算法思路很像,对比着记效率更高。

#include <stdio.h>
#include <stdbool.h>

#define INFINITY 65535
#define MAX_VERTEX_NUM 100

/**
 * @brief Floyd-Warshall 算法计算图中任意两点间的最短路径(多源最短路径)
 * * @param matrix  输入数组:原始图的邻接矩阵
 * @param n       顶点总数
 * @param dist    输出数组:dist[i][j] 存放顶点 i 到顶点 j 的当前最短距离
 * @param path    输出数组:path[i][j] 存放 i 到 j 的最短路径上,j 的前驱节点
 */
void Floyd(int matrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM], int n, int dist[MAX_VERTEX_NUM][MAX_VERTEX_NUM], int path[MAX_VERTEX_NUM][MAX_VERTEX_NUM]) {
    
    // ==========================================
    // 阶段一:初始化阶段
    // ==========================================
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            // 初始时,两点间的最短距离就是它们直连边的权值
            dist[i][j] = matrix[i][j];
            
            // 初始化前驱路径:
            // 如果 i 和 j 不是同一个点,且有边直连(距离小于无穷大),则 j 的前驱暂时设为 i;
            // 否则(自己到自己,或者不直连),前驱设为 -1
            path[i][j] = (i != j && matrix[i][j] < INFINITY) ? i : -1;
        }
    }

    // ==========================================
    // 阶段二:动态规划核心三层循环
    // ==========================================
    // 外层循环 k:代表当前允许中转的中间顶点索引(从 0 到 n-1)
    // 算法的核心思想是:依次允许节点 0, 1, 2...k 作为中转点,逐步刷新所有点对的最短距离
    for (int k = 0; k < n; k++) {
        // 中层循环 i:路径的起点
        for (int i = 0; i < n; i++) {
            // 内层循环 j:路径的终点
            for (int j = 0; j < n; j++) {
                
                // 核心状态转移方程判断:
                // 如果从 i 经过中转点 k 到达 j 的距离,比当前已知的 i 直达 j 的距离更短
                if (dist[i][k] + dist[k][j] < dist[i][j]) {
                    
                    // 更新最优距离:i 到 j 的距离缩短为两段相加
                    dist[i][j] = dist[i][k] + dist[k][j];
                    
                    // 更新前驱节点:
                    // 因为现在从 i 到 j 的后半段是“从 k 到 j”,
                    // 所以 j 的前驱节点应当变成“从 k 到 j”这条路径上 j 的前驱,即 path[k][j]
                    path[i][j] = path[k][j];
                }
            }
        }
    }
}

408 手算技巧: 初始矩阵就是邻接矩阵,无边填 \infty。依次用顶点 0、1、2……作为 kk,每一轮扫描整个矩阵看 ikji→k→j 是否更短。考场上问“这一步 kk 等于几”,就看被更新的路径都经过了哪个公共顶点。Floyd 还有一个好处 —— 它能处理负权边,但不能有负环(判断方法:跑完后看对角线上有没有出现负值)。

算法适用场景核心思想负权边限制时间复杂度
BFS无权图(等权边)单源最短路广度优先搜索无负权概念,不涉及O(n2)O(n^2)O(n+e)O(n+e)
Dijkstra带权图单源最短路贪心不支持负权边O(n2)O(n^2)(普通版)
Floyd带权图多源最短路动态规划支持负权,但不可存在负权环O(n3)O(n^3)
拓扑排序

拓扑排序处理的是有向无环图(DAG)。它要回答的问题是:如果一堆任务之间有先后依赖关系,应该按什么顺序执行?

这种场景太常见了 —— 比如大学选课,先修课没修就不能选后续课。408 里也经常出这种应用题,给一张 AOV 网(用顶点表示活动),让你排个合理的执行顺序。

核心操作:每次选一个入度为 0 的顶点输出,然后删掉它出发的所有边,重复直到所有顶点都输出完。如果最后还有顶点没输出,说明图里有环,拓扑排序失败。

#include <stdbool.h>
#include <stdio.h>

#define MAX_VERTEX_NUM 100

/**
 * @brief 拓扑排序(基于 Kahn 算法,引入安全检查并改用队列实现 BFS 层次序)
 * * @param graph  输入参数:二维数组邻接矩阵。graph[i][j] != 0 表示有一条从顶点 i 指向顶点 j 的有向边
 * @param n       输入参数:图中顶点的实际有效数量
 * @param result  输出参数:一维数组,用于顺序存放拓扑排序成功后的顶点序列
 * * @return true   排序成功:图是一个有向无环图 (DAG),result 中包含完整的拓扑序列
 * @return false  排序失败:图中存在有向环(回路),无法完成拓扑排序
 */
bool TopologicalSort(int graph[MAX_VERTEX_NUM][MAX_VERTEX_NUM], int n, int result[]) {
    
    // ==========================================
    // 1. 安全边界校验
    // ==========================================
    // 防御性编程:防止外部传入空指针、不合法的节点数,或者超出数组预设最大值的范围,从而避免运行时崩溃
    if (n <= 0 || n > MAX_VERTEX_NUM || graph == NULL || result == NULL) {
        return false;
    }

    // ==========================================
    // 2. 统计所有顶点的初始入度
    // ==========================================
    // indegree[i] 存储顶点 i 的入度(即有多少条有向边直接指向顶点 i)
    int indegree[MAX_VERTEX_NUM] = {0};

    // 遍历整个邻接矩阵
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            // 如果 graph[i][j] 不为 0,说明存在一条 i -> j 的有向边
            if (graph[i][j] != 0) {
                indegree[j]++; // 顶点 j 的依赖项(入度)增加 1
            }
        }
    }

    // ==========================================
    // 3. 初始化顺序队列模拟 BFS
    // ==========================================
    // 用普通的数组和头尾指针来模拟一个标准队列
    int queue[MAX_VERTEX_NUM];
    int front = 0; // 队头指针,用于出队操作
    int rear = 0;  // 队尾指针,用于入队操作

    // ==========================================
    // 4. 初始入度为 0 的顶点入队
    // ==========================================
    // 寻找图中所有不需要任何先修条件(入度为 0)的孤立点或起始点
    for (int i = 0; i < n; i++) {
        if (indegree[i] == 0) {
            queue[rear++] = i; // 将顶点 i 推入队尾,同时队尾指针后移
        }
    }

    // count 计数器:专门用来统计最终成功加入拓扑序列的顶点总数
    int count = 0;

    // ==========================================
    // 5. 核心循环:队列不为空时持续迭代
    // ==========================================
    // 当 front == rear 时说明队列已空,循环结束
    while (front < rear) {
        // 出队操作:遵从先进先出(FIFO)原则,从队头弹出一个当前无依赖的顶点 v
        int v = queue[front++]; 
        
        // 记录结果:将该顶点放入最终的拓扑序列数组中
        result[count++] = v;

        // 消除依赖:遍历顶点 v 指向的所有邻接点 w
        for (int w = 0; w < n; w++) {
            if (graph[v][w] != 0) { // 发现一条从 v 指向 w 的有向边
                indegree[w]--;      // 因为顶点 v 已经“被解决”,所以它的下游顶点 w 的入度减 1
                
                // 动态检查:如果在消去 v 的影响后,w 的入度也变成了 0
                // 说明 w 的所有前置依赖全部被清除,它已经具备了执行条件
                if (indegree[w] == 0) {
                    queue[rear++] = w; // 将 w 送入队尾,等待后续处理
                }
            }
        }
    }

    // ==========================================
    // 6. 判断拓扑排序结果并返回
    // ==========================================
    // 如果 count == n,说明所有的顶点都成功出过队并加入了拓扑序,图是无环的 (DAG)
    // 如果 count < n,说明图中有部分节点构成了环路(有向环上的节点入度永远不可能减到 0,因此无法入队)
    return count == n;
}

考点: DFS 也能做拓扑排序 —— 在 DFS 回溯时把顶点压入栈,最后出栈顺序就是逆拓扑序。其实这背后有个直觉:DFS 越深的点,依赖链越长,回溯时越晚被访问完,自然应该排在前面(逆序输出)。考场上遇到手算题,记住先找入度为 0 的,一步步删就行。如果某一步同时有多个入度为 0 的顶点,拓扑序列就不唯一 —— 这也可能会出现在选择题的选项辨析里。

关键路径

关键路径是在拓扑排序的基础上,解决“整个工程最短需要多长时间完成”的问题。它找出项目里那些绝对不能拖延的任务 —— 这些任务组成的路径就是关键路径。注意区分,拓扑排序用的是 AOV 网(顶点表示活动),而关键路径用的是 AOE 网(边表示活动,顶点表示事件),别搞混了。

需要计算四个量:

  • ve(v):事件 v 的最早发生时间(正着推,取 max)
  • vl(v):事件 v 的最迟发生时间(倒着推,取 min)
  • e(i):活动 i 的最早开始时间 = ve(弧尾)
  • l(i):活动 i 的最迟开始时间 = vl(弧头) - 活动耗时

e(i)==l(i)e(i) == l(i) 时,活动 ii 就在关键路径上。用大白话说:这个活动一点都不能拖,晚一天整个项目就晚一天。

#include <stdbool.h>
#include <stdio.h>

#define MAX_VERTEX_NUM 100

bool TopologicalSort_Improved(int graph[MAX_VERTEX_NUM][MAX_VERTEX_NUM], int n, int result[]);

/**
 * @brief 关键路径算法
 */
bool CriticalPath_Improved(int graph[MAX_VERTEX_NUM][MAX_VERTEX_NUM], int n) {
    int ve[MAX_VERTEX_NUM] = {0};
    int vl[MAX_VERTEX_NUM];
    int topo[MAX_VERTEX_NUM];

    // 1. 拓扑排序校验
    if (!TopologicalSort_Improved(graph, n, topo)) {
        return false; // 有环图直接退出
    }

    // 2. 正推计算 ve (Earliest Event Time)
    for (int i = 0; i < n; i++) {
        int v = topo[i];
        for (int w = 0; w < n; w++) {
            if (graph[v][w] != 0) { // 存在弧 <v, w>
                if (ve[v] + graph[v][w] > ve[w]) {
                    ve[w] = ve[v] + graph[v][w]; // 取最大值
                }
            }
        }
    }

    // 获取整个工程的终点事件和总工期
    int end_vertex = topo[n - 1];
    int total_project_time = ve[end_vertex];

    // 3. 初始化 vl 数组(全部置为总工期)
    for (int i = 0; i < n; i++) {
        vl[i] = total_project_time;
    }

    // 4. 逆推计算 vl (Latest Event Time)
    // 从拓扑序列的倒数第二个节点开始向前逆推(终点的 vl 已经等于总工期,无需计算)
    for (int i = n - 2; i >= 0; i--) {
        int v = topo[i];
        for (int w = 0; w < n; w++) {
            if (graph[v][w] != 0) { // 找到 v 的后置事件 w
                // 事件 v 的最迟发生时间,必须满足其所有后置活动的要求,故取最小值
                if (vl[w] - graph[v][w] < vl[v]) {
                    vl[v] = vl[w] - graph[v][w];
                }
            }
        }
    }

    // 5. 遍历所有边,计算活动的最早开始时间 e 和最迟开始时间 l,找出关键活动
    printf("--- 关键路径分析结果 ---\n");
    for (int v = 0; v < n; v++) {
        for (int w = 0; w < n; w++) {
            if (graph[v][w] != 0) {
                int e = ve[v];               // 活动 <v, w> 的最早开始时间等于起点事件的最早发生时间
                int l = vl[w] - graph[v][w]; // 活动 <v, w> 的最迟开始时间等于终点事件最迟发生时间减去活动持续时间
                
                if (e == l) {
                    printf("关键活动: 顶点 %d -> 顶点 %d (持续时间: %d)\n", v, w, graph[v][w]);
                }
            }
        }
    }
    printf("------------------------\n");

    return true;
}

手算速记: 拓扑排序→正推 ve(取 max)→逆推 vl(取 min)→算每条边的 e 和 l→相等的就是关键活动。整个工程的最短工期就是终点的 veve 值。408 偶尔还会问“缩短某个活动能不能缩短总工期”,答案就一句:只有缩短关键路径上的活动才有效,而且缩短之后要重新检查关键路径有没有发生变化。

核心逻辑口诀小结:

  • 事件最早来(veve):顺着拓扑序正推,遇到多条支路汇合,取最大值(MAX)。
  • 事件最迟走(vlvl):逆着拓扑序逆推,遇到多条支路分叉,取最小值(MIN)。
  • 活动不磨叽(e==le == l):活动的最早开始时间和最迟开始时间完全相等 —— 余量为零,就是关键路径

这一个多月落下不少进度。别人在一轮末尾查漏补缺,我还在跟邻接表、十字链表死磕。有时候也挺烦躁,尤其是数学证明题做不出来(我真的不喜欢证明题与概念题,写这两种题型时,半天‘憋’不出一个思路,真的很难受!哎,但恨归恨,考场上这两类题也会考,后续还得多练,硬着头皮也得把套路摸熟。)、专业课的代码又写出一堆 bug 的时候。

六月过了三分之二,上海交大还是个挺远的目标。其实想想,人生也像一张图,到处都是节点和带权值的边。前段时间正式被接收为预备党员,算是给这条路标了个最重要的节点。接下来就是一步步走,像 Dijkstra 那样,不去看整张图有多大、别人走得多快,只管每一步踩在当前最踏实的路径上。

图论这章硬骨头啃得差不多了,明天开始刷配套的习题,后续就开始下一章 —— 查找。保持节奏,继续走。

投喂·结缘
WeChatPay
WeChat 长按或识别
AliPay
Ali 长按或识别