【搜索回溯算法篇】:拓宽算法视野--BFS如何解决拓扑排序问题

news2025/2/1 16:54:20

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:搜索回溯算法篇–CSDN博客

在这里插入图片描述

文章目录

  • 一.广度优先搜索(BFS)解决拓扑排序
    • 1.拓扑排序简介
    • 2.解决拓扑排序的原理
  • 二.例题
    • 1.课程表
    • 2.课程表||
    • 3.火星词典

一.广度优先搜索(BFS)解决拓扑排序

1.拓扑排序简介

拓扑排序是对有向无环图(DAG, Directed Acyclic Graph)的顶点的一种排序,使得如果存在一条从顶点 u u u 到顶点 v v v 的有向边 ( u , v ) (u, v) (u,v),那么在排序中 u u u 出现在 v v v 之前。它在许多实际应用场景中有重要作用,例如任务调度(每个任务有前置任务要求)、课程安排(先修课程的顺序安排)等。

2.解决拓扑排序的原理

  1. 入度的概念

    • 对于有向图中的每个顶点,我们定义其入度为指向该顶点的边的数量。例如,在有向图中,如果顶点 v v v 有三条边指向它,那么顶点 v v v 的入度为3。
  2. BFS - 基于入度的操作

    • 首先,计算图中每个顶点的入度。
    • 将所有入度为0的顶点放入队列中。这些顶点是可以作为拓扑排序的起始点,因为没有边指向它们,也就没有前置的依赖关系。
    • 然后开始进行BFS:
      • 从队列中取出一个顶点 u u u,将其加入到拓扑排序的结果序列中。
      • 对于顶点 u u u 的所有邻接顶点 v v v,将它们的入度减1(因为 u u u v v v 的边被“处理”了,相当于减少了 v v v 的一个入度来源)。
      • 如果某个邻接顶点 v v v 的入度变为0,就将其加入队列。
    • 重复上述步骤,直到队列为空。
  3. 正确性证明

    • 由于我们首先选择入度为0的顶点加入队列,这些顶点没有前置依赖,可以首先出现在拓扑排序结果中。
    • 当我们处理一个顶点并减少其邻接顶点的入度时,实际上是在逐步消除依赖关系。当一个顶点的入度变为0时,说明它之前的所有依赖都已经被处理过了,所以可以将其加入队列并放入拓扑排序结果中。
    • 因为图是有向无环图,所以这个过程最终会处理完所有顶点,得到正确的拓扑排序结果。

二.例题

1.课程表

题目

在这里插入图片描述

算法原理

首先明白题意要求,给定一个二维数组,每一行中存储的是一对数字,比如示例一(1,0),表示学习课程1之前需要先学习课程0,用有向图表示就是0->1;要求判断给定的所有数字对是否能完成所有课程的学习;

本道题的重点就是如何使用拓扑排序来判断,但是在拓扑排序之前必须是有向图才可以拓扑排序,所以需要先根据给定的数组来建立有向图;建图通常借助两个哈希表或者数组来实现,一个用来表示指向关系,比如a->b;一个用来表示每个节点的入度个数;

在本道题中,因为课程是用数字来表示,正好对应下标,所以可以用数组来实现,这里我写的是用哈希表来表示指向关系,用数组来表示入度个数,具体可以看代码中的注释。

代码实现

bool canFinish(int numCourses, vector<vector<int>>& prerequisites){
    // 用哈希表来表示邻接表
    // key值表示一个节点,val表示一个数组,里面存放的是key值节点指向的下一个节点
    // key=0;val=[1,2,3];表示0指向1,2,3三个节点
    unordered_map<int, vector<int>> edges;
    //用数组来存放每个节点的入度,本道题中下标正好对应节点
    vector<int> in(numCourses);

    //建图
    for(auto& nums : prerequisites){
        //[a,b]表示b->a,在完成a之前先完成b
        int a = nums[0], b = nums[1];

        //b->a,存放b的下一个节点
        edges[b].push_back(a);
        //节点a入度+1
        in[a]++;
    }

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

    //拓扑排序
    while(!q.empty()){
        //获取队头节点并出队
        int t = q.front();
        q.pop();

        //遍历当前下标对应的所有下一个节点,将对应的节点入度-1,表示删除指向的边
        for(auto num : edges[t]){
            in[num]--;
            //如果对应节点的入度为0,入队
            if(in[num]==0){
                q.push(num);
            }
        }
    }

    //遍历入度数组,如果出现某个节点的入度不为0说明存在环,不能遍历所有节点
    for (auto num : in){
        if(num==1){
            return false;
        }
    }

    return true;
}

2.课程表||

题目

在这里插入图片描述

算法原理

本道题和上面一道题可以说是一模一样,只不过是如果可以完成所有的课程,就输出课程顺序,所以在上一道题的基础上在bfs实现拓扑排序时,只需要将每个出队的头节点存放到数组中即可,因为出队顺序就是拓扑排序的顺序。

代码实现

vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites){
    //本道题是上一道的变形,具体过程一模一样

    //用哈希表表示邻接表,数组存放每个节点的入度
    unordered_map<int, vector<int>> edges;
    vector<int> in(numCourses);

    //建表
    for(auto& e : prerequisites){
        //b->a
        int a = e[0], b = e[1];
        edges[b].push_back(a);
        in[a]++;
    }

    //将所有入度为0的入队
    queue<int> q;
    for (int i = 0; i < in.size(); i++){
        if(in[i]==0){
            q.push(i);
        }
    }

    //建立一个数组用来返回最终的结果
    vector<int> ret;

    //拓扑排序,bfs实现
    while(!q.empty()){
        //获取队头节点并出队
        int t=q.front();
        q.pop();

        ret.push_back(t);

        //通过当前节点遍历所有指向的节点
        for(auto num : edges[t]){
            //修改指向的节点的入度
            in[num]--;
            //如果入度为0,入队
            if(in[num]==0){
                q.push(num);
            }
        }
    }

    //遍历入度数组,如果出现某个节点的入度不为0,说明存在环,返回空数组
    for(auto num : in){
        if(num!=0){
            ret.clear();
            return ret;
        }
    }

    return ret;
}

3.火星词典

题目

在这里插入图片描述

算法原理

本道题其实也是根据拓扑排序的原理来解决,题意要求根据词典(words数组)来找到每个字母的先后顺序,然后返回正确的字母顺序;比如,示例一中的"wrt"和"wrf"因为前两个字母是一样的,而在第三个字母出现不同,但又因为"wrt"出现在"wrf"前面,所以字母"t"的顺序在字母"f"的前面,对应题意中的第一种情况;还有就是"abc"和"abcde",因为第一个字符串的长度小于第二个字符串,但是前三个字符又正好相同,没有找到不相同的字符,如果是这种情况,输出的字母顺序那就是字母"de"的顺序在前,然后"abc"三个字母的顺序随便,因为无法判断出这三个字母的顺序;但是可能会出现这种情况第一个字符串是"abcde"而第二个字符串是"abc",这种情况就是违反规则,直接返回空串即可。

所以实现过程还是先根据给定的信息建立有向图,然后拓扑排序,获取信息其实就是将给定的词典数组,两两字符串进行比较获取每个字母的顺序关系,根据上面的两种情况来获取信息。具体的过程注释可以看代码中写的。

代码实现

string alienOrder(vector<string>& words){
    // 建立一个边哈希表,key值表示字符,val值表示哈希表用来存放该字符指向的所有字符
    // 因为查找该字符的指向字符时,可能会重复出现,所以内层哈希表用set型去重
    // 例:key=t;val=[d,c,a];表示t->d&&t->c&&t->a;
    unordered_map<char, unordered_set<char>> edges;
    //建立一个入度哈希表,用来存放每个字符的入度
    //key值表示字符,val值表示该字符对应的入度
    unordered_map<char, int> in;

    //先遍历整个词典,将所有出现的字符存放到入度哈希表中并将入度初始化为0,防止后面某些字符没有遍历到
    for(auto s : words){
        for(auto ch : s){
            if(in.count(ch)==0){
                in[ch] = 0;
            }
        }
    }

    //两层for循环遍历词典,建AOV图
    for (int i = 0; i < words.size(); i++){
        string s1 = words[i];
        for (int j = i + 1; j < words.size(); j++){
            string s2 = words[j];

            //两个指针,一个指向第一个字符串中的起始位置,一个指向第二个字符串的起始位置
            int cur1 = 0, cur2 = 0;
            while(cur1<s1.size()&&cur2<s2.size()){
                //如果两个指针指向对应字符串中的字符不相同,表示s1[cur1]->s2[cur2]
                //s1的字符指向s2的字符,s2的字符入度+1
                if(s1[cur1]!=s2[cur2]){
                    //这里有一个细节,如果b->a重复出现,a字符的入度就会重复+1
                    //所以要进行一个判断,如果b字符的哈希表中已经存在字符a,直接跳过
                    if(edges[s1[cur1]].count(s2[cur2])==0){
                        edges[s1[cur1]].insert(s2[cur2]);
                        in[s2[cur2]]++;
                    }
                    //找到第一对不相同的字符就结束
                    break;
                }
                else{
                    cur1++;
                    cur2++;
                }
            }
            // 如果两个字符串没有找到相同的字符,有两种情况
            // 1.s1=ab;s2=abc;不用处理
            // 2.s1=abc;s2=ab;直接返回空串,因为违反规则
            if(cur2==s2.size()&&cur1<s1.size()){
                return "";
            }
        }
    }

    //设置一个结果字符串用来存放字符的顺序
    string ret;
    //设置一个队列
    queue<char> q;
    //遍历入度哈希表将所有入度为0的字符入队
    for(auto& [ch,count] : in){
        if(count==0){
            q.push(ch);
        }
    }

    //bfs实现拓扑排序
    while(!q.empty()){
        //获取队头字符并出队
        char ch = q.front();
        q.pop();

        //结果字符串加上队头字符
        ret += ch;

        //找到该字符指向的所有字符,将指向的字符入度-1,表示删除指向的边
        for(auto& nextch : edges[ch]){
            in[nextch]--;
            //如果指向的字符入度减为0,将该字符入队
            if(in[nextch]==0){
                q.push(nextch);
            }
        }
    }

    //遍历入度哈希表,如果出现某个字符的入度不为0,说明顺序错误,不能将所有字符拓扑排序
    for(auto& [ch,count] : in){
        if(count!=0){
            //直接返回空串
            return "";
        }
    }
    
    return ret;
}

以上就是关于bfs解决拓扑排序的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
在这里插入图片描述

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

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

相关文章

WPS怎么使用latex公式?

1、下载并安装mathtype https://blog.csdn.net/weixin_43135178/article/details/125143654?sharetypeblogdetail&sharerId125143654&sharereferPC&sharesourceweixin_43135178&spm1011.2480.3001.8118 2、将mathtype嵌入在WPS MathType面板嵌入器,免费工具…

简单的爱心跳动表白网页(附源码)

一&#xff1a;准备工作 在开始之前&#xff0c;确保已经具备基础的 HTML、CSS 和 JavaScript 知识。同时&#xff0c;也要准备好一个代码编辑器&#xff0c;比如 VS Code 或 Sublime Text。接下来&#xff0c;我们需要创建三个文件&#xff1a;index.html、styles.css 和 scr…

【AI】DeepSeek 概念/影响/使用/部署

在大年三十那天&#xff0c;不知道你是否留意到&#xff0c;“deepseek”这个词出现在了各大热搜榜单上。这引起了我的关注&#xff0c;出于学习的兴趣&#xff0c;我深入研究了一番&#xff0c;才有了这篇文章的诞生。 概念 那么&#xff0c;什么是DeepSeek&#xff1f;首先百…

【4Day创客实践入门教程】Day3 实战演练——桌面迷你番茄钟

Day3 实战演练——桌面迷你番茄钟 目录 Day3 实战演练——桌面迷你番茄钟1. 选择、准备元件、收集资料2. 硬件搭建3.编写代码 Day0 创想启程——课程与项目预览Day1 工具箱构建——开发环境的构建Day2 探秘微控制器——单片机与MicroPython初步Day3 实战演练——桌面迷你番茄钟…

AndroidCompose Navigation导航精通1-基本页面导航与ViewPager

文章目录 前言基本页面导航库依赖导航核心部件简单NavHost实现ViewPagerPager切换逻辑图阐述Pager导航实战前言 在当今的移动应用开发中,导航是用户与应用交互的核心环节。随着 Android Compose 的兴起,它为开发者提供了一种全新的、声明式的方式来构建用户界面,同时也带来…

Node.js——body-parser、防盗链、路由模块化、express-generator应用生成器

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

C语言指针专题四 -- 多级指针

目录 1. 多级指针的核心原理 1. 多级指针的定义 2. 内存结构示意图 3. 多级指针的用途 2. 编程实例 实例1&#xff1a;二级指针操作&#xff08;修改一级指针的值&#xff09; 实例2&#xff1a;动态二维数组&#xff08;二级指针&#xff09; 实例3&#xff1a;三级指…

深度学习的应用

目录 一、机器视觉 1.1 应用场景 1.2 常见的计算机视觉任务 1.2.1 图像分类 1.2.2 目标检测 1.2.3 图像分割 二、自然语言处理 三、推荐系统 3.1 常用的推荐系统算法实现方案 四、图像分类实验补充 4.1 CIFAR-100 数据集实验 实验代码 4.2 CIFAR-10 实验代码 深…

RabbitMQ 多种安装模式

文章目录 前言一、Windows 安装 RabbitMq1、版本关系2、Erlang2.1、下载安装 Erlang 23.12.2、配置 Erlang 环境变量 3、RabbitMQ3.1、下载安装 RabbitMQ 3.8.93.2、环境变量3.3、启动RabbitMQ 管理插件3.3、RabbitMQ3.4、注意事项 二、安装docker1、更新系统包&#xff1a;2、…

吴恩达深度学习——有效运作神经网络

内容来自https://www.bilibili.com/video/BV1FT4y1E74V&#xff0c;仅为本人学习所用。 文章目录 训练集、验证集、测试集偏差、方差正则化正则化参数为什么正则化可以减少过拟合Dropout正则化Inverted Dropout其他的正则化方法数据增广Early stopping 归一化梯度消失与梯度爆…

DDD - 微服务架构模型_领域驱动设计(DDD)分层架构 vs 整洁架构(洋葱架构) vs 六边形架构(端口-适配器架构)

文章目录 引言1. 概述2. 领域驱动设计&#xff08;DDD&#xff09;分层架构模型2.1 DDD的核心概念2.2 DDD架构分层解析 3. 整洁架构&#xff1a;洋葱架构与依赖倒置3.1 整洁架构的核心思想3.2 整洁架构的层次结构 4. 六边形架构&#xff1a;解耦核心业务与外部系统4.1 六边形架…

数据结构与算法之二叉树: LeetCode LCP 10. 二叉树任务调度 (Ts版)

二叉树任务调度 https://leetcode.cn/problems/er-cha-shu-ren-wu-diao-du/description/ 描述 任务调度优化是计算机性能优化的关键任务之一。在任务众多时&#xff0c;不同的调度策略可能会得到不同的总体执行时间&#xff0c;因此寻求一个最优的调度方案是非常有必要的 通…

玩转大语言模型——配置图数据库Neo4j(含apoc插件)并导入GraphRAG生成的知识图谱

系列文章目录 玩转大语言模型——使用langchain和Ollama本地部署大语言模型 玩转大语言模型——ollama导入huggingface下载的模型 玩转大语言模型——langchain调用ollama视觉多模态语言模型 玩转大语言模型——使用GraphRAGOllama构建知识图谱 玩转大语言模型——完美解决Gra…

计算机毕业设计Python+CNN卷积神经网络考研院校推荐系统 考研分数线预测 考研推荐系统 考研爬虫 考研大数据 Hadoop 大数据毕设 机器学习

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

OpenCV:闭运算

目录 1. 简述 2. 用膨胀和腐蚀实现闭运算 2.1 代码示例 2.2 运行结果 3. 闭运算接口 3.1 参数详解 3.2 代码示例 3.3 运行结果 4. 闭运算的应用场景 5. 注意事项 相关阅读 OpenCV&#xff1a;图像的腐蚀与膨胀-CSDN博客 OpenCV&#xff1a;开运算-CSDN博客 1. 简述…

智云-一个抓取web流量的轻量级蜜罐-k8s快速搭建教程

智云-一个抓取web流量的轻量级蜜罐-k8s快速搭建教程 github地址 https://github.com/xiaoxiaoranxxx/POT-ZHIYUN k8s搭建教程 首先下载代码文件 git clone https://github.com/xiaoxiaoranxxx/POT-ZHIYUN.git cd POT-ZHIYUN编译镜像 代码相关文件在github https://github.com/x…

万物皆有联系:驼鸟和布什

布什&#xff1f;一块布十块钱吗&#xff1f;不是&#xff0c;大家都知道&#xff0c;美国有两个总统&#xff0c;叫老布什和小布什&#xff0c;因为两个布什总统&#xff08;父子俩&#xff09;&#xff0c;大家就这么叫来着&#xff0c;目的是为了好区分。 布什总统的布什&a…

< OS 有关 > 阿里云:轻量应用服务器 的使用 :轻量化 阿里云 vpm 主机

原因&#xff1a; &#xff1c; OS 有关 &#xff1e; 阿里云&#xff1a;轻量应用服务器 的使用 &#xff1a;从新开始 配置 SSH 主机名 DNS Tailscale 更新OS安装包 最主要是 清除阿里云客户端这个性能杀手-CSDN博客 防止 I/O 祸害系统 操作&#xff1a; 查看进程&#x…

SOME/IP--协议英文原文讲解3

前言 SOME/IP协议越来越多的用于汽车电子行业中&#xff0c;关于协议详细完全的中文资料却没有&#xff0c;所以我将结合工作经验并对照英文原版协议做一系列的文章。基本分三大块&#xff1a; 1. SOME/IP协议讲解 2. SOME/IP-SD协议讲解 3. python/C举例调试讲解 Note: Thi…

unity学习24:场景scene相关生成,加载,卸载,加载进度,异步加载场景等

目录 1 场景数量 SceneManager.sceneCount 2 直接代码生成新场景 SceneManager.CreateScene 3 场景的加载 3.1 用代码加载场景&#xff0c;仍然build setting里先加入配置 3.2 卸载场景 SceneManager.UnloadSceneAsync(); 3.3 同步加载场景 SceneManager.LoadScene 3.3.…