图论之寻找桥边

news/2024/11/25 7:38:14/

目录

①基准法

②并查集

③逆向思维之标记环边

④并查集压缩路径


①基准法

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

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

如图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.ppmy.cn/news/779490.html

相关文章

C语言练习--英尺制转米制程序

#include <stdio.h>int main() {printf("请输入英尺制身高&#xff1a;");double foot 0;double inch 0;scanf("%lf %lf",&foot,&inch);//一尺等于12英寸 double meter (foot inch/12)*0.3048;printf("身高为&#xff1a;%f米"…

7-1 厘米换算英尺英寸 (C语言)

如果已知英制长度的英尺foot和英寸inch的值&#xff0c;那么对应的米是(footinch/12)0.3048。现在&#xff0c;如果用户输入的是厘米数&#xff0c;那么对应英制长度的英尺和英寸是多少呢&#xff1f;别忘了1英尺等于12英寸。 输入格式&#xff1a; 输入在一行中给出1个正整数…

C语言程序——米到英尺的转换

为学日益&#xff0c;为道日损 文章目录 前言一、米和英尺如何换算二、程序编写2.运行结果 总结 前言 C语言书写格式遵循的规范&#xff1a; 源程序通常使用小写字母&#xff0c;符号常量和特殊用途符号才大写。 一条语句占一行&#xff0c;也可多条语句占一行。 程序空行不影…

Java英尺和米的转换

英尺和米的转化公式&#xff1a; 1英尺12英寸0.3048米 方法一&#xff1a; int foot; int inch; Scanner in new Scanner(System.in);foot in.nextInt(); inch in.nextInt();System.out.println("foot"foot",inch"inch);System.out.println((footinch/…

7-2 厘米换算英尺英寸 (C语言)

如果已知英制长度的英尺foot和英寸inch的值&#xff0c;那么对应的米是(footinch/12)0.3048。现在&#xff0c;如果用户输入的是厘米数&#xff0c;那么对应英制长度的英尺和英寸是多少呢&#xff1f;别忘了1英尺等于12英寸。 输入格式&#xff1a; 输入在一行中给出1个正整数&…

用java写米与英尺,JAVA 英制变换(英尺与英寸)

JAVA 英制转换(英尺与英寸) 系统实际业务中&#xff0c;还有些外国船舶水尺用的英制计算单位。 写了一个简单的 英尺与英寸的转换 。 英尺 --> 英寸 的转换&#xff1a; public class FeetToInch { /** * param args */ public static void main(String[] args) { String a…

英尺C语言,C语言中关于英尺、英寸、厘米的换算

(footinch/12)*0.3048 cm / 100 footinch/12 cm / (100 * 0.3048) cm / 30.48 因为1foot 12inch&#xff0c;所以inch / 12 < 1&#xff0c;所以foot cm/30.48的整数部分 inch / 12 cm/30.48的小数部分。 六七行就是完成这个功能。 扩展资料&#xff1a; 一、英尺和英…

github上的typescript项目,如何运行

github上的typescript项目&#xff0c;如何运行&#xff08;简单项目都可以&#xff09; 环境 1.Ubuntu22.04 2.node js 14以上 安装notjs 14 以上 { sudo npm cache clean -f sudo npm install -g n sudo n stable // 安装最新stable版本 sudo n latest //安装最新版本 } 3.安…