图论:拓扑排序

我们经常会遇到存在依赖关系 的问题:比如课程先修、任务调度、路径规划(只能单向移动)等。这类问题抽象到图论中,就是有向无环图(DAG)的处理,而拓扑排序就是解决这类问题的核心算法。


一、核心概念

1.1 什么是拓扑排序

拓扑排序是对 ** 有向无环图(DAG)** 的所有顶点进行线性排序,使得对于图中任意一条有向边 u → v,顶点 u 在排序结果中一定出现在 v 之前。

简单来说:把有依赖关系的节点排成一个合法的线性序列

1.2 核心前提:必须是有向无环图(DAG)

拓扑排序仅适用于无环的有向图

  • 如果图中有环(比如 A→B,B→A),则不存在合法的拓扑序列
  • 本题中地窖只能从小编号移动到大编号,天然无环,适配拓扑排序

1.3 两种经典实现算法

  1. Kahn 算法(入度表 + 队列,最常用) 统计每个节点的入度,不断将入度为 0 的节点加入队列,删除其出边,重复操作直到所有节点处理完毕。
  2. DFS 拓扑排序 深度优先遍历,节点递归结束后加入栈,最终倒序输出即为拓扑序列。

二、拓扑排序的经典应用场景

拓扑排序不只是排序工具,更是解决DAG 相关问题的利器,核心应用:

  1. 判断有向图是否存在环
  2. DAG 上的最短 / 最长路径(本题核心考点);
  3. 任务调度、课程安排、编译依赖等依赖关系问题;
  4. 有向图的节点优先级排序。

三、例题:NOIP1996 挖地雷(P2196)

3.1 题目描述

N(N≤20) 个地窖,每个地窖有固定数量的地雷,地窖之间有单向路径:

  • 只能从当前地窖移动到编号更大且联通的地窖;
  • 从任意地窖开始挖,无满足条件的节点时结束;
  • 能挖到的最多地雷数 ,并输出挖地雷的路径

3.2 题目关键分析

  1. 图的建模 :地窖为节点,联通路径为有向边 i→j (j>i),构成有向无环图(DAG)
  2. 问题转化 :求 DAG 上的最长路径(路径权值为地雷数之和);
  3. 对比暴力 DFS :你之前写的 DFS 可以通过本题(N≤20),但数据量变大后会超时;而拓扑排序 + DP 时间复杂度仅 O(N+M),是最优解。

3.3 解题思路

  1. 建图:用邻接表存储地窖的连接关系,统计每个节点的入度;
  2. 拓扑排序:用 Kahn 算法得到 DAG 的拓扑序列;
  3. 动态规划
    • 定义 dp[i]:以节点 i 为终点的最大地雷数;
    • 转移方程:dp[j] = max(dp[j], dp[i] + boom[j])i→j 有边);
  4. 路径记录:用前驱数组记录每个节点的上一个节点,最终回溯得到最优路径。

3.4 代码实现(拓扑排序 + DP + 路径记录)

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;

const int MAXN = 25;
int n;
int boom[MAXN];               // 每个地窖的地雷数
vector<int> adj[MAXN];       // 邻接表存图
int in_degree[MAXN];         // 入度表
int dp[MAXN];                // dp[i]: 以i为终点的最大地雷数
int pre[MAXN];               // 记录路径前驱节点
vector<int> topo;            // 拓扑序列

// Kahn算法拓扑排序
void topological_sort() {
    queue<int> q;
    // 入度为0的节点入队
    for (int i = 1; i <= n; ++i) {
        if (in_degree[i] == 0) {
            q.push(i);
        }
    }
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        topo.push_back(u);
        // 遍历邻接点,更新入度
        for (int v : adj[u]) {
            in_degree[v]--;
            if (in_degree[v] == 0) {
                q.push(v);
            }
        }
    }
}

// 回溯输出路径
void print_path(int x) {
    if (pre[x] == 0) {
        cout << x;
        return;
    }
    print_path(pre[x]);
    cout << " " << x;
}

int main() {
    cin >> n;
    // 输入地雷数
    for (int i = 1; i <= n; ++i) {
        cin >> boom[i];
        dp[i] = boom[i];    // 初始化:每个节点自身为起点,地雷数为自身值
        pre[i] = 0;         // 初始化前驱为0
    }
    // 输入连接关系,建图
    for (int i = 1; i <= n-1; ++i) {
        for (int j = i+1; j <= n; ++j) {
            int x;
            cin >> x;
            if (x == 1) {
                adj[i].push_back(j);   // i->j 有边
                in_degree[j]++;        // j的入度+1
            }
        }
    }
    // 1. 拓扑排序
    topological_sort();
    // 2. 按拓扑序列DP求最长路径
    for (int u : topo) {
        for (int v : adj[u]) {
            if (dp[v] < dp[u] + boom[v]) {
                dp[v] = dp[u] + boom[v];
                pre[v] = u;  // 记录前驱
            }
        }
    }
    // 3. 找最大地雷数对应的终点
    int max_boom = 0, end_node = 1;
    for (int i = 1; i <= n; ++i) {
        if (dp[i] > max_boom) {
            max_boom = dp[i];
            end_node = i;
        }
    }
    // 4. 输出路径和结果
    print_path(end_node);
    cout << endl << max_boom << endl;
    return 0;
}

3.5 代码解析

  1. 建图 :严格按照题目输入格式,用邻接表存储 i→j 的有向边,统计入度;
  2. 拓扑排序:Kahn 算法生成拓扑序列,保证处理节点时,所有前驱节点已处理完毕;
  3. DP 转移:按拓扑序列遍历,更新每个节点的最大地雷数,同时记录前驱;
  4. 路径回溯:从最优终点递归回溯前驱节点,输出合法路径。

输入样例

复制代码
5
10 8 4 7 6
1 1 1 0
0 0 0
1 1
1

输出结果

复制代码
1 3 4 5
27

四、拓扑排序解题通用模板

4.1 Kahn 算法拓扑排序模板

cpp 复制代码
vector<int> adj[MAXN];
int in_degree[MAXN];
vector<int> topo;

void topo_sort(int n) {
    queue<int> q;
    for(int i=1;i<=n;i++) if(in_degree[i]==0) q.push(i);
    while(!q.empty()){
        int u=q.front();q.pop();
        topo.push_back(u);
        for(int v:adj[u]){
            if(--in_degree[v]==0) q.push(v);
        }
    }
}

4.2 DAG 最长路径通用模板

cpp 复制代码
int dp[MAXN];
// 初始化dp[i] = 节点i的权值
for(auto u:topo){
    for(auto v:adj[u]){
        if(dp[v] < dp[u] + weight[v]){
            dp[v] = dp[u] + weight[v];
            pre[v] = u; // 记录路径
        }
    }
}

五、总结

  1. 拓扑排序 :仅适用于有向无环图(DAG),核心是将依赖关系线性化;
  2. 最优应用 :DAG 上的最长 / 最短路径,拓扑排序 + DP 是标准解法;
  3. 本题关键:地窖只能向小编号到大编号移动,天然构成 DAG,完美适配拓扑排序。
相关推荐
星马梦缘35 分钟前
算法设计与分析 作业三 答案与解析
算法·线性规划·二分图匹配·多元最短路·流网络·bellmanford·匈牙利树算法
微风欲寻竹影37 分钟前
Java数据结构——二叉树(Binary Tree)详解
java·数据结构·算法
想吃火锅100538 分钟前
【leetcode】3.无重复字符的最长字串js版
算法·leetcode·职场和发展
smith成长之旅41 分钟前
08 | Mem0 框架分析: BM25 的 Sigmoid 归一化
数据库·python·算法
dongf201943 分钟前
R 语言随机森林算法
算法·随机森林·r语言
悠仁さん1 小时前
数据结构 排序
数据结构·算法·排序算法
阿文的代码库1 小时前
机器学习之精确率和召回率的关系
人工智能·算法·机器学习
咸鱼翻身小阿橙1 小时前
高斯模糊降噪/磨皮算法降噪图像
前端·opencv·算法·webpack·c#
代码中介商1 小时前
数据结构进阶(五):最短路径——Dijkstra 与 Floyd 算法
数据结构·算法