【算法】BFS解决拓扑排序类算法题(C++)

文章目录

前言

在数据结构中我们学过 拓扑排序以及图的相关知识,在这里我们进行简单的复习↓


有向无环图

我们下文要解的算法题,都可以用这种关系图来表示。


什么是拓扑排序?

在数据结构中我们学过:

  1. 拓扑排序是一种对 有向无环图(DAG) 进行排序的算法。
    • 在对于我们下面的解题,可以理解为拓扑排序是确定任务执行顺序的
  2. 如果图中存在环路,那么该图就没有拓扑排序,可以由此利用拓扑排序判断图是否有环

拓扑排序 实现思路

由于拓扑排序是用于找到一系列事件执行的先后顺序,实际上结果并不一定唯一。我们只需要从某个起点开始进行搜索,每次删掉搜索位置,直至最后看图是否有环。

如何进行排序?

  1. 找出图中入度为 0 的点,然后输出
  2. 删除与该点连接的边
  3. 重复 1、2 操作,直到图中没有点 / 没有入度为 0 的点为止

拓扑排序 代码思路

简单来说,我们提供的方法:借助队列,进行一次BFS

  1. 初始化: 把所有入度为 0 的点加入到队列中
  2. 当队列不为空时
    • 取队头元素,加入到最终结果中
    • 删除与该元素相连的边
    • 最后判断: 与删除边相连的点,入度是否变为 0
      • 如果入度为 0,加入到队列中

示例题

我们通过下面一道题加深对拓扑排序的理解,以及代码的编写。

207.课程表

示例:

思路

  • 题意分析:正如示例图所提到的,我们所需做的就是将题目给出的选修课顺序作图,并利用拓扑排序判断该图是否可以按顺序读完(无环)

怎么利用代码作图?

我们知道,对于任意的有向图,都可以用相应的邻接表 / 邻接矩阵表示,当我们要用代码作图,只需要利用邻接矩阵 + 容器即可。

  • 邻接矩阵:
  • 容器:

    我们可以选用如上图 ① 二维数组 ② 哈希表,进行邻接表的表示。

根据前面提到的实现思路 + 代码思路 ,以及上面的作图方法,就可以着手写代码了

代码

cpp 复制代码
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
    // 0. 初始化
    unordered_map<int, vector<int>> AList; // 哈希表作邻接表 表示图
    vector<int> inDegree(numCourses); // 记录每个点的入度

    // 1. 建图
    for(auto e : prerequisites)
    {
        int a = e[0], b = e[1]; // 提取一条边: b->a
        AList[b].push_back(a);
        inDegree[a]++; // 更新入度
    }

    // 拓扑排序
    // (1) 将所有入度为0的点加入到队列
    queue<int> q;
    for(int i = 0; i < numCourses; ++i)
    {
        if(inDegree[i] == 0)
            q.push(i);
    }
    
    // (2) 进行bfs
    while(q.size())
    {
        int t = q.front(); q.pop();
        // 对邻接表的行t进行: 
        for(int x : AList[t])
        {
            inDegree[x]--;
            if(inDegree[x] == 0) q.push(x);
        }
    }

    // 3. 判断此时表是否有环
    for(int i = 0; i < inDegree.size(); ++i)
        if(inDegree[i]) return false; // in[i] 如果!=0 则入度不为0,依然有元素(成环)
    
    return true;
}

210.课程表II

思路

  • 题意分析:这道题的思路与上一道题一模一样!唯一的区别就是本题要求返回任意一种存在的顺序即可。

  • 解法 :拓扑排序 + bfs

    • 这里我们选择用vector<vector<int>> 进行邻接表的创建
    • 我们只需要在bfs每次提取队头元素时,将当前元素加入到结果数组中即可,其余部分代码没有区别。

代码

cpp 复制代码
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
    vector<vector<int>> AList(numCourses); // 邻接表
    vector<int> inD(numCourses), ret; // inD用于记录入度

    // 1. 建图
    for(auto e : prerequisites)
    {
        int a = e[0], b = e[1];
        inD[a]++;
        AList[b].push_back(a);
    }

    // 2. 将入度为0的点入队
    queue<int> q;
    for(int i = 0; i < inD.size(); ++i)
        if(inD[i] == 0) q.push(i);

    // 3. 拓扑排序
    // (1) bfs
    while(q.size())
    {
        int t = q.front(); q.pop();
        ret.push_back(t);
        for(int x : AList[t])
        {
            inD[x]--;
            if(inD[x] == 0)
                q.push(x);
        }
    }

    // 存在环,返回空数组
    if(ret.size() != numCourses) return {};
    return ret;
}

LCR114.火星词典

思路

  • 题目分析 :题目要求根据给出的单词序列 求出相应的字典序,我们对示例1 进行分析:
  • 由上图思路,我们便可以知道这道题可以用拓扑排序解决。
  • 解法 :拓扑排序 + bfs
  • 细节问题
    1. 建图:在之前的题中,由于我们建图得到的元素都是由题干直接给出,且为数字,不会重复。我们有哈希嵌套数组数组嵌套数组 两种方式。
      对于本题由于单词序列中不同单词的比较可能会有相同的结果,为避免数据重复、我们可以使用哈希嵌套哈希 的方式建图。hash<type, hash<type>>
    2. 入度统计:本题中的元素是小写字母且并不一定都存在, 如果用数组统计入度,对于不存在的字母入度一样为0,虽然可以设定初始值,但我们这里用哈希表直接解决。hash<char, int>
    3. 比较单词的方法:利用一个指针,分别比较两个单词的字母即可。
    4. 特殊情况:当存在"abc" "ab" 的比较时,此时序列是不合法的,需要作特殊情况写出。

代码

cpp 复制代码
class Solution {
public:
    // 定义全局变量,在写功能函数时不用多余传参
    unordered_map<char, unordered_set<char>> AList; // 邻接表建图
    unordered_map<char, int> inD; // 统计入度
    bool Illegal; // 用于标记比较是否合法
    string alienOrder(vector<string>& words) {
        // 初始化入度哈希表 + 建图
        for(string &s : words)
            for(char ch : s)
                inD[ch] = 0; // 初始化入度哈希表

        for(int i = 0; i < words.size(); ++i)
            for(int j = i + 1; j < words.size(); ++j)
            {
                compare(words[i], words[j]);
                if(Illegal) return "";
            }

        // 拓扑排序
        queue<char> q;
        for(auto [a, b] : inD) // 将入度为0的字母加入到队列
            if(b == 0) q.push(a);

        string ret = "";
        while(q.size())
        {
            char t = q.front(); q.pop();
            ret += t;
            for(char ch : AList[t])
            {
                if(--inD[ch] == 0) q.push(ch);
            }
        }

        // 判断结果
        for(auto [a, b] : inD)
            if(b != 0) return "";

        return ret;
    }

    void compare(string &s1, string &s2)
    {
        int minLen = min(s1.size(), s2.size());
        int i = 0; // 循环外也需要i,定义到循环外
        for(; i < minLen; ++i)
        {
            if(s1[i] != s2[i])
            {
                char a = s1[i], b = s2[i];
                // 如果邻接哈希表没有a / 邻接表没有a->b
                if(!AList.count(a) || !AList[a].count(b)){
                    AList[a].insert(b); // 加上关系 a->b
                    inD[b]++;
                }
                break;
            }
        }
        // 特殊情况:abc ab
        if(i == minLen && s1.size() > s2.size()) Illegal = true;
    }
};
相关推荐
网易独家音乐人Mike Zhou2 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
‘’林花谢了春红‘’3 小时前
C++ list (链表)容器
c++·链表·list
机器视觉知识推荐、就业指导4 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
Swift社区5 小时前
LeetCode - #139 单词拆分
算法·leetcode·职场和发展
Kent_J_Truman6 小时前
greater<>() 、less<>()及运算符 < 重载在排序和堆中的使用
算法
IT 青年6 小时前
数据结构 (1)基本概念和术语
数据结构·算法
Yang.996 小时前
基于Windows系统用C++做一个点名工具
c++·windows·sql·visual studio code·sqlite3
熬夜学编程的小王6 小时前
【初阶数据结构篇】双向链表的实现(赋源码)
数据结构·c++·链表·双向链表
zz40_6 小时前
C++自己写类 和 运算符重载函数
c++