【数据结构——有向图】有环无环判定、拓扑排序(DFS、BFS)

news2024/11/26 2:42:23

文章目录

    • 1. 什么是有向图
    • 2. 什么是拓扑排序
    • 2. 有向图的拓扑排序
      • 2. 1 BFS 广度优先
      • 2. 2 DFS 深度优先
    • 3. 有向图有环无环判定

1. 什么是有向图

有向图(Directed Graph),也被称为有向图形或方向图,是一种图的类型。在有向图中,图中的边具有方向,从一个顶点指向另一个顶点。

在有向图中,每个顶点表示一个实体,而有向边则表示实体之间的关系或连接。这种有方向性的边表明了连接的起点和终点之间的单向关系。因此,有向图中的边具有起点和终点的概念,它们不能逆转方向。

与有向图对应的是无向图(Undirected Graph),在无向图中,边是没有方向的,可以双向移动。相比之下,有向图更适合描述具有明确方向性的关系,例如有向的路径、进程之间的依赖关系等。

有向图可以用来解决许多问题,如拓扑排序、最短路径、网络流等。它在计算机科学、图论、网络分析等领域都有广泛的应用。

在这里插入图片描述

2. 什么是拓扑排序

拓扑排序(Topological Sort)是对有向无环图(DAG)进行排序的一种算法。它将有向图的所有顶点排列成线性序列,使得对于任何的有向边 (u, v),顶点 u 都在序列中排在顶点 v 的前面。

拓扑排序的应用场景通常涉及到任务或事件之间的依赖关系,其中每个顶点表示一个任务或事件,有向边表示依赖关系。通过拓扑排序,可以确定这些任务或事件的执行次序,以满足依赖关系的约束。

拓扑排序算法的实现过程如下:

  1. 找到没有前置依赖的顶点,即入度为0的顶点,并将其加入结果序列中。
  2. 图中删除该顶点及其相关的边,即更新其他顶点的入度
  3. 重复步骤1和步骤2,直到图中所有顶点都被添加到结果序列中,或者无法找到入度为0的顶点为止
  4. 如果图中存在环路,则无法进行拓扑排序(或者排序得到的数组集合不等于节点总数),因为环路意味着存在循环依赖。

拓扑排序可以通过深度优先搜索(DFS)或广度优先搜索(BFS)来实现。其中,DFS算法更常用,它可以按照深度优先的顺序遍历图,并在遍历完成后逆序得到拓扑排序结果。

拓扑排序的时间复杂度为O(V+E),其中V和E分别是图中的顶点数和边数。

2. 有向图的拓扑排序

例如有向图:[[1,0],[2,0],[3,1],[4,3],[4,2],[5,4]] 数组后面的元素指向前面的元素
在这里插入图片描述

2. 1 BFS 广度优先

  • 广度优先搜索需要依赖一个节点的入度数组如下:(当然这里节点也作为数组下标,如果节点和下标不能一一对应,可以使用一个map哈希表去记录下节点与入度的映射表)

在这里插入图片描述

  • 然后还需要准备一个节点指向集合,记录节点与节点的指向关系(后续依照这个指向将被指向的节点的入度做减一操作)
    在这里插入图片描述
  • 最后就是准备一个队列,队列中依次加入入度为0 的节点,并且队列的出队顺序即为
  • 再根据节点指向集合,若被指向的节点入度为0,那么此时把入度为0的节点加入到队列循环,直到没有入度为0 的节点

模板代码:

      public int[] findOrder(int numCourses, int[][] prerequisites) {
      
      int[] cou = new int[numCourses];//节点入度数组

      int[] num  = new int[numCourses];//用于存储拓扑排序

      List<List<Integer>> couList = new ArrayList<>();//指向与被指向的集合映射

      Queue<Integer> queue = new LinkedList<>();//辅助队列 用于处理入度为0 的节点

      for(int i = 0 ;i<numCourses ;i++)//给集合中节点与被指向节点初始化集合
          couList.add(new ArrayList<Integer>());
    
      for(int[] pre : prerequisites){
        cou[pre[0]]++;//统计各节点的入度
        couList.get(pre[1]).add(pre[0]);//给集合中父节点设置指向子节点的子集合
      }

      for(int i = 0 ;i<numCourses ;i++){
        if(cou[i] == 0) queue.offer(i);//搜索第一个入度为0 的节点  加入队列
      }
      int i = 0;//用于将拓扑排序加入到一个数组用的下标
      while(!queue.isEmpty()){
            int ids = queue.poll();
            numCourses--;//取出一个元素  就让节点总数-1
            num[i] = ids;//拓扑排序 取出的元素加入到数组
            for(int cur : couList.get(ids)){//  couList.get(ids) 根据节点  取出父节点指向的子节点  让被指向的子节点入度 -1
                if(cou[cur] >= 1 ) cou[cur]--;
                if(cou[cur] == 0 ) queue.offer(cur);//若当前节点入度为0  则加入队列
            }
            i++;
      }
      if(numCourses == 0)  return num;
      // 若 numCourses(节点总数) 不等于0说明 说明最后还有入度不为0的节点,没有被处理,说明有环,则无拓扑排序
      // 或者如果拓扑排序数组num的长度 不等于节点总数 说明拓扑排序不完整,说明无拓扑排序
      else return new int[0];
    }

2. 2 DFS 深度优先

  • 深度优先搜索需要依赖一个节点的辅助数组默认都为0如下:(当然这里节点也作为数组下标,如果节点和下标不能一一对应,可以使用一个map哈希表去记录下节点与辅助值的映射表)
    在这里插入图片描述
    标记值为0:代表搜索起点(dfs入口)
    标记值为1:代表搜索中(如果搜索中碰到值标记值为1的节点,说明有环)
    标记值为2:代表搜索完成(搜索过程无环)

  • 然后还需要准备一个节点指向集合,记录节点与节点的指向关系(后续依照这个指向将被指向的节点的值做标记)
    在这里插入图片描述

  • 最后当标记值为2的时候,就代表此次dfs无环,利用一个栈将标记值为2的节点加入到栈中(栈的加入顺序就是拓扑排序的顺序)

  • 需要一个标志位(初始为true),如果搜索中dfs碰到节点值为1的节点的时候,代表出现了环,则直接将标志位标为false,结束此次递归

  List<List<Integer>> cousList;
    int[] cous;
    boolean valid = true;
public boolean canFinish(int numCourses, int[][] prerequisites) {
    
     cous = new int[numCourses];// 构造标志位 初始化全部位0  长度为节点数

     cousList = new ArrayList<>();//给集合中节点与被指向节点初始化集合

    for(int i = 0 ;i < numCourses ; i++) //指向(父节点)与被指向(子节点)的集合映射
      cousList.add(new ArrayList<>());
 
    for(int[] pre : prerequisites){//给集合中父1节点设置指向子节点的子集合
       cousList.get(pre[1]).add(pre[0]);
    }

    for(int i = 0 ; i<numCourses ; i++){//
      if(cous[i] == 0)  dfs(i);  //等于0未搜索过 进入dfs
    }

    return valid;

  }
     
    public void dfs(int c){
        cous[c] = 1;
        for(int cur : cousList.get(c)){//遍历该父节点的子节点集合
          if(cous[cur]==0){//如果指向的节点未搜索过,则深搜
            dfs(cur);
            if(!valid){
              return;
            }
          }else if(cous[cur]==1){//如果指向节点在搜索中,则有环,标记Vaild
                valid = false;
                return;
          }

        }
    cous[c]=2;//因为节点已经完成深搜,所以标记它的状态搜索完成
  }

// 方法二  dfs 深度优先
    int[] cou = null;// 设置全局变量  方便dfs使用
    int[] num = null;
    List<List<Integer>> couList = null;
    boolean valid = true;
    Deque<Integer> queue = null;
  public int[] findOrder(int numCourses, int[][] prerequisites) {
     this.cou = new int[numCourses];// 构造标志位 初始化全部位0  长度为节点数

     this.queue = new LinkedList<>();//用于配合输出拓扑排序

     this.num  = new int[numCourses];//用于存储拓扑排序

     this.couList = new ArrayList<>();//给集合中节点与被指向节点初始化集合

    for(int i = 0 ;i<numCourses ;i++)//指向(父节点)与被指向(子节点)的集合初始化
        couList.add(new ArrayList<Integer>());
   
    for(int[] pre : prerequisites){
      couList.get(pre[1]).add(pre[0]);//指向(父节点)与被指向(子节点)的集合映射
    }

    for(int i = 0 ; i<numCourses ;i++){
        if(cou[i] == 0)  dfs(i);//等于0未搜索过 进入dfs
    }

    if(queue.size() != numCourses) return new int[0]; //如果dfs完成之后  栈内元素个数不等于节点总数  说明 拓扑排序不完整,存在环,自然不能将全部节点遍历完,
    else{//否则就代表无环  可以得到完整的拓扑排序
      for(int i = 0 ; i<numCourses ; i++){
        num[i] = queue.pop();//将压栈的节点取出来 放到数组里面
      }
    }  
      return num;
  }


  public void dfs(int i){
    cou[i] = 1;
    for(int cur : couList.get(i)){//遍历该父节点的子节点集合
      if(cou[cur] == 0){//节点标记数组对应的值等于 0  继续递归
        dfs(cur);
        if(!valid) return ;  //根据标记为判断是否有环  有环说明不能得到拓扑排序 直接返回 不往下面执行了

      }else if(cou[cur] == 1){//如果搜索中存在环  将标志位设为fasle 
         valid = false;
         return;
      }
    }
    //一次遍历结束无环  就让该遍历元素位置的节点数组数值置为  2  代表以该点进行dfs  无环
    cou[i] = 2;
    queue.push(i); //让该dfs完的节点压栈  为什么要压栈  因为最后的拓扑排序,就是栈的出栈顺序
  }

3. 有向图有环无环判定

具体怎么判定有环和无环:

BFS下

本身这两种判断方式原理是一样的,如果节点都能根据箭头遍历得到,自然得到的拓扑排序数组的长度就是节点总数,否则拓扑排序数组的长度不等于节点总数,说明有节点并没有遍历到,说明存在了环。

  1. 第一种判断方式:如果最后得到的数组(拓扑排序)的长度不等于节点总数,则代表有环,使得bfs并没有按照箭头指向走完整个图,所以出现了环
  2. 第二种判断方式:每次让队列弹出一个入度为0的节点时,让节点总数减1,如果最后节点总数 == 0,说明无环,每个元素都被bfs到了

DFS下

  1. 最后的栈里面存的就是遍历到的节点,如果最后dfs结束后的栈的大小不等于节点总数,说明有节点没有被遍历到,说明出现了环,否则如果栈的大小等于节点数,代表都遍历到了,该图无环

栈的出栈顺序就是拓扑排序的循序

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

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

相关文章

联发科MTK6765处理器参数_4G安卓核心板主板定制方案

MT6765安卓核心板是一款基于MTK平台的高性能智能模块&#xff0c;是一款工业级的产品。该芯片也被称为Helio P35。MT6765核心板是目前市场上最受欢迎的低成本智能芯片之一&#xff0c;其卓越的性能和创新技术为用户提供了更加顺畅和高效的使用体验。 MTK6765&#xff08;曦力 …

h5开发网站-css实现页面的背景固定定位

一、需求&#xff1a; 在页面滚动时&#xff0c;背景图片保持不变&#xff0c;而不是跟随滚动。 二、解决方式&#xff1a; 使用背景固定定位&#xff0c;只需要在CSS中增加一个background-attachment: fixed;属性即可。 具体代码&#xff1a; <div class"item_right…

String.format() 格式化字符串的方法, 不同占位符表示的含义及使用方式

学习目标&#xff1a; 目标如下&#xff1a; String.format() 格式化字符串的方法&#xff0c; 不同占位符表示的含义及使用方式 学习内容&#xff1a; 内容&#xff1a; 占位符类型 String.format()方法是一种格式化字符串的方法 字符串&#xff1a;一个占位符"%s&q…

Spring Cloud Alibaba-Feign整合Sentinel

第1步: 引入sentinel的依赖 <!--sentinel客户端--> <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> 第2步: 在配置文件中开启Feign对Sentinel的…

面试题查漏补缺 i++和 ++ i哪个效率更高

i 和 i 哪个效率更高&#xff1f; 在这里声明&#xff0c;简单地比较前缀自增运算符和后缀自增运算符的效率是片面的&#xff0c;因为存在很多因素影响这个问题的答案。首先考虑内建数据类型的情况:如果自增运算表达式的结果没有被使用&#xff0c;而是仅仅简单地用于增加一员…

2023-09-05 LeetCode每日一题(从两个数字数组里生成最小数字)

2023-09-05每日一题 一、题目编号 2605. 从两个数字数组里生成最小数字二、题目链接 点击跳转到题目位置 三、题目描述 给你两个只包含 1 到 9 之间数字的数组 nums1 和 nums2 &#xff0c;每个数组中的元素 互不相同 &#xff0c;请你返回 最小 的数字&#xff0c;两个数…

Python 遍历字典的若干方法

字典是 Python 的基石。这门语言的很多方面都是围绕着字典构建的 模块、类、对象、globals()和 locals() 都是字典与 Python 实现紧密联系的例子 以下是 Python 官方文档定义字典的方式&#xff1a; An associative array, where arbitrary keys are mapped to values. The k…

机器人中的数值优化(十二)——带约束优化问题简介、LP线性规划

本系列文章主要是我在学习《数值优化》过程中的一些笔记和相关思考&#xff0c;主要的学习资料是深蓝学院的课程《机器人中的数值优化》和高立编著的《数值最优化方法》等&#xff0c;本系列文章篇数较多&#xff0c;不定期更新&#xff0c;上半部分介绍无约束优化&#xff0c;…

移动端App持续集成体系构建实战

这里写目录标题 一、目标1、前言2、优势&#xff1a;3、涉及技术点4、目标 二、测试app构建、打包过程1、安卓打包的环境要求 三、演示安卓源码打包四、演示安卓App部署1、前提条件2、命令控制apk安装与卸载 五、安卓UI自动化测试1、Appium app自动化测试-Python2、实现的验证点…

Vue3+Ts实现父子组件间传值的两种方式

文章目录 写在前面1、v-modelemit传值1.1父向子传递数据1.2子向父传递数据 2、使用v-bindemit2.1父向子传递数据2.2子向父传递数据 总结 写在前面 对于常用的组件间传参最近有了有点小心得总结一下&#xff0c;主要是两种子向父组件传参的方式总结。欢迎评论区讨论 概览&…

Linux的服务器日志分析及性能调优

作为网络安全和数据传输的重要环节&#xff0c;代理服务器在现代互联网中扮演着至关重要的角色。然而&#xff0c;在高负载情况下&#xff0c;代理服务器可能面临性能瓶颈和效率问题。本文将介绍如何利用Linux系统对代理服务器进行日志分析&#xff0c;并提供一些实用技巧来优化…

数组模拟实现环形队列

目录 前言 一、什么是队列? 二、数组模拟非环形队列的实现 三 . 数组模拟环形队列 总结 前言 笔者: 最爱吃兽奶 博文内容: 数据结构队列的模拟实现 这篇博客理解起来或许没有那么简单,我尽力讲的容易理解一点,接下来跟我一起去看看吧! 一、什么是队列? 队列是一种特殊…

#452. 序列操作

序列操作 - 题目 - Daimayuan Online Judge 问题描述&#xff1a; 思路&#xff1a;首先想的是第二次操作的y可以将前面所以操作进行抵消&#xff0c;只需要第二次操作的最大值即可。但是发现&#xff0c;对于第一个操作&#xff0c;它是单点修改&#xff0c;每修改一次对于第…

2023年7月京东投影仪行业品牌销售排行榜(京东大数据)

鲸参谋监测的京东平台7月份投影仪行业销售数据已出炉&#xff01; 7月份&#xff0c;投影仪市场呈现增长趋势。根据鲸参谋平台的数据可知&#xff0c;7月京东平台投影仪的销量将近20万&#xff0c;同比增长约16%&#xff1b;销售额将近3.8亿&#xff0c;同比增长约4%。 ​*数据…

机器人中的数值优化(十一)——高斯牛顿法、LMF方法、Dogleg方法

本系列文章主要是我在学习《数值优化》过程中的一些笔记和相关思考&#xff0c;主要的学习资料是深蓝学院的课程《机器人中的数值优化》和高立编著的《数值最优化方法》等&#xff0c;本系列文章篇数较多&#xff0c;不定期更新&#xff0c;上半部分介绍无约束优化&#xff0c;…

MySQL——MySQL的基础操作部分

使用命令行登录 mysql -u root -p 直接敲击回车后输入密码即可&#xff1a; 当看到出现“mysql>“的符号之后&#xff0c;就表示已经进入到了&#xff2d;&#xff59;&#xff33;&#xff31;&#xff2c;系统中&#xff0c;就可以输入&#xff2d;&#xff59;&#xf…

Streamlit项目:乐高风格马赛克设计工坊~打造个性化马赛克图案的平台

文章目录 1 前言2 项目概述2.1 项目背景和目标2.2 功能和特性2.3 技术实现2.4 开发计划2.5 预期成果2.6 应用场景 3 使用指南3.1 源代码使用指南3.2 普通网页用户使用指南3.3 文件保存3.3.1 导出图像文件3.3.2 获取像素RGB数据 3.4 注意事项 4 实现细节4.1 准备工作4.2 编写代码…

如何手动读取 GLTF/GLB 文件

推荐&#xff1a;使用NSDT场景编辑器快速搭建3D应用场景 文件类型 GLTF文件有两种不同的主要文件类型&#xff1a;.gltf和.glb。 GLTF文件本质上只是一个重新命名的json文件&#xff0c;它们通常与包含顶点数据等内容的.bin文件相提并论&#xff0c;但这些内容也可以直接包含…

聊聊大模型位置编码及其外推性

作者 | 王嘉宁 整理 | NewBeeNLP https://wjn1996.blog.csdn.net/article/details/131753251 大家好&#xff0c;这里是 NewBeeNLP。 现如今很多大模型都开始支持超过4096长度的推理&#xff0c;例如GPT-4支持超过30k&#xff0c;ChatGLM2-6B也支持最长为32K的文本。但是由于显…

CentOS7安装时直接跳过了安装信息摘要页面的解决方法

最近在配置Hadoop虚拟机的时候&#xff0c;创建的centos7虚拟机在安装信息摘要时直接自动跳过&#xff0c;直接跳到设置用户名和密码&#xff0c;在重复多次的重新删除安装后发现了问题所在&#xff1a; 在进行到选择操作系统来源时&#xff0c;注意是否出现“该操作系统将使用…