算法设计与分析:实验五 图论——桥问题

实验内容:

1. 桥的定义

在图论中,一条边被称为“桥”代表这条边一旦被删除,这张图的连通块数量会增加。等价地说,一条边是一座桥当且仅当这条边不在任何环上。一张图可以有零或多座桥。

2. 求解问题

找出一个无向图中所有的桥。

1.基准算法

(1)算法思路:

  • 遍历每条边:对于图中的每一条边 (u, v),逐条进行处理。
  • 移除边 (u, v):暂时从图中移除边 (u, v)。
  • 检查连通性:使用广度优先搜索(BFS)或深度优先搜索(DFS)检查移除边 (u, v) 后的图是否仍然连通。如果移除边 (u, v) 后,图变得不连通(即连通块的数量增加),则说明边 (u, v) 是一座桥。
  • 恢复边 (u, v):将边 (u, v) 加回到图中,恢复原始状态。
  • 重复上述步骤:对图中的每一条边重复上述步骤,最终找出所有的桥。

(2)伪代码:

基准算法

bool BFS(src,des,eidx) //BFS用来检查移除一条边后,图是否仍然连通

{

  Queue q  // 定义一个队列,用于广度优先搜索

  Vis[src]=true  // 标记起点为已访问

  For 枚举src的邻点j:

    If i>>1 != eidx-1:  //如果此边不是我们要删掉的就入队

      q.push(j),Vis[j]=1

  While(q.size()):  // 当队列不为空时,继续搜索

    u=q.front(),q.pop()  // 取出队首元素

    For 枚举u的邻点v:

      If v==d return True  //如果删掉src-des这条边,两个点还能连通则不是桥

      If !Vis[v]: Vis[v]=true,q.push(v)

  Return false;  // 如果搜索完毕仍未到达目标节点,返回 false,不连通

}

voids func0()  //找到图中的所有桥

{

  For i=1 to m: //枚举每条边

    u,v //u和v作为每条边的两个端点

    memset(Vis ,false,sizeof(Vis)) //每次删除每条边判连通都要初始化访问数组

    If !BFS(u,v,i):  //如果删掉i这条边后不连通则此条边为桥

    Ans++,edge[i].isbridge=true;

}

(3)时间复杂度:

对于每一条边 (u, v),需要执行一次 BFS,BFS会将所有的点和边遍历一遍,则最坏的时间复杂度为 O(n+m),其中n是顶点数,m是边数。由于需要对每一条边都进行一次这样的操作,算法的总时间复杂度为 O(m*(n+m))=O(m^2+nm)。对于稠密图,这样的复杂度是非常高的。基准算法尽管简单直接,但在实际应用中效率较低,因此通常使用更加优化的算法来降低复杂度。

2.优化算法--使用按秩合并及可撤销并查集

(1)算法分析:

  • 当我们处理图的连通性问题时,每次BFS都需要重新遍历所有的点,但删除或添加一条边后,实际上只有与这条边相关的部分需要重新检查,而不是整个图。这意味着我们可以利用一些数据结构来只处理变化部分,从而提高效率。
  • 使用并查集
  1. 并查集是一种高效处理连通性查询的树型数据结构,常用于处理一些不相交集合的合并及查询两个元素是否同属一个集合,支持快速的合并和查找操作。
  2. 并查集将所有点分成一个一个的集合,对每一个集合都设定一个父节点,集合内所有的点最终都会指向该父节点。因此通过检查两个点所在的集合的父节点是否相同,可以以较快的速度判断得到两个点是否处于同一个集合中,若相同则同属一个集合,若不同则不属同一个集合。
  3. 并查集有查找与合并两种操作:一是查询,查询当前两个元素是否在同一集合中;二是合并,将两个不在同一集合中的两个子集合合并为一个集合。

伪代码如下:

并查集基本操作

  1. int find (int x)  //查找当前元素所属集合的代表元/祖先结点
  2. {
  3.   If fa[x]==x: return x  //如果是本身就直接返回
  4.   Return fa[x]=find(fa[x])  //如果不是则继续递归向下找
  5. }
  6. void connect (int x,int y)  //合并两个元素/子集合
  7. {
  8.   Px=find(x) //找到x元素的祖先结点
  9.   Py=find(y) //找到y元素的祖先结点
  10.   If (Px==Py) return //如果两个元素已经属一个集合,就不进行操作
  11.   fa[Py]=Px  //进行合并
  12. }
  1. 标准的并查集在处理动态添加边时非常高效,但不能处理删除边的情况。
  2. 并查集判断一条边是不是割边:去掉一条边后,使用并查集快速查看此边的两个端点是否在同一个集合(即是否可以相互到达),如果是则不是割边,否则是割边。
  • 并查集合并操作采用按秩合并

  1. 在并查集结构中,每个集合都有一个属性称为rank(秩),代表了以该集合根节点为根的树的高度或近似高度。
  2. 按秩合并指的是在合并两个集合时,总是将秩较小的树的根节点指向秩较大的树的根节点。尽可能地减少树的高度增长,保持树的平衡,进而优化查找操作的效率
  3. 在并查集中,秩的初始值为0。当两个秩相同的树合并时,新的根节点的秩增加1。如下图所示,如果按秩合并将A集合合并到D集合,不难发现,合并后的层数依旧是三层;如果将D合并到A上,那么深度就会变成四层。多一层子节点的搜索就会变多,按秩合并就是通过减少层数来减少搜索次数,从而优化时间复杂度。
  4. 路径压缩防止不平衡状态:如果并查集是左边这种形式那么搜索后面的元素那么就会需要O(n)的复杂度,如果所有的元素全部搜索那么就会达到O(n^2)的复杂度。所以采取按秩合并的方法,合并后的形式,这样搜索每一个元素所需时间复杂度为O(1)。
  5. 按秩合并的过程:
  • 每个节点在初始状态下都是一个单独的集合,rk 为 0。
  • 当两个集合进行合并操作时,根据它们的秩进行合并:如果待合并的两个集合的 秩不同,将秩较小的集合合并到秩较大的集合中,这样不会增加整体树的高度。如果待合并的两个集合的秩相同,则任意选择一个作为新的根节点,但需要将秩加 1,以保持整体平衡。

 在这种优化策略下,操作的时间复杂度能够保持在 O(logm),其中 m 是操作次数。

  • 使用支持删除操作的可撤销并查集

  1. 为了处理删除边的情况,可以使用可撤销并查集。通过记录操作的历史,我们可以在需要时回滚操作,恢复之前的状态。这使得我们可以高效处理边的删除和添加。
  2. 考虑使用栈存储并查集合并时修改的信息。撤销的操作遵循栈后进先出的规律。如果要撤销合并操作,可以将栈中的数据弹出,进行数据复原便完成了合并操作撤销。

(2)算法思路:

  1. 先判断第一条边是不是割边。将除第一条边外的所有边,按照边序号从后到前遍历,将边连接的两个点进行并查集的合并操作。然后查询第一条边连接的两个点是否在一个集合里。
  2. 然后判断第二条边是不是割边。撤销并查集对第二条边的修改。将第一条边连接的两个点进行合并。并查集查询第二条边连接的两个点是否在一个集合里。
  3. 然后判断第三条边是不是割边,撤销并查集对第一三条边的修改。将第一第二条边里连接的两个点进行合并。并查集查询第二条边连接的两个点是否在一个集合里。
  4. 以此类推可以通过并查集判断第四条边是不是割边直到最后一条边是不是割边。

(3)伪代码:

优化算法--使用按秩合并及可撤销并查集

  1. void connect( u , v )  //按秩合并
  2. {
  3.   Pu=find(u),Pv=find(v)  //Pu和Pv为两个元素各自所在集合的根节点
  4.   If Pu==Pv: return  //不用合并
  5.   If rk[Pu]<rk[Pv]: stack[++sp]=Node(Pv,Pu,rk[Pv])  //Pu指向Pv,秩为Pv的秩
  6.   Else stack[++sp]=Node(Pu,Pv,rk[Pu])  //Pv指向Pu,秩为Pu的秩
  7.   If rk[Pu]<rk[Pv]: fa[Pu]=Pv  //秩小的根节点指向秩大的根节点
  8.   Else if rk[Pu]==rk[Pv]: fa[Pv]=Pu,rk[Pu]++;
  9.   Else fa[Pv]=Pu
  10. }
  11. void Pop(size) //撤销操作
  12. {
  13.   Node temp;
  14.   While(sp>size):
  15.     Temp=stack[sp--]  //取出栈顶元素
  16.     rk[temp.u]=temp.rk  //恢复秩
  17.     fa[temp.v]=temp.v   //恢复父节点
  18. }
  19. void Check() //遍历每条边
  20. {
  21.   For i from 1 to m:
  22.     u=edge[i].u, v=edge[i].v
  23.     int size=sp;  //记录当前栈顶位置
  24.     For j from m to 1 dec:  //合并除当前边以外的所有边
  25.       If i==j: continue  
  26.       connect(edge[j].u,edge[j].v)
  27.     If find(u)!=find(v):  //如果当前边两个点不在同一个集合即当前边是桥
  28.       edge[i].isbridge=true,ans++;
  29.     Pop(size)  //撤销操作
  30. }
  31. Int func1()
  32. {
  33.   For i from 1 to n:
  34.     fa[i]=i, rk[i]=0  //并查集初始化操作
  35.   sp=0  //栈顶指针初始为0
  36.   Check()
  37. }

(4)时间复杂度:

  1. 合并除当前边以外的所有边:内层循环遍历所有边,除去当前边,时间复杂度是 O(m)。在按秩合并的并查集操作时间复杂度是O(logm)。总的来说,内层循环的时间复杂度是 O(m*logm)。
  2. 撤销操作:Pop(siz); 的时间复杂度取决于操作数量。在最坏情况下,需要撤销所有的合并操作,时间复杂度是 O(m)。
  3. 因此,Check 函数中每次外层循环的时间复杂度是:O(m*logm)。由于外层循环执行 m 次,总时间复杂度是:O(m^2*logm)
  4. 包括初始化的时间复杂度,总的时间复杂度是:O(n+m^2*logm)
  5. 因此总的时间复杂度为:O(m^2*logm)对于边数 m 非常大的图,性能会下降。

3.优化算法--使用可撤销并查集+线段树分治

(1)算法分析:

在使用可撤销并查集判断割边时,每条边都需要通过多次的合并和撤销操作来确定是否是割边。原始方法是逐条边处理,每处理一条边,需要撤销前面所有边的合并操作,这样会导致前面的边在栈中频繁弹入弹出,而后面的边基本没有太多操作。因此,整体效率较低。因此可以利用线段树的分治思想来优化这个过程,使得整体的弹入弹出次数降低。具体来说,通过分治的方式将问题分成多个子问题,解决子问题时可以一次性处理一个区域的边,减少频繁的单条边操作。使用线段树分治优化栈的弹出弹入方法。可以连续弹入或弹出一片区域中的边。

  1. 线段树:线段树是一种二叉搜索树 。它将一段区间划分为若干单位区间 ,每一个节点都储存着一个区间。线段树支持区间求和,区间最大值,区间修改,单点修改等操作。线段树的思想和分治思想很相像。对边进行编号划分,得到所有的边按序排好作为一个区间标记为一号节点,然后将前面一半的边作为一个区间标记为二号节点,将后面一半的边作为一个区间标记为三号节点,以此类推,得到下图所示的线段树结构。当查询到下图箭头所指区域时,其他边(绿色块)都处于被压入栈的情况,并且深度越大的绿色块,在栈中的高度越高
  2. 利用线段树优化查询:实际运行中,会以深搜的方法遍历每一个叶子节点,而每一个叶子节点就对应一条边,比如以上图为例,首先会经过1,2,4号节点到达8号结点,找到第1号边;然后回溯到4号节点,再来到9号节点,找到第2号边;然后再回溯到4号节点和2号节点,进入5号节点和10号节点,找到第3号边。以此类推。查询完橙色的4号边之后,要查询5号边,首先根据深搜的顺序,回溯到1号节点,途中将10,4,3号节点所囊括的边全部弹出栈中,然后进入3号节点,进入3号节点前,先将2号节点囊括的边压入栈中,然后进入6号节点,进入6号节点前,先将7号节点所囊括的边压入栈中,最后进入12号节点,进入12号节点前,将13号节点所囊括的边压入栈中,得到此时仅剩12号节点对应的5号边没有连接。如果后面要查询6号边操作同理,过程如下图表所示(从上到下从左到右):


再使用并查集查询某边连接的两个点是否在用一个集合,以此来判断此边是否为割边。以此类推遍历每一个叶节点的边就可以完成查询所有边是否为割边(即是否为桥)。

(2)算法实现思路:

  1. 对于每个区间 [l, r],如果区间只有一条边,则直接判断是否为桥边。
  2. 否则,递归处理右半部分 [mid+1, r],合并区间内所有边,再递归处理左半部分 [l, mid],最后撤销合并操作。
  3. 通过分治递归,有效减少了频繁的栈操作,提高了整体的处理效率。

(3)伪代码:

优化算法--使用按秩合并及可撤销并查集+线段树分治

  1. void Push(l , r)
  2. {  //将编号为l到r的边进行合并
  3.   For i from r to l dec:
  4.     connect(edge[i].u,edge[i].v)
  5. }
  6. void DFS(l , r)  //递归函数
  7. {
  8.   If l==r : //基本情况,区间只有一条边,直接判断
  9.     If find(edge[l].u)!=find(edge[l].v) : //如果不连通则是桥
  10.       edge[l].isbridge=true,ans++
  11.     Return
  12.   size=sp  //保存当前栈顶位置
  13.   mid=(l+r)>>1
  14.   Push(mid+1,r) //处理右半部分
  15.   DFS(l,mid)
  16.   Pop(size) //撤销右半部分的合并操作
  17.   Push(l,mid) //处理左半部分
  18.   DFS(mid+1,r)
  19.   Pop(size) //撤销左半部分的合并操作
  20. }
  21. Int func2()
  22. {
  23.   For i from 1 to n:
  24.     fa[i]=i, rk[i]=0  //并查集初始化操作
  25.   sp=0  //栈顶指针初始为0
  26.   DFS(1,m)
  27. }

(4)时间复杂度:

  1. 在按秩合并的可撤销并查集操作时间复杂度下降为O(logm)。
  2. 在线段树分治中,递归深度为O(logm),每次递归中调用Push和Pop操作,分别处理两个子区间,合并和撤销的边数最多为O(m),即为O(mlogm)
  3. 因此,总的时间复杂度为O(m*(logm)^2)
  4. 此外,并查集的操作实际上很多时候并不会到达O(logm)的时间复杂度,所以总体而言这个时间复杂度还是比较不错的。

4.拓展:Tarjan算法验证并查集优化+线段树分治的算法正确性

使用 Tarjan 算法找到所有的桥边后,可以与并查集优化+线段树分治算法优化的结果进行比较,验证两者是否一致。若一致,说明线并查集优化+线段树分治的方法正确无误。

Tarjan算法:通过深度优先搜索(DFS)结合时间戳和回溯时间戳来判断一个边是否是桥边。算法的时间复杂度是O(n+m),其中n是节点数,m是边数。

Tarjan算法思路:

 1.初始化:将 dfn (用于存储每个节点的首次访问时间戳)和 low(用于存储节点的回溯时间戳,即当前节点或其子树能够回溯到的最早的祖先节点的时间戳) 数组清零。初始化时间戳 tstp 为0。

2.遍历所有节点:对每个节点,如果还没有被访问过(即 dfn[i] == 0),调用 tarjan 函数从该节点开始进行DFS。

3.DFS过程:遍历所有邻接节点 v,如果 v 未被访问过,递归调用 tarjan,更新 low[u]。如果 dfn[u] < low[v],说明 u-v 是桥边。如果 v 已被访问,且不是通过父边返回,更新 low[u]。

Tarjan算法伪代码:

Tarjan算法

  1. Int dfn[MAXN],low[MAXN],tstp=0
  2. void tarjan(u , pre)
  3. {
  4.   dfn[u]=low[u]=++tstp //初始化dfn和low
  5.   For 枚举u的邻点v:
  6.     If !dfn[v]://如果v未被访问
  7.       tarjan(v,i)  //递归访问v
  8.       low[u]=min(low[u],low[v])  //更新low[u]
  9.       If dfn[u]<low[v]://判断是否为桥边,如果dfn[u]<low[v]则是桥边
  10.         edge[i/2+1].isbridge=true,ans++
  11.     Else if (i!=(pre^1))://更新low[u](忽略父边)
  12.       low[u]=min(low[u],dfn[v])
  13. }
  14. Int func3()
  15. {
  16.   For i from 1 to n:
  17.     dfn[i]=low[i]=0  
  18.   tstp=0  
  19.   For i from 0 to n-1:
  20.     If !dfn[i]:tarjan( i , -1)
  21. }

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

若依,前后端分离项目,部署到服务器

1.后端项目用maven打包 正式服的话&#xff0c;测试不用加。 application.yml加上context-path: /prod-api 一定要选择root的ruoyi&#xff0c;他会把你自动打包其他模块的依赖 全部成功。然后去ruoyi-admin拿到这个包&#xff0c;java -jar ruoyi-admin.jar就可以了 将jar上…

Linux上启动redis

1.默认启动方式:在系统的任意位置执行 redis-server即可启动 ps:这是前端界面启动&#xff0c;无法直接连接redis&#xff0c;想要连接的话只能另外启动一个窗口&#xff0c;因此下面我们介绍后台启动redis 2.指定配置启动&#xff1a; redis的配置文件位置&#xff1a…

数学建模--皮尔逊相关系数、斯皮尔曼相关系数

目录 1.总体的皮尔逊相关系数 2.样本的皮尔逊相关系数 3.对于皮尔逊相关系数的认识 4.描述性统计以及corr函数 ​编辑 5.数据导入实际操作 6.引入假设性检验 6.1简单认识 6.2具体步骤 7.p值判断法 8.检验正态分布 8.1jb检验 8.2威尔克检验&#xff1a;针对于p值进行…

Java基础(6)- Java代码笔记3

目录 一、二维数组 1.二维数组定义 a.动态初始化 b.静态初始化 c.简单静态初始化 2.获取数组长度 二、方法 1.无参无返回值方法 2.有参无返回值方法 3.无参有返回值方法 4.有参有返回值方法 5.形式参数和实际参数 6.三层架构思想 7.方法注意事项 8.数组作为方法…

深度强化学习算法(六)(附带MATLAB程序)

深度强化学习&#xff08;Deep Reinforcement Learning, DRL&#xff09;结合了深度学习和强化学习的优点&#xff0c;能够处理具有高维状态和动作空间的复杂任务。它的核心思想是利用深度神经网络来逼近强化学习中的策略函数和价值函数&#xff0c;从而提高学习能力和决策效率…

8.30工作笔记

要做的事情&#xff1a; 1 测试剩下的三个因子&#xff1a;coppock 潮汐因子 云开雾散 2 整理需要时间序列的因子 以及截面因子 3 灾后重建多了一列&#xff0c;灾后重建’所有值都是nan&#xff0c;这里不仅是灾后重建&#xff0c;所有的都要改 4 coppock 潮汐因子 云开雾散在…

【Qt】菜单栏

目录 菜单栏 例子&#xff1a;创建菜单栏、菜单、菜单项 例子&#xff1a;给菜单设置快捷键 例子&#xff1a;给菜单项设置快捷键 例子&#xff1a;添加子菜单 例子&#xff1a;添加分隔线 例子&#xff1a;添加图标 菜单栏 Qt中的菜单栏是通过QMenuBar这个类实现的&…

MySQL:复合查询

MySQL&#xff1a;复合查询 聚合统计分组聚合统计group byhaving 多表查询自连接子查询单行子查询多行子查询多列子查询from子查询 合并查询unionunion all 内连接外连接左外连接右外连接全外连接 视图 MySQL 复合查询是数据分析和统计的强大工具&#xff0c;本博客将介绍如何使…

当AI遇上制药:加速跑向未来的快车道,还是布满荆棘的征途?

01 在全球科技领域&#xff0c;AI的崛起无疑掀起了一场变革的风暴&#xff0c;其影响力已渗透至各行各业&#xff0c;促使各领域积极寻求与AI技术的深度融合&#xff0c;以提升效率、创新产品及优化服务。在医疗健康领域&#xff0c;AI与制药的结合自2007年起航&#xff0c;历…

第八周:机器学习

目录 摘要 Abstract 一、注意力机制V.S.自注意力机制 1、引入 2、注意力机制 3、自注意力机制 二、自注意力机制 1、输入 2、输出 3、序列标注 4、Multi-head Self-attention 5、比较 总结 摘要 前两周学习了CNN的基本架构&#xff0c;针对全局信息的考虑问题&…

行为识别实战第二天——Yolov5+SlowFast+deepsort: Action Detection(PytorchVideo)

Yolov5SlowFastdeepsort 一、简介 YoloV5SlowFastDeepSort 是一个结合了目标检测、动作识别和目标跟踪技术的视频处理框架。这一集成系统利用了各自领域中的先进技术&#xff0c;为视频监控、体育分析、人机交互等应用提供了一种强大的解决方案。 1. 组件说明&#xff1a; Y…

如何通过住宅代理进行高效SSL检查

引言 什么是SSL检查&#xff1f;有哪些内容&#xff1f; 为什么要使用SSL检查&#xff1f; SSL检查是如何进行的&#xff1f; 总结 引言 在现代互联网环境中&#xff0c;SSL/TLS协议已成为确保网络通信安全的基石。随着网络攻击手段的不断演进&#xff0c;仅仅依赖于基础的…

数据中心和算力中心的区别

数据中心&#xff08;Data Center&#xff09;和算力中心&#xff08;Computing Power Center 或 HPC Center&#xff09;虽然都涉及数据处理和存储&#xff0c;但它们的重点和用途有所不同。下面将详细介绍两者之间的区别&#xff1a; 数据中心&#xff08;Data Center&#x…

torch、torchvision、torchtext版本兼容问题

1、torch与torchtext版本兼容 参考torchtext PyPI 2、 torch与torchvision版本兼容 参考torchvision PyPI

【最新华为OD机试E卷】最长连续方波信号(200分)-多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-E/D卷的三语言AC题解 💻 ACM金牌🏅️团队| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,…

从跟跑到领跑:AIGC时代国产游戏的崛起与展望

引言 在人工智能技术快速发展的背景下,AIGC(人工智能生成内容)时代的到来正在重新定义游戏产业的未来。人工智能技术,尤其是生成对抗网络(GAN)、自然语言处理(NLP)、深度学习等领域的突破,正在为游戏开发带来前所未有的机会和挑战。这些技术不仅改变了游戏内容的创作…

51单片机-定时器介绍

时间&#xff1a;2024.8.31 作者&#xff1a;Whappy 目的&#xff1a;手撕51 代码&#xff1a; 现象&#xff1a;

UnrealEngine学习(01):安装虚幻引擎

1. 下载安装 Epic Games 目前下载UE引擎需要先下载Epic Games&#xff0c;官网为我们提供了下载路径&#xff1a; https://www.unrealengine.com/zh-CN/downloadhttps://www.unrealengine.com/zh-CN/download 我们点击图中步骤一即可进行下载。 注释&#xff1a;Unreal Engi…

揭秘扩散模型:DDPM的数学基础与代码实现全攻略!

(DDPM) denoising diffusion probabilistic models 理论学习 本文价值 本文是 Diffusion 这一类模型的开山之作&#xff0c;首次证明 diffusion 模型能够生成高质量的图片&#xff0c;且奠定了所有后续模型的基本原理&#xff1a;加噪 --> 去噪。DDPM 模型的效果如下&#x…

驾驭高效编程:一探C++ STL的奥秘

1.什么是STL 2.:STL的版本 2.1:原始版本 2.2:P.J版本 2.3:RW版本 2.4:SGI版本 3:STL的六大组件 4:如何学习STL 5:STL的缺陷 1.什么是STL STL(standdard template library-标准模板库):是C标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包含数据结构与算法软…