拓扑排序

有向无环图

在讨论拓扑排序之前,要先知道一个重要的概念:有向无环图。

有向无环图,简称 DAG(Directed Acyclic Graph),是一种图论中的概念,它具有以下两个主要特征:

  1. 有向(Directed):图中的每条边都有一个方向,即每条边从一个节点指向另一个节点。例如,如果存在一条边 u→v,这表示从节点 u 指向节点 v;
  2. 无环(Acyclic):图中不存在任何包含多个节点的环路。也就是说,从一个节点出发,通过若干条边可以到达的其他节点中,不可能回到这个出发节点;

DAG 是能够进行拓扑排序的唯一图结构。因为没有环,节点可以按照依赖关系排序。

拓扑排序的思想

依赖关系的排序

拓扑排序的目标是对节点进行排序,确保在排序结果中,每个节点都在其所有依赖节点之后。也就是说,如果有一条有向边 u→v,则在排序结果中 u 必须排在 v 之前。

无环性

拓扑排序只能应用于有向无环图(DAG)。如果图中存在环(即有回路),则无法完成拓扑排序,因为环中的节点彼此依赖,无法确定一个合适的排序顺序。

入度的利用

入度表示指向某个节点的边的数量。拓扑排序从入度为 0 的节点开始,因为这些节点没有依赖关系,可以首先被处理。将一个入度为 0 的节点加入排序后,删除与该节点相关的边,并更新其他节点的入度。对于每一个入度变为 0 的节点,将其加入到下一轮排序中。

递归消除依赖

通过不断移除入度为 0 的节点及其边,图中的节点和边逐渐减少,最终所有节点都被排序,或检测到剩下的节点形成了环。

拓扑排序实现

Kahn 算法

这种方法直接使用入度的概念:

  1. 找出所有入度为 0 的节点,将它们放入一个队列中;
  2. 从队列中依次取出节点,将其加入拓扑排序结果;
  3. 删除该节点的所有出边,更新目标节点的入度。如果某个节点的入度变为 0,则其加入队列;
  4. 重复步骤 2 和 3,直到队列为空;
  5. 如果所有节点都被处理,则得到有效的拓扑排序;如果有节点未被处理,说明图中存在环;

DFS

通过深度优先搜索(DFS)实现拓扑排序:

  1. 对图中的每个节点执行 DFS,按照完成时间的逆序记录节点;
  2. 当 DFS 完成对某个节点的访问时,将该节点加入一个栈中;
  3. 最终栈中的节点顺序即为拓扑排序的结果;
  4. 这种方法也可以检测环,如果在 DFS 过程中遇到已经在访问中的节点,则说明图中存在环;

例题

拓扑排序的一大应用在于解决依赖问题。

题目

设计一个“包的依赖加载”算法,给定一个二维数组,其中一维代表包的索引,二维代表该包的依赖包索引(即必须先加载其依赖包才能加载该包),算法要求返回一个能让包正确加载的顺序。例如给定二维数组为[[2],[0,2],[3],[]],则代表包 0 的依赖包为包 2,包 1 的依赖包为包 0 和包 2,包 2 的依赖包为包 3,包 3 没有依赖包,所以返回的加载顺序应该是[3,2,0,1]。

解决步骤

这道题可以用基于 DFS 的拓扑排序实现:

  1. 将问题转化为有向图,每个包为一个节点,依赖关系为有向边;
  2. 使用深度优先搜索(DFS)检测每个节点是否被访问过;
  3. 如果某个包还没被加载,先加载它的依赖包,再加载该包;
  4. 确保避免循环依赖(这个问题假设依赖是无环的);

代码

#include <iostream>
#include <vector>
#include <stack>

using namespace std;

// 递归函数,使用DFS访问包,维护加载顺序
void dfs(int package, const vector<vector<int>> &dependencies, vector<bool> &visited, stack<int> &loadOrder)
{
    // 标记当前包为已访问
    visited[package] = true;

    // 遍历当前包的依赖包
    for (int dep: dependencies[package])
    {
        if (!visited[dep])
        {
            dfs(dep, dependencies, visited, loadOrder);
        }
    }

    // 当前包的所有依赖包都已加载,将当前包加入加载顺序
    loadOrder.push(package);
}

// 主算法函数
vector<int> loadPackages(const vector<vector<int>> &dependencies)
{
    int n = dependencies.size(); // 包的数量
    vector<bool> visited(n, false); // 记录包是否已访问
    stack<int> loadOrder;           // 存储加载顺序

    // 对每个包进行DFS
    for (int i = 0; i < n; i++)
    {
        if (!visited[i])
        {
            dfs(i, dependencies, visited, loadOrder);
        }
    }

    // 将栈中的加载顺序转换为结果数组
    vector<int> result;
    while (!loadOrder.empty())
    {
        result.push_back(loadOrder.top());
        loadOrder.pop();
    }

    return result;
}

int main()
{
    // 输入二维数组,表示每个包的依赖包索引
    vector<vector<int>> dependencies = {
            {2},       // 包0依赖包2
            {0, 2},    // 包1依赖包0和包2
            {3},       // 包2依赖包3
            {}         // 包3没有依赖
    };

    // 调用算法计算加载顺序
    vector<int> loadOrder = loadPackages(dependencies);

    // 输出加载顺序
    for (int pkg: loadOrder)
    {
        cout << pkg << " ";
    }

    return 0;
}

解释

  1. 输入是一个二维数组,表示每个包的依赖包索引。dependencies[i] 表示第 i 个包的所有依赖包的索引;
  2. DFS递归函数:通过深度优先搜索遍历所有包的依赖,确保在访问每个包之前,先访问它的所有依赖包;
  3. 结果存储在栈中:因为深度优先搜索是后序遍历(先访问依赖包,再访问自身),所以将结果存入栈中,最后再从栈中弹出生成正确的加载顺序;

示例运行:

对于输入 [[2], [0, 2], [3], []],输出将是:

3 2 0 1

即先加载包3,再加载包2、包0,最后加载包1。

算法复杂度:

  • 时间复杂度:O(V + E),其中V是包的数量,E是依赖关系的数量。
  • 空间复杂度:O(V),主要用于存储递归栈和结果。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇