无向图求桥(深大算法实验5)报告+代码

2023-11-11 02:51

本文主要是介绍无向图求桥(深大算法实验5)报告+代码,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

实验代码 + 报告资源:
链接: https://pan.baidu.com/s/1CuuB07rRFh7vGQnGpud_vg 
提取码: ccuq 

目录

  • 写在前面
  • 问题描述
  • 求解问题的算法原理描述
    • 基准算法
    • 基准法+并查集
    • 引入生成树优化
    • 基准法+并查集+生成树优化
    • Lca求环边求桥
    • Lca求环边:路径压缩策略
    • Tarjan(塔扬)算法直接求桥:
    • 总览
    • 结论:
  • 给定数据测试
  • 代码

写在前面

期末终于算法课快要完结了。

这学期算法课可谓是最难顶的课程了,又正好是线上上课,提问互动的机会相对较少,老师上课抛砖引玉,实验内容又比较难,我花了大部分的时间在找算法,实现算法,改算法bug上。

我也参考过很多往届师兄的报告,但是大多都比较抽象晦涩,而且没有代码只讲方法,比较难以理解具体实现的细节。

所以我打算记录一下自己的报告+代码,前人coding后人copying ,希望让大家少走弯路。。。

注意:不要直接copy代码,这是冲塔行为!查重系统鲨疯辣。

问题描述

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

求解问题的算法原理描述

基准算法

穷举边并且删除这条边,然后计算连通分支数目,使用dfs计算连通分支数目
在这里插入图片描述
在这里插入图片描述
复杂度分析:其中 n为顶点数,e为边数
穷举删除的边需要e次,每次删除都要dfs判断连通分支数目,需要O(n+e),复杂度O(e)
对于稀疏图(e=n),复杂度为O(n2)
对于稠密图(e=n2),复杂度为O(n4)
在这里插入图片描述
分别使用稀疏图和稠密图测量其运行时间,其中横坐标是顶点数n,纵坐标是运行时间
在这里插入图片描述
在这里插入图片描述
可以看到稀疏图中,基准法符合O(n2)的复杂度,而稠密图中,基准法符合O(n4)的复杂度

基准法+并查集

和基准法思路相同,通过删除边并计算连通分支数目来查找桥,只是计算连通分支时使用并查集而不是dfs

并查集计算连通分支数目:
枚举边,对每个边上的两点v1和v2,查询v1和v2所属的集合f1,f2,如果v1和v2不在同一个集合则合并v1和v2所属的两个集合,最后统计集合的个数,即为连通分支数目

在这里插入图片描述
使用路径压缩策略,使得并查集的查询复杂度均摊下来为O(1)
在这里插入图片描述
在这里插入图片描述
复杂度分析:其中 n为顶点数,e为边数
穷举删除的边需要e次,每次删除都用并查集判断连通分支数目,需要O(e),复杂度O(e)
对于稀疏图(e=n),复杂度为O(n2),对于稠密图(e=n2),复杂度为O(n4)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到稀疏图中,算法符合O(n2)的复杂度,而稠密图中,算法符合O(n4)的复杂度

引入生成树优化

因为桥边一定会出现在生成树上,所以对于基准法,我们只需要枚举生成树上的边,而不需要枚举所有的边,一样能找到答案
在这里插入图片描述
生成树优化能够使得枚举边的代价从O(e)变为O(n),我们可以利用这个特性优化基准法

基准法+生成树优化
生成树优化能够使得枚举边的代价从O(e)变为O(n),对于稀疏图,复杂度为O(n2),对于稠密图,复杂度为O(n3)

在这里插入图片描述

在这里插入图片描述
可以看到在图像和数据上,稀疏图的情况下算法时间复杂度符合O(n2)增长,在稠密图的情况下算法的时间复杂度符合O(n3)增长

基准法+并查集+生成树优化

生成树优化能够使得枚举边的代价从O(e)变为O(n),对于稀疏图,复杂度为O(n2),对于稠密图,复杂度为O(n3)
在这里插入图片描述
在这里插入图片描述
可以看到在图像和数据上,稀疏图的情况下算法时间复杂度符合O(n2)增长,在稠密图的情况下算法的时间复杂度基本符合O(n3)增长

Lca求环边求桥

逆向思维:我们不去主动找桥,因为定义【一条边是一座桥当且仅当这条边不在任何环上】,我们把环边找出来,然后剩下的不是环边的边,就是桥了

引入lca问题:最近公共祖先
最近公共祖先,顾名思义即两个节点的最近的能找到的公共的祖先
在这里插入图片描述

先利用dfs建立生成树,对于图中那些不在生成树上的边,我们叫它【重边】

对于重边上的两点v1和v2,我们找v1和v2的最近公共祖先,并且把从v1和v2出发,到他们的最近公共祖先的路径上的边,全部标记为环边

在这里插入图片描述

dfs配合并查集求解lca问题:
lca[x]表示集合x最高顶点的编号,father[]为并查集数组

收集所有lca查询并且使用dfs建立生成树,然后按照生成树顺序对图做dfs

每次递归开始前:x节点自成一个集合

递归所有子树

一个子树递归结束:将子树并到x节点集合

x所有子树递归结束时:遍历和x有关的查询,并尝试回答这些查询

假设查询为 (x, y) 如果y节点及其所有子树已经完成递归,那么答案是y所在的集合的最高点编号,如果y未完成所有的递归,那么先搁置这个查询(等一下y完成递归的时候会再次尝试回答)

ps:这里lac[x]其实就是father[x]
在这里插入图片描述
在这里插入图片描述
复杂度分析:
以dfs为模板求lca的时间复杂度是O(n+e+q)其中q为查询次数,对于稀疏图,查询次数q为n,对于稠密图,查询次数q为n2,因为除了生成树上的n-1条边,其他的都要查询

对于每个查询,都需要沿着生成树,往回找他们的最近公共祖先,并且标记沿途的边,这个操作的时间复杂度是不确定的,但是其上界是O(n),因为走完所有的顶点,故整个算法的时间复杂度上界是:稀疏图O(n2),稠密图O(n3)

在这里插入图片描述

在这里插入图片描述
可以看到在图像和数据上,稀疏图的情况下算法时间复杂度符合O(n2)增长,在稠密图的情况下算法的时间复杂度基本符合O(n3)增长

Lca求环边:路径压缩策略

注:
这里的最近公共祖先算法好像有点问题,根据大佬反应,情况如下:


如果一开始重边里的两个点的祖先就是一样的情况 跑当时老师给的小数据集跑出了和蛮力法不一样的效果(就是蛮力法是没有桥的但是压缩出现了桥) 加上这个条件后就对了

我当时做的时候好像没有注意到这个情况。。。总之就开始的时候是多一步判断两个点的 father 是不是一样的特殊判断

如图,因为边非常多,我们有时候可能会走很多冤枉路
在这里插入图片描述
我们在沿着生成树向上找lca的时候,找到lca并记录它,然后不妨将路径上的所有节点挂到lca上面(即查找的终点),表示该点和lca点之间的路径被记录过了,在下次向上查找的时候,我们就会直接跳到lca上,不必走冤枉路
在这里插入图片描述
Lca+路径压缩:复杂度分析:
因为加入了路径压缩策略,和并查集类似,我们最多把n个顶点挂到父节点上面,每次找路径都要挂,总共找e次路径,均摊下来使得找路径的复杂度为O(1),于是我们算法的复杂度变为:稀疏图:O(e)*O(1) = O(n) 稠密图:O(e)*O(1) = O(n2)

注:我们也可以直接将环边上的两个点先调整至同一高度,然后一起向上找公共祖先,然后找到后再沿路标记环边。不必像上面所说的那样先找lca再标记。这样更快

/*	*	@function bridge_bcj_lca : lca求环边 + 路径压缩 *	@explain 			     : ---- */
int tcnt=0;
void bridge_bcj_lca2()
{init_all();// 生成生成树 for(int i=0; i<n; i++) if(vis[i]==0) dfs_getTree_relation_dep(i, i, 0); visit_init();for(int i=0; i<n; i++) tree_edges.insert(vector<int>{tree[i],i});for(int i=0; i<edges.size(); i++){if(tree_edges.find(edges[i])==tree_edges.end()){int p1=edges[i][0], p2=edges[i][1], pre,pre1,pre2;cir_edges.insert(vector<int>{p1, p2});	// 标记边 if(depth[p1]<depth[p2]) swap(p1, p2);	// 默认p1为深的点 while(depth[p1]>depth[p2]) 				// 同步高度 pre1=p1,p1=tree[p1],cir_edges.insert(vector<int>{pre1, p1});while(tree[p1]!=tree[p2])				// 找lca {pre1=p1, pre2=p2;p1=tree[p1], p2=tree[p2];cir_edges.insert(vector<int>{pre1, p1});cir_edges.insert(vector<int>{pre2, p2});}// 压缩策略 沿途将路径上所有点挂到lca上 int lca = tree[p1];p1=edges[i][0], p2=edges[i][1];while(tree[p1] != lca){int pf = tree[p1];tree[p1] = lca;depth[p1] = depth[lca] + 1;p1 = pf;}while(tree[p2] != lca){int pf = tree[p2];tree[p2] = lca;depth[p2] = depth[lca] + 1;p2 = pf;}//tcnt++;	// 调试进度 // if(tcnt%100000==0) cout<<tcnt<<endl;}}/*for(int i=0; i<e; i++)if(cir_edges.find(edges[i])==cir_edges.end())cout<<edges[i][0]<<" -- "<<edges[i][1]<<" 是桥"<<endl;*/
}

Lca+路径压缩:时间测试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到算法在稀疏图下的复杂度和我们预想的一样保持在O(n),稠密图则为O(n2)

Tarjan(塔扬)算法直接求桥:

Tarjan算法基于dfs,一次dfs即可求解问题。

在一次dfs中维护两个数组,分别是dfn[]即深度优先数,和min_ancestor[]即最小可达祖先

dfn[]数组描述了dfs进行的顺序,而min_ancestor[x]则记录了x及其所有子孙节点能够到达的【dfn值最小的祖先节点(不包括x的直接父节点)】的dfn数值

在这里插入图片描述

如果一条边的两个顶点v1和v2,满足dfn[v1] < min_ancestor[v2],那么这条边是桥

可以显然的从定义得知这个事实:因为除了和x相连的边,没有任何途径可以到达x之前的节点(体现在最小可达祖先大于x直接父节点的dfn值),也就是说x是唯一通路,是桥

图片描述:
在这里插入图片描述
在这里插入图片描述
Tarjan算法时间复杂度分析:
共需要一次dfs,故稀疏图中复杂度为O(n),稠密图中复杂度为O(n2)
在这里插入图片描述在这里插入图片描述在这里插入图片描述

总览

(注:tarjan算法在这个规模过于快了,以至于测出来都是0或者0.0001不好对比)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

结论:

稀疏图
复杂度:
Tarjan = lca+压缩 < lca = 基准+生成树 = 基准+并查集+生成树 = 基准 = 基准+并查集

用时:
Tarjan < lca+压缩 < lca < 基准+生成树 = 基准+并查集+生成树 = 基准+并查集 = 基准

稠密图
复杂度:
Tarjan = lca+压缩 < lca = 基准+生成树 = 基准+并查集+生成树 < 基准 = 基准+并查集

用时:
Tarjan < lca+压缩 < 基准+生成树 = 基准+并查集+生成树 < lca < 基准+并查集 = 基准

结论:

  1. 一次dfs解决,tarjan(塔扬)算法是最快的算法
  2. 引入路径压缩的lca第二快(复杂度低),但是操作语句较为繁琐所以慢于tarjan
  3. Lca求环边和带生成树优化的基准法远远地快于普通基准法
  4. Lca求环边在边情况少的图中速度远远快于基准法,而在稠密图中,慢于基准法+生成树优化,虽然同为O(n3)的复杂度,但是因为代码的语句数量明显增多(先生成生成树,然后反选重边,然后查询重边的lca,递归沿着路径标记环边,然后通过环边反选桥),而且使用多种复杂的数据结构,比如并查集,哈希set,vector,并且沿着环边标记路径使用的是递归操作,导致算法的常数开销过大,在边多的情况下,不理想
  5. Lca求环边在稠密图中达到最坏情况,即复杂度上限O(n3),从图中曲线也可以看出
  6. 基准法用并查集计算连通分支数目,虽然有路径压缩策略,但总体执行语句数量多于dfs法,因此代码的常数开销过大,导致并查集慢于dfs,但其实它们拥有相同的复杂度

给定数据测试

在这里插入图片描述
在测试数据样例和mediumDG中测试时间,因为规模过小导致时间不好测量

在largeG里面测试,只有lca+压缩和tarjan可以跑完整个过程,但是因为tarjan算法代码实现和语句较为简单,而相对来说lca+压缩语句比较复杂,所以很慢

Lca+压缩,用到较多的复杂数据结构,并且未使用任何针对C++STL的编译优化,所以是代码常数开销很大慢于tarjan

总结:
并查集数据结构要尽量使用路径压缩策略,能够大大加快查询的速度
在求解大规模问题的时候尽量选择简单的数据结构
编写代码时要注意代码语句不能过长,否则同样复杂度的算法,执行用时会有差距
在大规模递归的时候要修改编译器的栈空间,否则数据容量一大,容易爆栈

代码

在这里插入图片描述

#include <bits/stdc++.h>using namespace std;/**	@edge_hash : 边哈希函数 哈希set反取边及去重用 */
struct edge_hash
{size_t operator () (const vector<int> e1) const{int aa=(e1[0]<e1[1])?(e1[0]):(e1[1]

这篇关于无向图求桥(深大算法实验5)报告+代码的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/387052

相关文章

python使用Akshare与Streamlit实现股票估值分析教程(图文代码)

《python使用Akshare与Streamlit实现股票估值分析教程(图文代码)》入职测试中的一道题,要求:从Akshare下载某一个股票近十年的财务报表包括,资产负债表,利润表,现金流量表,保存... 目录一、前言二、核心知识点梳理1、Akshare数据获取2、Pandas数据处理3、Matplotl

Django开发时如何避免频繁发送短信验证码(python图文代码)

《Django开发时如何避免频繁发送短信验证码(python图文代码)》Django开发时,为防止频繁发送验证码,后端需用Redis限制请求频率,结合管道技术提升效率,通过生产者消费者模式解耦业务逻辑... 目录避免频繁发送 验证码1. www.chinasem.cn避免频繁发送 验证码逻辑分析2. 避免频繁

精选20个好玩又实用的的Python实战项目(有图文代码)

《精选20个好玩又实用的的Python实战项目(有图文代码)》文章介绍了20个实用Python项目,涵盖游戏开发、工具应用、图像处理、机器学习等,使用Tkinter、PIL、OpenCV、Kivy等库... 目录① 猜字游戏② 闹钟③ 骰子模拟器④ 二维码⑤ 语言检测⑥ 加密和解密⑦ URL缩短⑧ 音乐播放

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

Python实现MQTT通信的示例代码

《Python实现MQTT通信的示例代码》本文主要介绍了Python实现MQTT通信的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 安装paho-mqtt库‌2. 搭建MQTT代理服务器(Broker)‌‌3. pytho

MySQL进行数据库审计的详细步骤和示例代码

《MySQL进行数据库审计的详细步骤和示例代码》数据库审计通过触发器、内置功能及第三方工具记录和监控数据库活动,确保安全、完整与合规,Java代码实现自动化日志记录,整合分析系统提升监控效率,本文给大... 目录一、数据库审计的基本概念二、使用触发器进行数据库审计1. 创建审计表2. 创建触发器三、Java

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

Java中调用数据库存储过程的示例代码

《Java中调用数据库存储过程的示例代码》本文介绍Java通过JDBC调用数据库存储过程的方法,涵盖参数类型、执行步骤及数据库差异,需注意异常处理与资源管理,以优化性能并实现复杂业务逻辑,感兴趣的朋友... 目录一、存储过程概述二、Java调用存储过程的基本javascript步骤三、Java调用存储过程示

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性:

MySQL数据库的内嵌函数和联合查询实例代码

《MySQL数据库的内嵌函数和联合查询实例代码》联合查询是一种将多个查询结果组合在一起的方法,通常使用UNION、UNIONALL、INTERSECT和EXCEPT关键字,下面:本文主要介绍MyS... 目录一.数据库的内嵌函数1.1聚合函数COUNT([DISTINCT] expr)SUM([DISTIN