图论专题(十八):“逆向”拓扑排序——寻找图中的「最终安全状态」

哈喽各位,我是前端小L。

欢迎来到我们的图论专题第十八篇!我们已经习惯了"从入度为0"的节点开始拓扑排序,这代表着"事件的开始"。

今天,我们要解决的问题是:"哪些节点是安全的?"

  • 安全节点 :从这个节点出发,无论怎么走,最终一定 能到达一个"终端节点"(即没有出边的节点),而绝对不会陷入"死循环"(环)。

这听起来像是要预测未来。但如果我们反过来想

  1. 终端节点(出度为0)本身肯定是安全的。

  2. 如果一个节点,它所有 的连线都指向安全节点,那它自己也一定是安全的!

这不就是拓扑排序的逻辑吗?只不过方向反了!

  • 标准拓扑排序:从"入度0"开始,顺着边找。

  • 安全节点查找 :从"出度0"开始,逆着边找!

力扣 802. 找到最终的安全状态

https://leetcode.cn/problems/find-eventual-safe-states/

题目分析:

  • 输入 :一个有向图 graph(邻接表形式,graph[i]i 指向的节点列表)。

  • 目标:返回所有"最终安全节点"的列表,按升序排列。

  • 定义:如果一个节点的所有路径最后都能停下(不进环),它就是安全的。

思路一:DFS 三色标记法 (检测环)

我们在 LC 207 中学过用 DFS 检测环。这里逻辑是一样的:

  • 0 (White):未访问。

  • 1 (Gray) :正在访问(当前递归栈中)。如果遇到 Gray 节点,说明遇到了环 ,当前路径上的所有节点都不安全

  • 2 (Black) :已访问且安全(所有子孙都安全)。

算法流程 : 对每个节点调用 DFS。如果 DFS 返回 true(安全),加入结果列表。

  • dfs(u) 中:

    • 标记 u 为 1。

    • 遍历邻居 v

      • 如果 v 是 1,说明有环,u 不安全,返回 false

      • 如果 v 是 0 且 dfs(v) 返回 falseu 也不安全,返回 false

      • (如果 v 是 2,说明 v 安全,继续检查其他邻居)。

    • 所有邻居都检查完了,说明 u 安全。标记 u 为 2,返回 true

思路二:"逆向" Kahn 算法 (BFS) ------ 更加优雅的拓扑视角

DFS 虽然有效,但"三色标记"稍微有点绕。让我们试试"逆向思维"。

我们知道 Kahn 算法 是不断移除 入度为 0 的节点。 现在,我们要找"安全节点",其实就是要不断移除 出度为 0 的节点!

核心逻辑:

  1. 终端节点(出度为 0)是安全的。

  2. 当我们把所有"终端节点"从图中移除(这是逻辑上的移除,实际上是把指向它们的边的源头 节点的出度减 1),那些原本指向终端节点 的节点,如果出度也变成了 0,说明它们现在的路也"断"了(只能通向已知的安全区),那么它们也变成了新的安全节点!

  3. 这个过程不断重复,直到没有新的出度为 0 的节点产生。

  4. 所有被"选中"过的节点,就是安全节点。

工程实现上的关键点: 我们需要"逆着边"找。原图给出的是 u -> v。为了知道"谁指向了 v",我们需要构建一个反图 (Reverse Graph)v -> u

算法流程:

  1. 建反图 & 统计出度

    • 创建 rev_adj。遍历原图,对于边 u -> v,在反图中添加 v -> u

    • 同时,统计原图中每个节点 u出度 outdegree[u]

  2. 初始化队列

    • 将所有出度为 0 的节点(终端节点)入队。
  3. BFS (逆向拓扑)

    • while (!q.empty()):

      • curr = q.front(); q.pop();

      • safe[curr] = true;

      • 遍历反图 rev_adj[curr] 中的邻居 prev(即原图中指向 curr 的节点):

        • outdegree[prev]--prev 的一条出路通向了安全区,可以"划掉"了)。

        • 核心判断 :如果 outdegree[prev] == 0,说明 prev 的所有出路都通向了安全区,那么 prev 也是安全的!入队

  4. 整理结果

    • 遍历 safe 数组,收集所有 true 的索引,排序返回。

代码实现 (逆向 Kahn 算法)

C++

复制代码
#include <vector>
#include <queue>
#include <algorithm>

using namespace std;

class Solution {
public:
    vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
        int n = graph.size();
        vector<vector<int>> rev_adj(n);
        vector<int> outdegree(n, 0);

        // 1. 建反图 + 统计原图出度
        for (int u = 0; u < n; ++u) {
            outdegree[u] = graph[u].size();
            for (int v : graph[u]) {
                // 原图 u -> v
                // 反图 v -> u
                rev_adj[v].push_back(u);
            }
        }

        // 2. 将所有出度为 0 的节点(终端节点)入队
        queue<int> q;
        for (int i = 0; i < n; ++i) {
            if (outdegree[i] == 0) {
                q.push(i);
            }
        }

        // 3. 逆向 BFS
        vector<bool> isSafe(n, false);
        while (!q.empty()) {
            int curr = q.front();
            q.pop();
            isSafe[curr] = true;

            // 在反图中,curr 指向 prev (即原图中 prev -> curr)
            for (int prev : rev_adj[curr]) {
                outdegree[prev]--;
                if (outdegree[prev] == 0) {
                    q.push(prev);
                }
            }
        }

        // 4. 收集结果
        vector<int> result;
        for (int i = 0; i < n; ++i) {
            if (isSafe[i]) {
                result.push_back(i);
            }
        }
        return result;
    }
};

深度复杂度分析

  • V (Vertices) :节点数 n

  • E (Edges):边数。

  • 时间复杂度 O(V + E)

    • 建反图遍历一次所有边:O(E)。

    • 初始化队列遍历所有节点:O(V)。

    • BFS 过程中,每个节点最多入队一次,每条边(反图中的)最多被遍历一次:O(V + E)。

    • 结果排序(收集过程本身是有序的,所以只需要 O(V))。

  • 空间复杂度 O(V + E)

    • rev_adj 反图存储所有边。

    • outdegree 数组 O(V)。

    • 队列 O(V)。

总结

今天这道题,让我们见识了拓扑排序的灵活应用。

  • 标准拓扑排序:从"入度0"开始,解决"依赖/顺序"问题。

  • 逆向拓扑排序:从"出度0"开始,解决"安全/终点"问题。

通过构建反图,我们成功地复用了 Kahn 算法的模板,将一个看似复杂的"路径终点判定"问题,转化为一个清晰的"层层剥离"过程。

在下一篇中,我们将面对一个更具挑战性的 DAG 问题------平行课程 。我们需要计算的不再是顺序,而是修完所有课程所需的最少学期数(实际上是求 DAG 上的最长路径)。

下期见!

相关推荐
前端小L1 小时前
图论专题(十七):从“判定”到“构造”——生成一份完美的「课程表 II」
算法·矩阵·深度优先·图论·宽度优先
qq_433554541 小时前
C++ 稀疏表
开发语言·c++·算法
小白程序员成长日记2 小时前
2025.11.21 力扣每日一题
算法·leetcode·职场和发展
q***42822 小时前
IPV6公网暴露下的OPENWRT防火墙安全设置(只允许访问局域网中指定服务器指定端口其余拒绝)
服务器·安全·php
jenchoi4132 小时前
【2025-11-19】软件供应链安全日报:最新漏洞预警与投毒预警情报汇总
网络·安全·web安全·网络安全·npm
小年糕是糕手3 小时前
【C++】C++入门 -- inline、nullptr
linux·开发语言·jvm·数据结构·c++·算法·排序算法
网硕互联的小客服3 小时前
Linux 系统CPU 100% 怎么办?如何处理?
运维·服务器·网络·安全
jenchoi4133 小时前
【2025-11-18】软件供应链安全日报:最新漏洞预警与投毒预警情报汇总
网络·数据库·安全·web安全·网络安全
Black蜡笔小新3 小时前
视频融合平台EasyCVR助力守护渔业牧区安全与增效
安全·音视频