【拓扑系列】拓扑排序

前言

认识有向无环图

图中不会形成环状的有向图就是有向无环图。

在这里插入图片描述

入度和出度

  • 出度:一个顶点指向另一个节点的数量。
  • 入度:一个顶点有多少是指向它的。

在这里插入图片描述

认识AOV网:顶点活动图

  • AOV网(Activity On Vertex Network)‌是一个用顶点表示活动,有向边表示活动之间优先关系的有向图。在这个图中,顶点代表工程中的各个活动,而有向边则反映了这些活动之间的先后依赖关系。

AOV网的特点包括:

  1. 顶点表示活动‌:在AOV网中,每一个顶点都代表一个具体的活动,这些活动构成了工程的各个组成部分。
    ‌有向边表示优先关系‌:有向边用来表示活动之间的优先关系,即一个活动必须在另一个活动完成之后才能开始。这种关系通过有向边在图中进行表示,从而形成一个有向图。
  2. 无回路‌:AOV网中不允许存在回路,即不存在一个活动直接或间接地依赖于它自身的完成,这保证了活动的逻辑顺序和工程的可行性。
  3. AOV网的概念在现代化管理中非常有用,特别是在描述和分析一项工程的计划和实施过程中。通过将工程分解为多个小的子工程(即活动),并用有向图来表示这些活动之间的依赖关系,可以帮助理解和优化工程的执行流程。此外,拓扑排序是AOV网的一个重要应用,用于判断网中是否存在环,确保所有活动都能按照正确的顺序执行,避免出现无法完成的循环依赖‌。

拓扑排序

  • 拓扑排序‌是对一个有向无环图(DAG)进行排序,将图中的所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。这样的线性序列称为满足拓扑次序的序列,简称拓扑序列。‌12

  • 拓扑排序常用于确定一个依赖关系集中事物发生的顺序。例如,在项目管理中,可以确定项目各阶段之间的依赖关系,从而计算出项目的执行顺序。

  • 拓扑排序的实现方法通常使用深度优先搜索(DFS)或广度优先搜索(BFS)。在树结构中,拓扑排序可以简单地通过层次遍历实现。

我们用更直接的话来讲就是:拓扑排序的作用就是找到做事情的先后顺序,并且拓扑排序的结果不唯一。

拓扑排序的具体操作:

  1. 找出图中入度为0的节点,然后输出
  2. 删除这个节点的连接的边
  3. 重复步骤1,2操作,直到图中没有点或者没有入度为0的节点为止(可能存在环)。所以可以根据这个特性判断图中是否有环。

实现拓扑排序:借助队列,来一次bfs即可
4. 初始化:将所有入度为0的节点添加的队列中
5. 当队列不为空的时候,拿出队头元素添加到结果中,删除该元素相连的边,判断删完后与删除边相连接的点入度是否为0,如果为0添加到队列,然后一直循环。

1. 课程表

1.1 题目来源

207. 课程表

1.2 题目描述

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false

  1. 示例 1:
    输入:numCourses = 2, prerequisites = [[1,0]]
    输出:true
    解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
  2. 示例 2:
    输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
    输出:false
    解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

提示:
1 <= numCourses <= 2000
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i] 中的所有课程对 互不相同

1.3 题目解析

class Solution {
public:
    vector<vector<int>> edges;
    vector<int> in;
    queue<int> q;
    vector<int> ret;

    bool bfs(int n)
    {
        // 将入度数为0的放入队列
        for (int i = 0; i < n; i++)
        {
            if (in[i] == 0)
            {
                q.push(i);
            }
        }
        while (!q.empty())
        {
            int t = q.front();
            q.pop();
            ret.push_back(t);
            for (auto v : edges[t])
            {
                if(--in[v] == 0)
                {
                    q.push(v);
                }
            }
        }
        return ret.size() == n; // 判断是否有环
    }
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) 
    {
        in.resize(numCourses);
        edges.resize(numCourses);
        // 建图
        for (auto point : prerequisites)
        {
            edges[point[1]].push_back(point[0]);
            //计入计入入度数
            in[point[0]]++;
        }
        return bfs(numCourses);
    }
};

2. 课程表 II

2.1 题目来源

210. 课程表 II

2.2 题目描述

现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。

例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。

  1. 示例 1:
    输入:numCourses = 2, prerequisites = [[1,0]]
    输出:[0,1]
    解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
  2. 示例 2:
    输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
    输出:[0,2,1,3]
    解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
    因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3]
  3. 示例 3:
    输入:numCourses = 1, prerequisites = []
    输出:[0]

提示:
1 <= numCourses <= 2000
0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2
0 <= ai, bi < numCourses
ai != bi
所有[ai, bi] 互不相同

2.3 题目解析

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) 
    {
        vector<vector<int>> edges(numCourses);
        vector<int> in(numCourses);
        vector<int> ret;
        queue<int> q;

        // 建图
        for (auto v : prerequisites)
        {
            edges[v[1]].push_back(v[0]);
            in[v[0]]++;
        }

        //将入度数为0的放入队列
        for (int i = 0; i < numCourses; i++)
        {
            if (in[i] == 0) q.push(i);
        }

        //深度遍历
        while (!q.empty())
        {
            int t = q.front();
            q.pop();
            ret.push_back(t);
            for (auto v : edges[t])
            {
                if (--in[v] == 0)
                    q.push(v);
            }
        }
        if (ret.size() == numCourses)
            return ret;
        else return vector<int>();
    }
};

3. LCR 114. 火星词典

3.1 题目来源

LCR 114. 火星词典

3.2 题目描述

现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。

给定一个字符串列表 words ,作为这门语言的词典,words 中的字符串已经 按这门新语言的字母顺序进行了排序 。

请你根据该词典还原出此语言中已知的字母顺序,并 按字母递增顺序 排列。若不存在合法字母顺序,返回 “” 。若存在多种可能的合法字母顺序,返回其中 任意一种 顺序即可。

字符串 s 字典顺序小于 字符串 t 有两种情况:

在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t 。
如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。

  1. 示例 1:
    输入:words = [“wrt”,“wrf”,“er”,“ett”,“rftt”]
    输出:“wertf”
  2. 示例 2:
    输入:words = [“z”,“x”]
    输出:“zx”
  3. 示例 3:
    输入:words = [“z”,“x”,“z”]
    输出:“”
    解释:不存在合法字母顺序,因此返回 “” 。
  4. 示例 4:
    输入:words = [“abc”,“ab”]
    输出:“”
    解释:不存在合法字母顺序,因此返回 “” 。

提示:
1 <= words.length <= 100
1 <= words[i].length <= 100
words[i] 仅由小写英文字母组成

3.3 题目解析

这个题目的关键就在于要理解题目意思,题目意思是,两个字符进行对比,一旦遇到了不相同的字符,字符在前面的是小于字符在后面的,并且如果s1的长度小于s2的话并且s1的长度大于s2的长,如果s2等于s1的部分字符串,就类似于示例4,那么也是不符合的。
在这里插入图片描述
所以我们该怎么处理words字典呢,这里我们就可以直接使用两层for循环,依次进行比较,并且一旦找到了的话其实可以用图的方式进行来连接例如(a->b 就标识a小于b)于是有了这个思路我们就可以使用拓扑排序了。

上面的几题都是对int整形进行比较的,也是使用的vector来存放图,但是这里就不一样了,类型是char,所以这里我们一应该使用hash表来进行存储,即unordered_map<char, unordered_set< char >>,因为字典中会后重复的,所以我们的邻接表也应该使用一个hash来进行存储。
而至于我们存放入度数的话也不能直接使用vector或者数组来存储了,也应该使用hash来进行存储。

所以我们的具体步骤就是:

  1. 创建unordered_map<char, unordered_set< char>> edges来建图,创建unordered_map<char, int> in来存放入度数
  2. 遍历整个字典将初始化入度in
  3. 使用双指针遍历字典,建立图
  4. 深度遍历
class Solution {
public:
    unordered_map<char, unordered_set<char>> edges;
    unordered_map<char, int> in;
    queue<char> q;
    string ret;
    string alienOrder(vector<string>& words) 
    {
        for (auto &s : words)
        {
            for (auto c : s)
            {
                in[c] = 0;
            }
        }
        
        // 建图
        for (int i = 0; i < words.size(); i++)
        {
            for (int j = i + 1; j < words.size(); j++)
            {
                if(add(words[i], words[j]))
                    return "";
            }
        }

        // 将度为0的放入队列
        for (auto &[a, b] : in)
        {
            if (b == 0)
                q.push(a);
        }
		// 进行深度遍历
        while (!q.empty())
        {
            char t = q.front();
            q.pop();
            ret += t;
            for (auto v : edges[t])
            {
                if (--in[v] == 0)
                    q.push(v);
            }
        }
        std::cout << ret;
        if (ret.size() == in.size()) return ret;
        else return "";
    }

    bool add (string& s1, string& s2)
    {
        int n = min(s1.size(), s2.size());
        int i = 0;
        for (; i < n; i++)
        {
            if (s1[i] != s2[i])
            {
                char a = s1[i], b = s2[i];
                if (!edges.count(a) | !edges[a].count(b))
                {
                    edges[a].insert(b);
                    in[b]++;
                }
                break;
            }
        }
        if (i == s2.size() && i < s1.size()) return true; // 处理特殊情况,示例4
        else return false;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三问走天下

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值