算法学习(24)—— BFS解决拓扑排序

news2025/1/13 4:47:03

关于拓扑排序

①有向无环图(DAG图)

  •  就跟它的名字一样,有方向但是没有环的图,如下图:
  • 我们了解下入度和出度,二者都是针对一个点来说的,就以上图为例
  • 入度:表示有多少条边指向一个点,比如上面,1的入度就是0,4的入度就是2
  • 出度:表示有多少条边从这个点出去,比如1的出度就是2,2的出度就是1

②AOV图:顶点活动图

  •  就是在有向无环图的基础上,用顶点来表示一个活动,用边来表示活动的先后顺序的图结构,如下图:

③拓扑排序

  •  既然有排序二字,那么这个算法的目的很明确了,就是根据某个依据来排序,结合上面的AOV图,这个排序的依据就是做事的先后顺序,所以拓扑排序的目就是:在AOV网中找到做事情的先后顺序
  • 以上面的做饭流程图为例,有些步骤只有前置步骤完成后才能执行,比如炒菜;有些活动直接可以执行,比如准备厨具和买菜,所以拓扑排序的结果可能不是唯一的
  • 如果我买完菜了,那么买菜到洗菜的这个箭头就可以去掉,就相当于洗菜,也是一个可以直接执行的步骤了,当准备厨具和洗菜执行完后,就可以腌肉了,然后一直按相同的步骤执行,最后找到的执行结果就是“买菜-->准备厨具-->洗菜-->腌肉-->切菜-->炒菜-->装盘-->干饭”,拓扑排序的目的就是找到这种顺序

④实现拓扑排序(伪代码)

  •  我们先把买菜和准备厨具这些入度为0的点拿出来,删除箭头;然后再把入度为0的拿出来删除箭头,然后重复这几步,直到没有点为止,步骤为:
  • ①找到途中入度为0的点,然后输出    ②删除于该点连接的边    ③重复上面两步,直到图中没有点或者没有入度为0的点为止(加上这个是防止有环结构出现死循环)
  • 所以拓扑排序的一个重要应用就是判断图中是否有环,就是直接搞一次拓扑排序,如果过程中发现没有入度为0的点了,但是图中还有剩余点,可以判断图中一定有环形结构

 实现拓扑排序, 只要来一次BFS即可:

  • 首先初始化,就是把所有入度为0的点加入到队列中
  • 然后就是当队列不为空时,一直循环:拿出队头元素,加入到最终结果中,然后删除与该元素相连的边,删完之后判断与删除相连边的点的入度是否变成0
  • 如果入度为0,加入到队列中,继续循环;

还有一个步骤就是“建图”,这个我们在下面的第一道题会详细讲解 

部分OJ题详解

207. 课程表

207. 课程表 - 力扣(LeetCode)

题目先给我们一个数,我们用这个数可以构建一个该大小的有序数组,表示我们总共要学习的科目,然后又给我们一个二维数组,其中二维数组里面也是一个一个的大小为2的数组假设为[1, 0],如果我们要学习课程1,那么必须先学习课程0,然后要我们判断能否学习完所有课程,下面来分析下这道题:

  • 题目考察的就是:“能否拓扑排序?”,也就是“是否有有向无环图” --> 有向图中是否有环
  • 只要执行一次拓扑排序,能把所有点都给搞出来,那么就返回true;如果最后没有入度为0的点但是有向图中还有剩余的点,返回false

算法原理就是上面的,这道题我们最重要的就是要知道“如何建图”?

建图最重要的,就是灵活使用语言提供的容器:先看图的稠密程序也就是数据量,然后就是根据实际使用邻接矩阵或邻接表,这里我们只介绍下邻接表:

邻接表的概念其实很简单,重要的就是我们如何用代码实现邻接表

  • 我们没有必要真的搞一个链表出来,只要搞一个vector<vector<int>>,或者也可以搞一个哈希表unordered_map<int, int<vector>>,用数组嵌套的话比较局限,只限于数字;第二种方式就很可以用其它的类型代替int

除了建图,我们还有个小细节需要注意解决下:

  • 我们需要统计每个顶点的入度是多少,这个很简单,搞一个数组即可vector<int>,下标表示对应的点,里面的值表示入度即可
class Solution 
{
    unordered_map<int, vector<int>> edges; //邻接表图
    vector<int> in; //保存每个点的入度       
    queue<int> q;
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) 
    {
        vector<int> in(numCourses); //保存每个点的入度    

        //1,根据题目信息建图
        for(auto& e : prerequisites)
        {
            int a = e[0], b = e[1]; //b --> a 的一条边
            edges[b].push_back(a);
            in[a]++; //根据下标添加入度
        }

        //2,拓扑排序
        //把入度为0的点扔队列里去
        for(int i = 0; i < numCourses; i++)
            if(in[i] == 0) q.push(i); //此时in[i]是入度,i才是这个点,所以是把i扔队列里去
        
        while(q.size())
        {
            int t = q.front();
            q.pop();
            //我们要把入度为0的点的箭头去掉,其实只要修改下所指向点的入度即可
            for(auto e : edges[t]) 
            {
                in[e]--; //减少对应的点的入度
                if(in[e] == 0) q.push(e);
            }
        }

        //3,判断是否有环
        //只需要判断入度数组还有没有入度不为0的点
        for(int i = 0; i < numCourses; i++) 
            if(in[i] != 0) return false;
        return true;
    }
};

210. 课程表 Ⅱ

210. 课程表 II - 力扣(LeetCode)

这道题其实就是上面那道题稍微变了一下,上面是要我们判断能不能学完,这道题是要我们判断能否学完的同时,要我们返回学习完的顺序即可

  • 解题思路是一样的,代码只要在拓扑排序时记录下结果最后修改一下返回即可,如下代码:
class Solution 
{
    unordered_map<int, vector<int>> edges; //邻接表图
    vector<int> in; //保存每个点的入度       
    queue<int> q;
    vector<int> ret; 
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) 
    {
        vector<int> in(numCourses); //保存每个点的入度

        //1,根据题目信息建图
        for(auto& e : prerequisites)
        {
            int a = e[0], b = e[1]; //b --> a 的一条边
            edges[b].push_back(a);
            in[a]++; //根据下标添加入度
        }

        //2,拓扑排序
        //把入度为0的点扔队列里去
        for(int i = 0; i < numCourses; i++)
            if(in[i] == 0) q.push(i); //此时in[i]是入度,i才是这个点,所以是把i扔队列里去
        
        while(q.size())
        {
            int t = q.front();
            q.pop();
            ret.push_back(t);
            //我们要把入度为0的点的箭头去掉,其实只要修改下所指向点的入度即可
            for(auto e : edges[t]) 
            {
                in[e]--; //减少对应的点的入度
                if(in[e] == 0) q.push(e);
            }
        }

        if(ret.size() == numCourses) return ret;
        else return {};
    }
};

LCR 144. 火星词典

LCR 114. 火星词典 - 力扣(LeetCode)

 这道题难的不是算法思想,算法思想很简单,难的就是题目的意思,光看题目标题就知道了这道题肯定不简单,还是个困难题,下面就分步骤讲解一下题干:

  • 现有一个新的字典序,比如英语的字典序是a到z,但是这个新的字典序是b-j,也是26个字母但是顺序不同
  • 题目给我们一个字符串数组,如示例1,这个数组作为一门语言的词典,并且里面的字符串已经按这门新语言的字母顺序进行了排序
  • 题目要求我们根据字符串数组,还原出已知的字母顺序,并按递增排序并返回,如果有非法字母顺序返回空串,如果有多种可能的顺序,返回任意一个即可

 在讲解这道题之前,我们先来复习下如何比较字典序

  • 假设以我们英语的字典序也就是abc为例,给我们一个abf,我们对两个字符串都搞一个指针,从最左边移动,当遇到第一个不相同的字母时,由于c比f小,那么不论后面的字符是什么,都可以认为abc是比abf小的

 然后我们通过示例1来还原下“火星语言”的字典序是咋样的:

 

下面来讲下算法原理:

  • 我们最开始可以 通过两层循环遍历字符串数组并用双指针两两比较,就能找出所有的所需要的零碎的顺序,就和上面示例1一样
  • 然后就可以尝试把这些零碎的顺序全部拼成一个有向图,以示例1为例,画出来的有向图就是:
  • 最开始w是入度为0的,把w拿出来放队列里,然后后面的步骤就不赘述了,就是搞一次拓扑排序,最后的结果就是我们需要的字典序
  • 如何建图?这一道题我们不能用数组嵌套来搞因为数组嵌套只能应付数字,对于字符我们可以用哈希表来搞hash<char, char[]>,后面的数组就是前面这个字符后面连接的点
  • 但是我们最前面找零碎顺序时是会有重复的,所以不能无脑地把所有顺序都搞char[]里面去,所以我们再哈希表里面再搞一个哈希表,这就是泛型编程地好处:hash<char, hash<char>>
  • 再然后就是和上一题一样搞一个数组统计入度信息,但是不推荐,因为这道题里面不是所有的字符都会出现,再搞一个数组的话会有误差;这个简单,再搞一个哈希表即可hash<char, int>
  • 但是要先遍历一下字典序,每找到一个字符后把这个字符添加到哈希表里并把入度初始化为0,不然后面对图进行拓扑排序时,想加入度为0的点时无法加入,因为哈希里面啥也没有,存东西时只会对入度++,但是不初始化的话根本就没有这个值,所以无法加入
  • 最后收集信息,还是和上一题一样,用双指针来遍历两个字符串即可,识别到第一个不相等的字符串时,根据先后顺序扔哈希表里,然后更新下入度即可

细节处理:

  • 如果题目给我们的是{'abc', 'ab'},这个其实是不合法,因为根据计算机的底层逻辑,像这种情况,是哪个字符串长哪个就要在后面的,所以正确的顺序应该是{'ab', 'abc'},也就是说题目一开始给我们的字符串数组就是错的,最后应该返回空串
  • 但是我们上面的拓扑排序没办法解决这个细节,需要特殊处理一下
  • 我们在信息处理时可以处理一下,就是双指针同时移动时,如果后面的指针指向空了,前面的还没有,以abc和ab为例,ptr1指向c,ptr2指向空了,但是由于ptr1在ptr2前面,所以直接返回空即可
class Solution 
{
    unordered_map<char, unordered_set<char>> edges; //邻接表来表示图
    unordered_map<char, int> in; //统计入度
    bool check; //处理abc和ab的极端情况
public:
    string alienOrder(vector<string>& words) 
    {
        //1,初始化入度哈希表
        for(auto& e : words)
            for(auto ch : e) in[ch] = 0;

        //2,建图
        int n = words.size();
        for(int i = 0; i < n; i++)
            for(int j = i + 1; j < n; j++)
            {
                add(words[i], words[j]); //add的作用就是把这个对应关系加到 edges 里去,并检测是否出现极端情况
                if(check) return "";
            }
        
        //3,拓扑排序
        queue<int> q;
        string ret;
        for(auto& [a, b] : in)
            if(b == 0) q.push(a);
        while(q.size())
        {
            char t = q.front();
            q.pop();
            ret += t;
            for(char e : edges[t])
            {
                in[e]--;
                if(in[e] == 0) q.push(e);
            }
        }

        //4,判断是否有环
        for(auto& [a, b] : in)
            if(b != 0) return "";
        return ret;
    }

    void 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]; //找到一个a --> b 的信息
                if(!edges.count(a) || !edges[a].count(b)) //如果a是第一次来是可以存的,或者a已经存过,但是a --> b 的这个信息没有存,也可以存
                    {
                       edges[a].insert(b);
                        in[b]++;
                    } 
                break;
            }
        }
        if(i == s2.size() && i < s1.size()) check = true;
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2275806.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【深度学习入门_基础篇】概率论

开坑本部分主要为基础知识复习&#xff0c;新开坑中&#xff0c;学习记录自用。 学习目标&#xff1a; 随机事件与概率、随机变量及其分布、多维随机变量及其分布、大数定律与中心极限定理。 强烈推荐此视频&#xff1a; 概率论_麻省理工公开课 废话不多说&#xff0c;直接…

Gitlab-Runner配置

原理 Gitlab-Runner是一个非常强大的CI/CD工具。它可以帮助我们自动化执行各种任务&#xff0c;如构建、测试和部署等。Gitlab-Runner和Gitlab通过API通信&#xff0c;接收作业并提交到执行队列&#xff0c;Gitlab-Runner从队列中获取作业&#xff0c;并允许在不同环境下进行作…

多并发发短信处理(头条项目-07)

1 pipeline操作 Redis数据库 Redis 的 C/S 架构&#xff1a; 基于客户端-服务端模型以及请求/响应协议的 TCP服务。客户端向服务端发送⼀个查询请求&#xff0c;并监听Socket返回。通常是以 阻塞模式&#xff0c;等待服务端响应。服务端处理命令&#xff0c;并将结果返回给客…

OSPF - 1类LSA(Router-LSA)

前篇博客有对常用LSA的总结 1类LSA是OSPF计算最原始的材料&#xff0c;他会泛洪发给所有的路由器 LSA是包含在LSU中的&#xff0c;一条LSU能够携带多条LSA options位所有LSA都会有&#xff0c;用于标记起源于什么类型的区域&#xff0c;具体查看文章【邻居建立】 flags位是一…

pdf提取文本,表格以及转图片:spire.pdf

文章目录 &#x1f412;个人主页&#xff1a;信计2102罗铠威&#x1f3c5;JavaEE系列专栏&#x1f4d6;前言&#xff1a;&#x1f380; 1. pdfbox1.1导入pdfbox 的maven依赖1.1 提取文本1.2 提取文本表格&#xff08;可自行加入逻辑处理&#xff09;1.3 pdf转换成图片代码&…

_STM32关于CPU超频的参考_HAL

MCU: STM32F407VET6 官方最高稳定频率&#xff1a;168MHz 工具&#xff1a;STM32CubeMX 本篇仅仅只是提供超频&#xff08;默认指的是主频&#xff09;的简单方法&#xff0c;并未涉及STM32超频极限等问题。原理很简单&#xff0c;通过设置锁相环的倍频系数达到不同的频率&am…

图片和短信验证码(头条项目-06)

1 图形验证码接口设计 将后端⽣成的图⽚验证码存储在redis数据库2号库。 结构&#xff1a; {img_uuid:0594} 1.1 创建验证码⼦应⽤ $ cd apps $ python ../../manage.py startapp verifications # 注册新应⽤ INSTALLED_APPS [django.contrib.admin,django.contrib.auth,…

解决idea中无法拖动tab标签页的问题

1、按 Ctrl Alt S 打开设置&#xff0c;找到路径 File | Settings | Appearance & Behavior | Appearance 2、去掉勾选 Drag-and-drop with Alt pressed only 即可

单片机(MCU)-简单认识

简介&#xff1a; 内部集成了CPU&#xff0c;RAM&#xff0c;ROM&#xff0c;定时器&#xff0c;中断系统&#xff0c;通讯接口等一系列电脑的常用硬件功能。 单片机的任务是信息采集&#xff08;依靠传感器&#xff09;&#xff0c;处理&#xff08;依靠CPU&#xff09;&…

QT c++ 样式 设置 按钮(QPushButton)的渐变色美化

上一篇文章中描述了标签的渐变色美化,本文描述按钮的渐变色美化。 1.头文件 #ifndef WIDGET_H #define WIDGET_H #include <QWidget> //#include "CustomButton.h"#include <QVBoxLayout> #include <QLinearGradient> #include <QPushButton&…

【物流管理系统 - IDEAJavaSwingMySQL】基于Java实现的物流管理系统导入IDEA教程

有问题请留言或私信 步骤 下载项目源码&#xff1a;项目源码 解压项目源码到本地 打开IDEA 左上角&#xff1a;文件 → 新建 → 来自现有源代码的项目 找到解压在本地的项目源代码文件&#xff0c;点击确定&#xff0c;根据图示步骤继续导入项目 查看项目目录&#xff…

【数据结构-堆】【二分】力扣3296. 移山所需的最少秒数

给你一个整数 mountainHeight 表示山的高度。 同时给你一个整数数组 workerTimes&#xff0c;表示工人们的工作时间&#xff08;单位&#xff1a;秒&#xff09;。 工人们需要 同时 进行工作以 降低 山的高度。对于工人 i : 山的高度降低 x&#xff0c;需要花费 workerTimes…

如何用SQL语句来查询表或索引的行存/列存存储方式|OceanBase 用户问题集锦

一、问题背景 自OceanBase 4.3.0版本起&#xff0c;支持了列存引擎&#xff0c;允许表和索引以行存、纯列存或行列冗余的形式创建&#xff0c;且这些存储方式可以自由组合。除了使用 show create table命令来查看表和索引的存储类型外&#xff0c;也有用户询问如何通过SQL语句…

CDA数据分析师一级经典错题知识点总结(3)

1、SEMMA 的基本思想是从样本数据开始&#xff0c;通过统计分析与可视化技术&#xff0c;发现并转换最有价值的预测变量&#xff0c;根据变量进行构建模型&#xff0c;并检验模型的可用性和准确性。【强调探索性】 2、CRISP-DM模型Cross Industry Standard Process of Data Mi…

算法题(32):三数之和

审题&#xff1a; 需要我们找到满足以下三个条件的所有三元组&#xff0c;并存在二维数组中返回 1.三个元素相加为0 2.三个元素的下标不可相同 3.三元组的元素不可相同 思路&#xff1a; 混乱的数据不利于进行操作&#xff0c;所以我们先进行排序 我们可以采取枚举的方法进行解…

【设计模式】介绍常见的设计模式

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 文章目录 ✨ 介绍一下常见的设计模式✨ Spring 中常见的设计模式 这期内容主要是总结一下常见的设计模式&#xff0c;可…

单通道串口服务器(三格电子)

一、产品介绍 1.1 功能简介 SG-TCP232-110 是一款用来进行串口数据和网口数据转换的设备。解决普通 串口设备在 Internet 上的联网问题。 设备的串口部分提供一个 232 接口和一个 485 接口&#xff0c;两个接口内部连接&#xff0c;同 时只能使用一个口工作。 设 备 的网 口…

【蓝牙】win11 笔记本电脑连接 hc-06

文章目录 前言步骤 前言 使用电脑通过蓝牙添加串口 步骤 设置 -> 蓝牙和其他设备 点击 显示更多设备 更多蓝牙设置 COM 端口 -> 添加 有可能出现卡顿&#xff0c;等待一会 传出 -> 浏览 点击添加 hc-06&#xff0c;如果没有则点击 再次搜索 确定 添加成…

信息安全、网络安全和数据安全的区别和联系

信息安全、网络安全和数据安全是信息安全领域的三大支柱&#xff0c;它们之间既存在区别又相互联系。以下是对这三者的详细比较&#xff1a; 一.区别 1.信息安全 定义 信息安全是指为数据处理系统建立和采用的技术和管理的安全保护&#xff0c;保护计算机硬件、软件和数据不…

oracle闪回表

文章目录 闪回表案例1&#xff1a;&#xff08;未清理回收站时的闪回表--成功&#xff09;案例2&#xff08;清理回收站时的闪回表--失败&#xff09;案例3&#xff1a;彻底删除表&#xff08;不经过回收站--失败&#xff09;案例4&#xff1a;闪回表之后重新命名新表总结1、删…