图论理论基础(4)

文章目录

  • [题型:拓扑排序(Topological Sort)](#题型:拓扑排序(Topological Sort))
    • [1. 核心思路](#1. 核心思路)
      • [1.1 基本概念](#1.1 基本概念)
      • [1.2 适用场景](#1.2 适用场景)
      • [1.3 核心思想](#1.3 核心思想)
    • [2. 模板](#2. 模板)
      • [2.1 Kahn算法(BFS方法,推荐)](#2.1 Kahn算法(BFS方法,推荐))
      • [2.2 DFS方法](#2.2 DFS方法)
      • [2.3 时间复杂度分析](#2.3 时间复杂度分析)
    • [3. 常见变形](#3. 常见变形)
      • [3.1 判断有向图是否有环](#3.1 判断有向图是否有环)
      • [3.2 求所有可能的拓扑排序](#3.2 求所有可能的拓扑排序)
      • [3.3 字典序最小的拓扑排序](#3.3 字典序最小的拓扑排序)
      • [3.4 分层拓扑排序](#3.4 分层拓扑排序)
      • [3.5 最长路径(关键路径)](#3.5 最长路径(关键路径))
    • [4. 典型应用场景](#4. 典型应用场景)
    • [5. Kahn算法 vs DFS方法](#5. Kahn算法 vs DFS方法)
    • [6. 注意事项](#6. 注意事项)

题型:拓扑排序(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方法,推荐)

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

cpp 复制代码
#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方法

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

cpp 复制代码
#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 判断有向图是否有环

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

cpp 复制代码
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 求所有可能的拓扑排序

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

cpp 复制代码
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 字典序最小的拓扑排序

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

cpp 复制代码
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 分层拓扑排序

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

cpp 复制代码
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中,拓扑排序可以用于计算最长路径:

cpp 复制代码
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方法统一处理
相关推荐
好易学·数据结构6 小时前
可视化图解算法72:斐波那契数列
数据结构·算法·leetcode·动态规划·力扣·牛客网
崇山峻岭之间6 小时前
C++ Prime Plus 学习笔记025
c++·笔记·学习
bkspiderx6 小时前
C++操作符优先级与结合性全解析
c++·思维导图·操作符优先级·结合性
楼田莉子6 小时前
基于Linux的个人制作的文件库+标准输出和标准错误
linux·c语言·c++·学习·vim
数据门徒6 小时前
《人工智能现代方法(第4版)》 第6章 约束满足问题 学习笔记
人工智能·笔记·学习·算法
FPGA_无线通信6 小时前
OFDM 频偏补偿和相位跟踪(1)
算法·fpga开发
数据门徒7 小时前
《人工智能现代方法(第4版)》 第8章 一阶逻辑 学习笔记
人工智能·笔记·学习·算法
繁华似锦respect7 小时前
单例模式出现多个单例怎么确定初始化顺序?
java·开发语言·c++·单例模式·设计模式·哈希算法·散列表
风止何安啊7 小时前
递归 VS 动态规划:从 “无限套娃计算器” 到 “积木式解题神器”
前端·javascript·算法