图论之寻找桥边

目录

①基准法

②并查集

③逆向思维之标记环边

④并查集压缩路径


①基准法

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

因此找桥边最简单粗暴的思想,也就是基准算法的思想就是可以暴力枚举每一条边,将这条边删除后,判断图的连通分量有没有因此而增加,如果图的连通分量增加了,那么说明这是一条桥边。

如图1所示,我们将图的每一条边都删除一遍,然后数一下图的连通分量有没有因此而增加,如果图的连通分量因此增加了,那么说明我们刚刚删除的这条边就是桥边。

图1 基准暴力枚举

图的连通分量的个数可以通过深度遍历的次数来计算,将图的所有顶点都访问一遍所需要的深度遍历次数即是该图连通分量的个数。

则很容易找出图中的六条桥边,如图2中红色边所示,即(0,1),(2,3),(2,6),(6,7),(9,10),(12,13),删除任意一条桥边,图的连通分量都会增加。

图2 小图桥边

C++代码

//
// Created by YEZI on 2023/5/29.
//#ifndef BRIDGE_BENCHMARK_H
#define BRIDGE_BENCHMARK_H#include<iostream>
#include<vector>using namespace std;
namespace Benchmark {class Map {vector<pair<int, int>> bridges;vector<vector<int>> map;vector<bool> visited;vector<pair<int, int>> edges;int edgeNumber;int vertexNumber;int connectedComponent;int count;public:Map(int edgeNumber, int vertexNumber) : edgeNumber(edgeNumber), vertexNumber(vertexNumber) {map.resize(vertexNumber);}void addEdge(int head, int tail, bool init = false) {map[head].push_back(tail);map[tail].push_back(head);if (init) {edges.emplace_back(head, tail);}}void removeEdge(int head, int tail) {for (auto it = map[head].begin(); it != map[head].end(); it++) {if (*it == tail) {map[head].erase(it);break;}}for (auto it = map[tail].begin(); it != map[tail].end(); it++) {if (*it == head) {map[tail].erase(it);break;}}}void DFS(int &current) {if (visited[current])return;visited[current] = true;count++;for (auto next: map[current]) {DFS(next);}}int countComponent() {int component = 0;visited.assign(vertexNumber, false);for (int i = 0; i < vertexNumber; i++) {count = 0;DFS(i);if (count) {component++;}}return component;}void findBridge() {connectedComponent = countComponent();for (auto &edge: edges) {removeEdge(edge.first, edge.second);if (connectedComponent < countComponent()) {bridges.emplace_back(edge.first, edge.second);}addEdge(edge.first, edge.second);}}void showBridge() {for (auto &bridge: bridges) {cout << bridge.first << '-' << bridge.second << endl;}}};
}
#endif //BRIDGE_BENCHMARK_H

图使用邻接表来存储,先用图2的小规模图来检验基准算法的正确性,结果如图3所示,基准算法可以在4微秒内正确找出所有的桥边,算法正确。

图3 基准算法 小规模图

再用基准算法跑medium图和large图,medium可以在124微秒内跑完,是没有桥边的,而large图无法在短时间跑出结果,具体数据如表1所示。

表1 基准算法

②并查集

如图4所示,并查集是一种树形的数据结构,用来维护元素的不相交集合,支持元素的查找和合并的操作。元素的查询只需一路向上找到根节点,集合的合并只需将一棵树的根节点连到另一棵树的根节点上。

图4 并查集

在基准算法的基础,我们可以使用并查集来进行进一步的优化。在基准算法中,我们通过记录遍历所有图顶点所需要的深度遍历次数来计算图的连通分量数目,在这里我们可以使用并查集来计算图的连通分量数目。具体操作如下。

我们首先初始化并查集,将图的每一个节点都作为单独的一个集合,如图5所示。

图5 初始化并查集

然后遍历图中的每一条边,判断每一条边的两个顶点是否处在同一集合,如果不在同一集合,则将这两个顶点所在的两个集合合并成为一个集合,如图6所示,最后集合的数目即为图的连通分量数目。

图6 并查集 合并

 C++代码

#ifndef BRIDGE_DISJOINT_H
#define BRIDGE_DISJOINT_H#include<iostream>
#include<vector>using namespace std;namespace Disjoint {class Map {vector<pair<int, int>> bridges;vector<pair<int, int>> edges;vector<pair<int,int>>edgesTemp;vector<int> root;int edgeNumber;int vertexNumber;int connectedComponent;public:Map(int edgeNumber, int vertexNumber) : edgeNumber(edgeNumber), vertexNumber(vertexNumber) {root.resize(vertexNumber);}void addEdge(int head, int tail, bool init = false) {if (init) {edges.emplace_back(head, tail);}else{edgesTemp.emplace_back(head,tail);}}int countComponent() {int component = 0;for (int i = 0; i < vertexNumber; i++) {root[i] = i;}for(auto&edge:edgesTemp){merge(edge.first,edge.second);}for(int i=0;i<vertexNumber;i++){if(root[i]==i){component++;}}return component;}int findRoot(int&vertex){if(root[vertex]==vertex){return vertex;}return root[vertex]= findRoot(root[vertex]);}void merge(int&u,int&v){int uRoot= findRoot(u);int vRoot= findRoot(v);if(uRoot!=vRoot){root[vRoot]=uRoot;}}void removeEdge(pair<int,int>edge){for(auto it=edgesTemp.begin();it!=edgesTemp.end();it++){if(*it==edge){edgesTemp.erase(it);break;}}}void findBridge() {edgesTemp=edges;connectedComponent=countComponent();for(auto&edge:edges){removeEdge(edge);if(connectedComponent<countComponent()){bridges.emplace_back(edge.first,edge.second);}addEdge(edge.first,edge.second);}}void showBridge() {for (auto &bridge : bridges) {cout << bridge.first << '-' << bridge.second << endl;}}};
}#endif //BRIDGE_DISJOINT_H

先用小规模图来检验算法的正确性,结果如图7所示,使用并查集可以在3微秒内正确找出所有桥边,算法正确,且比基准算法更快。

图7 并查集 小规模图

再跑medium图和large图,medium可以在100微秒内跑完,相比基准算法跑的更快了,但large图仍无法在短时间跑出结果,具体数据如表2所示。

表2 并查集

③逆向思维之标记环边

我们在前面说过,在图论中,一条边被称为“桥”代表这条边一旦被删除,这张图的连通块数量会增加。等价地说,一条边是一座桥当且仅当这条边不在任何环上。也就是说环边绝对不是桥边,桥边绝对不是环边,即桥边是非环边。

因此,我们可以先找出所有的环边并标记上,然后剩下的非环边即是我们要寻找的桥边。

那么怎么样找出所有的环边呢?我们先用深度优先遍历将所有顶点通过边连接的关系生成一棵棵树,如图8所示。

图8 生成树

然后将遍历每一条非树边,由于非树边是构建生成树多余的边,所以非树边一定是环边,且每一条非树边的两个顶点开始往上直到它们最近公共祖先的路径上的所有边都是环边,如图9所示,非树边(14,15)的两个顶点14和15属于同一棵树,顶点14和顶点15往上直到它们的最近公共祖先10的路径上所有边都是环边。

图9 寻找环边

C++代码

//
// Created by YEZI on 2023/5/31.
//#ifndef BRIDGE_LOWESTCOMMONANCESTOR_H
#define BRIDGE_LOWESTCOMMONANCESTOR_H#include<iostream>
#include<vector>using namespace std;
namespace LCA {class Map {vector<vector<int>> map;vector<bool> visited;vector<pair<int, int>> edges;vector<pair<int, int>> notTreeEdges;vector<bool> notLoopEdges;vector<int> depth;vector<int> father;int vertexNumber;public:Map(int edgeNumber, int vertexNumber) :  vertexNumber(vertexNumber) {map.resize(vertexNumber);depth.resize(vertexNumber);notLoopEdges.assign(vertexNumber, false);visited.assign(vertexNumber, false);father.resize(vertexNumber);for (int i = 0; i < vertexNumber; i++) {father[i] = i;}}void buildTree(int &current, int deep, int &currentFather) {depth[current] = deep;father[current] = currentFather;visited[current] = true;for (auto &son: map[current]) {if (!visited[son]) {notLoopEdges[son] = true;buildTree(son, deep + 1, current);}}}void createTree() {for (int i = 0; i < vertexNumber; i++) {if (!visited[i]) {buildTree(i, 0, i);}}}void addEdge(int head, int tail, bool init = false) {map[head].push_back(tail);map[tail].push_back(head);if (init) {edges.emplace_back(head, tail);}}void findNotTreeEdge() {for (auto &edge: edges) {if (father[edge.first] != edge.second && father[edge.second] != edge.first) {notTreeEdges.emplace_back(edge.first, edge.second);}}}void findLoopEdge(pair<int, int> &edge) {int u=edge.first;int v=edge.second;while(true){if(depth[u]>depth[v]){notLoopEdges[u]=false;u=father[u];}else if(depth[u]<depth[v]){notLoopEdges[v]=false;v=father[v];}else if(u!=v){notLoopEdges[u]=false;u=father[u];notLoopEdges[v]=false;v=father[v];}else{break;}}}void findBridge() {createTree();findNotTreeEdge();for (auto &edge: notTreeEdges) {findLoopEdge(edge);}}void showBridge() {for(int i=0;i<vertexNumber;i++){if(notLoopEdges[i]){cout<<i<<'-'<<father[i]<<endl;}}}};
}
#endif //BRIDGE_LOWESTCOMMONANCESTOR_H

 

先用小规模图来检验算法的正确性,结果如图10所示,可以在1微秒内正确找出所有桥边,算法正确,且比之前的算法更快。

图10 标记环边 小规模图

再跑medium图和large图,medium可以在7微秒内跑完,相比之前算法跑的更快了,但large图仍无法在短时间跑出结果,具体数据如表3所示。

表3 标记环边

④并查集压缩路径

标记环边的方法在寻找非树边两个顶点的最近公共祖先的时候如果树的深度很深那么消耗的时间会很多,我们可以使用并查集减小树的深度,如图10所示,我们可以将同属于一棵树的所有节点的父节点都设为根节点,这样可以减小树的深度,从而大大减小寻找最近公共祖先的时间。实际上,并查集存储的是同一个环的边,可以通过一个记录父节点的数组实现并查集。

图10 路径压缩

C++代码

//
// Created by YEZI on 2023/5/31.
//#ifndef BRIDGE_LOWESTCOMMONANCESTOR_H
#define BRIDGE_LOWESTCOMMONANCESTOR_H#include<iostream>
#include<vector>using namespace std;
namespace LCA {class Map {vector<vector<int>> map;vector<bool> visited;vector<pair<int, int>> edges;vector<pair<int, int>> notTreeEdges;vector<bool> notLoopEdges;vector<int> depth;vector<int> father;int vertexNumber;public:Map(int edgeNumber, int vertexNumber) :  vertexNumber(vertexNumber) {map.resize(vertexNumber);depth.resize(vertexNumber);notLoopEdges.assign(vertexNumber, false);visited.assign(vertexNumber, false);father.resize(vertexNumber);for (int i = 0; i < vertexNumber; i++) {father[i] = i;}}void buildTree(int &current, int deep, int &currentFather) {depth[current] = deep;father[current] = currentFather;visited[current] = true;for (auto &son: map[current]) {if (!visited[son]) {notLoopEdges[son] = true;buildTree(son, deep + 1, current);}}}void createTree() {for (int i = 0; i < vertexNumber; i++) {if (!visited[i]) {buildTree(i, 0, i);}}}void addEdge(int head, int tail, bool init = false) {map[head].push_back(tail);map[tail].push_back(head);if (init) {edges.emplace_back(head, tail);}}void findNotTreeEdge() {for (auto &edge: edges) {if (father[edge.first] != edge.second && father[edge.second] != edge.first) {notTreeEdges.emplace_back(edge.first, edge.second);}}}void compressPath(int current,int ancestor){while(father[current]!=ancestor){int next=father[current];father[current]=ancestor;depth[current]=depth[ancestor]+1;current=next;}}void findLoopEdge(pair<int, int> &edge) {int u=edge.first;int v=edge.second;while(true){if(depth[u]>depth[v]){notLoopEdges[u]=false;u=father[u];}else if(depth[u]<depth[v]){notLoopEdges[v]=false;v=father[v];}else if(u!=v){notLoopEdges[u]=false;u=father[u];notLoopEdges[v]=false;v=father[v];}else{compressPath(edge.first,father[u]);compressPath(edge.second,father[u]);break;}}}void findBridge() {createTree();findNotTreeEdge();for (auto &edge: notTreeEdges) {findLoopEdge(edge);}}void showBridge() {for(int i=0;i<vertexNumber;i++){if(notLoopEdges[i]){cout<<i<<'-'<<father[i]<<endl;}}}};
}
#endif //BRIDGE_LOWESTCOMMONANCESTOR_H

 

先用小规模图来检验算法的正确性,结果如图11所示,可以在1微秒内正确找出所有桥边,算法正确,且比之前的算法更快。

图11 路径压缩跑小规模图

再跑medium图和large图,medium可以在6微秒内跑完,相比之前算法跑的更快了, large图只花了0.452秒便跑出了结果,成功找出8条桥边,如图12所示。

图12 路径压缩跑large图

具体数据如表4所示。

表4 路径压缩

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

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

相关文章

golang数据库操作相应内容--推荐【比较全】

Go为开发数据库驱动定义了一些标准接口&#xff0c;开发者可以根据定义的接口来开发相应的数据库驱动&#xff0c;只要是按照标准接口开发的代码&#xff0c; 以后需要迁移数据库时&#xff0c;不需要任何修改。 一、database/sql接口 1.1sql.Register 这个存在于database/s…

Java新特性:Lambda表达式

Java新特性&#xff1a;Lambda表达式 Lambda 表达式&#xff08;Lambda expression&#xff09;&#xff0c;也可称为闭包&#xff08;Closure&#xff09;&#xff0c;是 Java&#xff08;SE&#xff09;8 中一个重要的新特性。Lambda 表达式允许我们通过表达式来代替功能接口…

Django_haystack全文搜索

haystack是全文搜索的框架&#xff0c;支持whoosh、solr、Xapian、Elasticsearc四种全文检索引擎&#xff0c;点击查看官方网站。 whoosh是用纯Python编写的全文搜索引擎&#xff0c;虽然性能比不上sphinx、xapian、Elasticsearc等&#xff0c;但是无二进制包&#xff0c;程序…

一百二十八、Kettle——从Hive增量导入到ClickHouse

一、目标 用Kettle把Hive的DWS层数据增量导入到ClickHouse中 工具版本&#xff1a;Kettle&#xff1a;8.2 Hive:3.1.2 ClickHouse21.9.5.16 全量导入请访问拙作链接 http://t.csdn.cn/Rqvuvhttp://t.csdn.cn/Rqvuv 二、前提准备 &#xff08;一&#xff09;kettl…

Vue3 +Echarts5 可视化大屏——屏幕适配

项目基于Vue3 Echarts5 开发&#xff0c;屏幕适配是使用 scale 方案 Echarts组件按需引入&#xff0c;减少打包体积 地图组件封装&#xff08;全国&省份地图按需加载&#xff09; 效果图&#xff1a; 屏幕适配 大屏适配常用的方案有 rem vw/vh 和 scale 。 rem vw/vh …

vue 使用 npm run dev命令后 自动打开浏览器为谷歌

文章目录 需求分析 需求 vue 启动后&#xff0c;想要其自动打开指定浏览器&#xff08;谷歌&#xff09;并设置要打开的IP地址和端口号 分析 package.json 打开package.json文件加上 --open chrome index.js 打开index.js文件&#xff0c;将浏览器设置为自动打开

模板类与继承

模板类与继承 模板类继承普通类普通类继承模板类的实例化版本。普通类继承模板类模板类继承模板类模板类继承模板参数给出的基类 模板类继承普通类 基类 派生类 测试函数; 普通类继承模板类的实例化版本。 模板基类 普通类继承模板基类的实例化版本&#xff1a; 普通…

LVS和keepa lived群集

keepa lived 简述 一.keepalived 服务重要功能 1管理LS负载均衡器软件 keepalived可以通过读取自身的配置文件&#xff0c;实现通过更底层的接口直接管理Lvs配置以及服务的启动 停止功能 这会使 LVS应用跟更加简便 2 支持故障自动切换 (failover) ①两台知己同时安装好kee…

Kotlin~Decorator装饰器模式

概念 装饰模式指的是在不必改变原类文件和使用继承的情况下&#xff0c;动态地扩展一个对象的功能。它是通过创建一个包装对象&#xff0c;也就是装饰来包裹真实的对象。可以运行时动态添加新的功能&#xff0c;而无需改变原来的代码。 特点&#xff1a; 灵活扩展新的功能动态…

buu-Reverse-[2019红帽杯]childRE

目录 [2019红帽杯]childRE 修饰函数名和函数签名是什么&#xff1f; 对于变换部分的具体分析&#xff1a; [2019红帽杯]childRE 下载附件&#xff0c;查壳&#xff0c;无壳 在IDA中打开&#xff0c;定位主函数 int __cdecl main(int argc, const char **argv, const char …

Kotlin~责任链模式

概念 允许多个对象按顺序处理请求或任务。 角色介绍 Handler: 处理器接口,提供设置后继者&#xff08;可选&#xff09;ConcreteHandler&#xff1a;具体处理器&#xff0c;处理请求 UML 代码实现 比如ATM机吐钱就可以使用责任链实现。 class PartialFunction<in P1, o…

POLARDB IMCI 白皮书 云原生HTAP 数据库系统 一 列式数据是如何存储与处理的

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…