深入解析:图论理论基础(4)

题型:拓扑排序(Topological Sort)

1. 核心思路

拓扑排序是对**有向无环图(DAG, Directed Acyclic Graph)**的顶点进行线性排序,使得对于图中的每一条有向边 (u, v),u 在排序中都出现在 v 之前。

1.1 基本概念

  • 有向无环图(DAG):有向图中不存在环的图
  • 拓扑排序:将DAG的所有顶点排成线性序列,满足所有有向边的方向性
  • 入度(In-degree):指向该顶点的边的数量
  • 出度(Out-degree):从该顶点出发的边的数量

示例

有向图:       拓扑排序结果:
A → B → D      A → B → C → D
↓   ↓         或
C   E          A → C → B → D → E
    ↓
    D

1.2 适用场景

  • 课程表问题:先修课程必须在后续课程之前完成
  • 任务调度:某些任务必须在其他任务之前执行
  • 依赖关系:编译顺序、软件包安装顺序
  • 判断有向图是否有环:如果拓扑排序结果包含所有节点,则无环;否则有环

1.3 核心思想

拓扑排序的核心思想是从入度为0的节点开始,逐步移除节点并更新其他节点的入度

算法流程

  1. 初始化:统计所有节点的入度
  2. 找起点:将所有入度为0的节点加入队列
  3. 处理节点
    • 从队列中取出一个节点,加入结果
    • 将该节点指向的所有节点的入度减1
    • 如果某个节点的入度变为0,将其加入队列
  4. 判断:如果结果集中节点数等于图中节点数,则拓扑排序成功;否则图中有环

图解过程

初始图:       步骤1:A入度为0     步骤2:处理A后     步骤3:处理B后
A → B → D      A入队              B入度变为1         C入度变为0
↓   ↓          B入度=1            C入度=1           C入队
C   E          C入度=1            D入度=1            D入度=1
    ↓          D入度=2            E入度=1            E入度=1
    D          E入度=1
最终结果:A → B → C → D → E

2. 模板

2.1 Kahn算法(BFS方法,推荐)

基于入度的拓扑排序,使用队列实现

#include<iostream>
  #include<vector>
    #include<queue>
      #include<unordered_map>
        using namespace std;
        int main(){
        int n, m, s, t;  // n是节点数,m是边数
        cin >> n >> m;
        vector<int> inDegree(n, 0);  // 记录每个节点的入度
          unordered_map<int, vector<int>> graph;  // 记录图的邻接关系
            vector<int> result;  // 记录拓扑排序结果
              // 读入边,构建图和入度数组
              while(m--){
              cin >> s >> t;  // s指向t,即s是t的前驱
              inDegree[t]++;  // t的入度加1
              graph[s].push_back(t);  // s指向t
              }
              queue<int> que;
                // 将所有入度为0的节点加入队列
                for(int i = 0; i < n; i++){
                if(inDegree[i] == 0) {
                que.push(i);
                }
                }
                // BFS处理
                while(!que.empty()){
                int cur = que.front();  // 取出队首节点
                que.pop();
                result.push_back(cur);  // 加入结果
                // 遍历cur指向的所有节点
                vector<int> neighbors = graph[cur];
                  for(int i = 0; i < neighbors.size(); i++){
                  int next = neighbors[i];
                  inDegree[next]--;  // 入度减1
                  if(inDegree[next] == 0) {  // 如果入度变为0,加入队列
                  que.push(next);
                  }
                  }
                  }
                  // 输出结果
                  if(result.size() == n){
                  // 拓扑排序成功,输出结果
                  for(int i = 0; i < n - 1; i++) {
                  cout << result[i] << " ";
                  }
                  cout << result[n - 1] << endl;
                  } else {
                  // 图中有环,无法完成拓扑排序
                  cout << -1 << endl;
                  }
                  return 0;
                  }

2.2 DFS方法

基于深度优先搜索的拓扑排序

#include<iostream>
  #include<vector>
    #include<unordered_map>
      using namespace std;
      vector<int> result;
        vector<int> visited;  // 0:未访问, 1:正在访问, 2:已访问
          bool dfs(int node, unordered_map<int, vector<int>>& graph) {
            if(visited[node] == 1) return false;  // 发现环
            if(visited[node] == 2) return true;   // 已处理过
            visited[node] = 1;  // 标记为正在访问
            // 递归处理所有邻居
            for(int neighbor : graph[node]) {
            if(!dfs(neighbor, graph)) {
            return false;  // 发现环
            }
            }
            visited[node] = 2;  // 标记为已访问
            result.push_back(node);  // 后序遍历,最后加入结果
            return true;
            }
            int main(){
            int n, m, s, t;
            cin >> n >> m;
            unordered_map<int, vector<int>> graph;
              visited.resize(n, 0);
              while(m--){
              cin >> s >> t;
              graph[s].push_back(t);
              }
              // 对所有未访问的节点进行DFS
              for(int i = 0; i < n; i++){
              if(visited[i] == 0) {
              if(!dfs(i, graph)) {
              cout << -1 << endl;  // 有环
              return 0;
              }
              }
              }
              // 反转结果(DFS是后序遍历,需要反转)
              reverse(result.begin(), result.end());
              for(int i = 0; i < n - 1; i++) {
              cout << result[i] << " ";
              }
              cout << result[n - 1] << endl;
              return 0;
              }

2.3 时间复杂度分析

  • Kahn算法(BFS)
    • 时间复杂度:O(V + E),每个节点和边访问一次
    • 空间复杂度:O(V),队列和入度数组
  • DFS方法
    • 时间复杂度:O(V + E)
    • 空间复杂度:O(V),递归栈深度

3. 常见变形

3.1 判断有向图是否有环

拓扑排序的副产品,如果结果集中节点数小于图中节点数,则存在环:

bool hasCycle(int n, vector<vector<int>>& edges) {
  vector<int> inDegree(n, 0);
    vector<vector<int>> graph(n);
      // 构建图和入度
      for(auto& edge : edges) {
      graph[edge[0]].push_back(edge[1]);
      inDegree[edge[1]]++;
      }
      queue<int> que;
        for(int i = 0; i < n; i++) {
        if(inDegree[i] == 0) que.push(i);
        }
        int count = 0;
        while(!que.empty()) {
        int cur = que.front();
        que.pop();
        count++;
        for(int next : graph[cur]) {
        inDegree[next]--;
        if(inDegree[next] == 0) {
        que.push(next);
        }
        }
        }
        return count != n;  // 如果count < n,说明有环
        }

3.2 求所有可能的拓扑排序

使用回溯法枚举所有可能的拓扑排序:

vector<vector<int>> allTopologicalSorts(int n, vector<vector<int>>& edges) {
  vector<vector<int>> graph(n);
    vector<int> inDegree(n, 0);
      for(auto& edge : edges) {
      graph[edge[0]].push_back(edge[1]);
      inDegree[edge[1]]++;
      }
      vector<vector<int>> result;
        vector<int> path;
          vector<bool> visited(n, false);
            function<void()> backtrack = [&]() {
              if(path.size() == n) {
              result.push_back(path);
              return;
              }
              for(int i = 0; i < n; i++) {
              if(!visited[i] && inDegree[i] == 0) {
              visited[i] = true;
              path.push_back(i);
              // 更新入度
              for(int next : graph[i]) {
              inDegree[next]--;
              }
              backtrack();
              // 回溯
              for(int next : graph[i]) {
              inDegree[next]++;
              }
              path.pop_back();
              visited[i] = false;
              }
              }
              };
              backtrack();
              return result;
              }

3.3 字典序最小的拓扑排序

使用优先队列(小根堆)保证字典序最小:

vector<int> topologicalSortLexicographically(int n, vector<vector<int>>& edges) {
  vector<vector<int>> graph(n);
    vector<int> inDegree(n, 0);
      for(auto& edge : edges) {
      graph[edge[0]].push_back(edge[1]);
      inDegree[edge[1]]++;
      }
      priority_queue<int, vector<int>, greater<int>> pq;  // 小根堆
        for(int i = 0; i < n; i++) {
        if(inDegree[i] == 0) pq.push(i);
        }
        vector<int> result;
          while(!pq.empty()) {
          int cur = pq.top();
          pq.pop();
          result.push_back(cur);
          for(int next : graph[cur]) {
          inDegree[next]--;
          if(inDegree[next] == 0) {
          pq.push(next);
          }
          }
          }
          return result;
          }

3.4 分层拓扑排序

按拓扑排序的层次输出结果:

vector<vector<int>> levelTopologicalSort(int n, vector<vector<int>>& edges) {
  vector<vector<int>> graph(n);
    vector<int> inDegree(n, 0);
      for(auto& edge : edges) {
      graph[edge[0]].push_back(edge[1]);
      inDegree[edge[1]]++;
      }
      queue<int> que;
        for(int i = 0; i < n; i++) {
        if(inDegree[i] == 0) que.push(i);
        }
        vector<vector<int>> levels;
          while(!que.empty()) {
          int size = que.size();
          vector<int> level;
            for(int i = 0; i < size; i++) {
            int cur = que.front();
            que.pop();
            level.push_back(cur);
            for(int next : graph[cur]) {
            inDegree[next]--;
            if(inDegree[next] == 0) {
            que.push(next);
            }
            }
            }
            levels.push_back(level);
            }
            return levels;
            }

3.5 最长路径(关键路径)

在DAG中,拓扑排序可以用于计算最长路径:

int longestPath(int n, vector<vector<int>>& edges) {
  vector<vector<int>> graph(n);
    vector<int> inDegree(n, 0);
      vector<int> dist(n, 0);  // 记录到每个节点的最长路径
        for(auto& edge : edges) {
        graph[edge[0]].push_back(edge[1]);
        inDegree[edge[1]]++;
        }
        queue<int> que;
          for(int i = 0; i < n; i++) {
          if(inDegree[i] == 0) {
          que.push(i);
          dist[i] = 1;  // 起点距离为1
          }
          }
          while(!que.empty()) {
          int cur = que.front();
          que.pop();
          for(int next : graph[cur]) {
          dist[next] = max(dist[next], dist[cur] + 1);
          inDegree[next]--;
          if(inDegree[next] == 0) {
          que.push(next);
          }
          }
          }
          return *max_element(dist.begin(), dist.end());
          }

4. 典型应用场景

  1. 课程表问题

    • LeetCode 207. 课程表(判断是否可以完成)
    • LeetCode 210. 课程表 II(输出学习顺序)
    • 判断先修课程关系是否合理
  2. 任务调度问题

    • 项目任务依赖关系
    • 编译顺序问题
    • 软件包安装顺序
  3. 依赖关系问题

    • 文件依赖关系
    • 模块依赖关系
    • 事件先后顺序
  4. 判断有向图是否有环

    • 检测依赖关系中的循环依赖
    • 验证任务调度的可行性
  5. 关键路径问题

    • 项目管理中的关键路径分析
    • 最长路径计算

5. Kahn算法 vs DFS方法

比较项Kahn算法(BFS)DFS方法
实现方式队列 + 入度统计递归 + 三色标记
代码复杂度简单中等
空间复杂度O(V)O(V)(递归栈)
时间复杂度O(V+E)O(V+E)
优势实现简单,易于理解可以检测环的具体位置
劣势需要额外空间存储入度递归可能栈溢出
推荐推荐使用特殊场景使用

选择建议

  • 一般情况 → 使用 Kahn算法(BFS),实现简单,效率高
  • 需要检测环的位置 → 使用 DFS方法
  • 需要所有拓扑排序 → 使用 回溯法

6. 注意事项

  1. 只适用于有向无环图(DAG)

    • 如果图中有环,拓扑排序无法完成
    • 可以通过结果集大小判断是否有环
  2. 拓扑排序结果不唯一

    • 一个DAG可能有多个有效的拓扑排序
    • 如果需要字典序最小,使用优先队列
  3. 入度统计要准确

    • 确保正确统计每个节点的入度
    • 注意边的方向(s → t 表示s是t的前驱)
  4. 处理多个连通分量

    • 如果图不连通,需要对每个连通分量分别处理
    • 或者使用DFS方法统一处理
posted @ 2026-01-04 19:44  gccbuaa  阅读(2)  评论(0)    收藏  举报