【算法分析与设计】5 桥

本文详细探讨了图论中的桥概念,介绍了如何通过深度优先搜索(DFS)和查并集(Disjoint Set Union, DSU)算法来寻找图中的桥。桥是删除后会导致图连通性增加的边。文章对比了DFS和DSU两种算法的效率,并提出了优化策略,如使用路径压缩和最小公共祖先(LCA)来提升查桥算法的性能。实验结果显示,结合DSU和LCA的算法在效率上有显著优势。
摘要由CSDN通过智能技术生成

相关资源下载

(62条消息) 算法设计与分析-5图论桥报告.docx-算法与数据结构文档类资源-CSDN文库

(62条消息) 算法设计与分析-5图论桥preppt.pptx-算法与数据结构文档类资源-CSDN文库

(62条消息) 算法设计与分析-5图论桥源代码.cpp-算法与数据结构文档类资源-CSDN文库

目录

相关资源下载

概览

求解问题

基准算法

查并集

查找函数find()伪代码

合并函数join()伪代码(基本实现)

数据获取

基准算法

连通块获取

基准法1:深度优先dfs实现的 DFScoutArea()函数

基准法2:查并集dsu实现的DSUcoutArea()函数

查并集连通块数获取

高效算法

高效算法1:dfs基准算法优化(判断可达)

高效算法2:查并集+最小公共祖先

查并集生成树细节

寻找最小公共祖先lca(edge)函数(参数为加入的非桥边edge)

查并集+最小公共祖先法确定桥数lcaCountBridge()

时间复杂度

数据处理

总结


概览

  1. 图的连通性。
  2. 并查集的基本原理和应用

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

                                  

图 1 没有桥的无向连通图                         图 2 这是有16个顶点和6个桥的图(桥以红色线段标示)

求解问题

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

基准算法

    For every edge (u, v), do following
        a) Remove (u, v) from graph
        b) See if the graph remains connected (We can either use BFS or DFS)
        c) Add (u, v) back to the graph.

查并集

        它是1个树型数据结构,即获得连通块的集合。往往表现为森林,通过更新维护父亲结点实现。需要整型数组father[vn](vn为结点数),初始为自身;查找函数find(),查找每个结点的父亲(可用路径压缩);合并函数join(),将边加入并用find()更新father。

查找函数find()伪代码

find(x):
    if father[x]==x:
        return x    //本身就是祖先直接返回
    return father[x]=find(father[x])    //调用查找函数查找祖先,同时路径压缩

合并函数join()伪代码(基本实现)

join(x,y):    //将断点为x,y的2条边加入,同时更新祖先
    fx=find(x)    //查找端点x的祖先
    fy=find(y)    //查找端点y的祖先
    if fx!=fy:
        father[fx]=fy    //不同属1个家族需要合并

  • 数据获取

        我对每个点的邻点进行记录,避免边稀疏导致较多的空间浪费。同时是动态分配邻接表的大小,我采用集合容器set进行记录,能实现排序和去重功能。

  • 基准算法

连通块获取

  • 基准法1:深度优先dfs实现的 DFScoutArea()函数

        可通过dfs获取,设置father数组标记相同块,每次新标记代表一个新块。

dfs连通块获取伪代码

dfs(node) 	//深度优先搜索
	while j in node.adj:
		if father[j] == father[node]:
			continue
		father[j] = father[node]
		dfs(j)

DFScoutArea():
	res = 0
	while i = 0 to vn:
		if father[i]==i:
			dfs(i)
			res++
	return res

  • 基准法2:查并集dsu实现的DSUcoutArea()函数

        针对原理处进一步展开实现细节如下:

查找find()函数

        这个算法实现可以借鉴一个故事,如果几个家族(同一连通块)在只知道自己父亲的情况下想找祖先(根节点),他们可以一层一层向上问直到找到祖先。祖先相同为同一家族。同时为了避免他的后代在找祖先时有重复了相同的路径,我们干脆在father数组记录他找到的祖先,这样子他的后代追溯到他这里时就立即可以知道祖先,而不用重复在继续找下去。(路径压缩

        find()函数伪代码

find(x):    //找x的祖先
	if x == father[x]:    //找到祖先(没有父亲,父亲就是本身)
		return x    //返回祖先
	return father[x] = find(father[x])    //否则一路向上找祖先,且找到后一路都返回标记祖先(路径压缩)

合并join()函数

        如果有边连通2个独立的连通块,那么他们将合并并归为同一祖先,为了提高算法效率,我们需要让小连通块加入大的连通块

  • 为了实现这一目标,一种方法是用一个大小为结点数vn的数组num存储连通块里点数,下标为祖先。每次有点加入连通块时对应点数增加,2连通块合并时检查大小再更新。

        查找join()函数优化伪代码

join(x, y) {    //边的2端点x y
	xf = find(x)    //记录x的根节点
	yf = find(y)    //记录y的根节点
	if xf != yf:    //非同一连通块时合并
		if num[xf] >= num[yf]:    //判断连通块大小
			father[xf] = yf    //小家族加入大家族
			num[xf]+= num[yf]    //连通块里点数更新
		else:
			father[yf] = xf
			num[yf]+= num[xf]

  • 另外是在查找祖先find()函数时记录更新各节点的深度,各节点深度应该是其子节点的深度加1或本身结点深度,2者取最大。即加入:

depth[father[x]] = max(depth[father[x]], depth[x] + 1)

        在合并函数join()也做相应更新。

查并集连通块数获取

        先初始化父亲数组,用并查集将全部边加入,接着计算家族数

dsu():
	for e in edge:    //遍历全部边加入
		join(e.head, e.tail)

DSUcoutArea():
	unitFather()   //初始化父亲数组,自身为祖先
	dsu()    //并查集
	return familyNum()    //返回家族数

  • 查桥FindBridge()函数

        如果删掉边之后连通块增加,说明为桥。

查桥伪代码

FindBridge():
	area = DFScoutArea()
	while i = 0 to vn:	
		while j in i.adj: // ⽤迭代器遍历集合s⾥⾯的每⼀个元素
			i.erase(j)
			j.erase(i)
			area2 = coutArea()
			if (area2> area) {
				bridge++
				show(i,j)
			node[i].insert(j);
			node[j].insert(i);

  • 时间复杂度分析

   dfs基准法时间复杂度分析

        设点数为n,边数为e,需要遍历全部点检查是否已经标记祖先O(n),又需要遍历全部边标记祖先O(e)。则dfs算法时间复杂度为O(n+e)。查桥需要遍历全部边调用e次dfs,则总的时间复杂度为

O(e(n+e))=O(e*n+e*e)

并查集查桥时间复杂度分析

        由于有路径压缩,即每次将节点直接连到对应的祖先节点上,所以每次查询祖先操作时我们最多通过2向上搜寻即可找到祖先节点。所以单次查询祖先操作的时间复杂度可近似认为O(1),设边数为e,则共有e加边过程时间复杂度为O(e)

        另外在该过程时间开销比较大的就是对父亲结点的更新了。由《算法导论》一书中定理21.1的证明可知,一个有n个点的集合,每个点的父亲记录最多被更新logn。则该方面时间复杂度为O(nlogn)

        所以单次并查集连通块数获取的时间复杂度为

O(e+nlogn)

        查桥需要遍历全部边调用e次dsu,则总的时间复杂度为

O(e(e+nlogn))=O(e* e+nlogn *e)

        明显可见在基准算法深度优先dfs效率会优于并查集dsu

高效算法

  • 高效算法1dfs基准算法优化(判断可达)

        如果删除某条边,可以从该边一端进行深度优先搜索,看能否达到另一端点。如果可达则连通,非桥。查桥思维依旧和基准算法一样,但无需每次重新计算连通块数,而是直接判断删边后的这2点的连通性有无改变。

        最坏情况是每次判断都需要遍历这张图,那么耗费时间会等同简单基准算法,时间复杂度同样为

O(e*n+e*e)

基准算法优化伪代码

dfsBridge(x, y) {
	father[x] = 1
	for i in x.adj:
		if i == y:
			return true
		else if father[i]==1
			continue
		else if dfsBridge(i, y)==true
			return true

  • 高效算法2:查并集+最小公共祖先

        最小公共祖先简称LCA(Lowest Common Ancestor),即2个节点离根最远的祖先节点。我们可以先通过并查集生成树,记录树边(初始为桥边)和非树边。遍历全部非树边,通过LCA找到形成的环并把在该环的树边从桥边去除,桥数减少。

  • 查并集生成树细节

对于上面实现的查并集,需要在增加记录树边和非树边的过程。对于二者的记录,我先创建struct结构体类型edge进行边存储,包括头尾结点head tail的记录、构造函数、以及后续操作所需的等于运算符重载。我采用vector容器记录,且统一head<tail。因为其生成树为等效连通图,所以对树边的记录也需要是等效树边,即原树边的两端点更新为父亲结点。

  • 寻找最小公共祖先lca(edge)函数(参数为加入的非桥边edge

对于每一非树边的加入,我们需要对其两端点进行最小公共祖先的确定其所在环,并把环上树边从桥中去除,桥数对应减少。具体实现细节是先将深度较大结点进行溯源删桥至两端点深度相同,再进行两端点同时溯源删桥直到其找到的最小公共祖先。

寻找最小公共祖先lca(edge)函数(参数为加入的非桥边edge)伪代码

lca(e):	//寻找最小公共祖先lca(edge)函数(参数为加入的非桥边edge)
	x = e.head
y = e.tail
	if depth[x] < depth[y]:
		swap(x, y)  //使x深度大
	while depth[x]>depth[y]://先将深度大点进行溯源删桥至两端点深度相同
		z = father[x]
k = x
		if k > z:
			swap(k, z)
		if e in tree:	//找到桥删除
			tree.erase(e)
			bridge--
		x = father[x]	//溯源更新
	while x!=y:	//再进行两端点同时溯源删桥直到其找到的最小公共祖先
		z = father[x]
k=x
		if k > z:
			swap(k, z)
		if e in tree:
			tree.erase(e)
			bridge--
		x = father[x]
		z = father[y]
k=y
		if k > z:
			swap(k, z)
		if e in tree
			tree.erase(e)
			bridge--
		y = father[y]

  • 查并集+最小公共祖先法确定桥数lcaCountBridge()

    需要先调用查并集生成树,将桥数初始为树边数,再遍历非树边,依次加入对其利用最小公共祖先删除非桥树边。

    lcaCountBridge()伪代码

  lcaCountBridge()伪代码如下
lcaCountBridge():
	unitFather()	//初始祖先为自身
	dsu()  //查并集生成树
	bridge = tree.size()
	for e in notree
		lca(e)

  • 时间复杂度

        仅调用1次查并集,接下来的最小共同祖先查找遍历全部边,所以时间复杂度为O(e+nlogn)

数据处理

        为了减少误差我采取测试100次取平均值,以上算法运行时间如下:

图表 1 各种算法运行时间

算法

基准dfs

基准dsu

基准dsu(路径压缩)

dfs可达

dsu+lca

Time/ms

2.93

9.01

8.24

1.74

0.38

图表 2 各种算法运行时间

 

总结

        由上面的数据可以看出:

  1. 基准算法深度优先DFS比并查集DSU效率高。
  2. 在小规模数据由于深度不大,所以路径压缩效果不明显。
  3. 将基准算法改为判断可达后时间可以缩短40%,效果较明显。
  4. 通过查并集dsu+最近公共祖先lca的方法,可以避免大量的冗余计算,效果明显。

        通过本次实验,我加深对图的连通性的理解和运用,直到如何利用深度优先DFS算法、广度优先BFS算法、查并集DSU算法生成生成树并确定连通性。掌握并查集的基本原理和应用,通过父亲数组father、查找find()、合并join()实现并查集,以确定图的连通性。同时也了解到通过路径压缩和按秩合并的并查集优化方法。路径压缩在图规模较大、树深度较大时效果会比较好。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jennie佳妮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值