图论之寻找桥边

news2024/9/21 4:41:06

目录

①基准法

②并查集

③逆向思维之标记环边

④并查集压缩路径


①基准法

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

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

如图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.coloradmin.cn/o/733466.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FFmpeg5.0源码阅读—— av_read_frame

摘要&#xff1a;本文主要描述了FFmpeg中用于打开编解码器接口av_read_frame的具体调用流程&#xff0c;详细描述了该接口被调用时所作的具体工作。   关键字&#xff1a;ffmpeg、av_read_frame   读者须知&#xff1a;读者需要了解FFmpeg的基本使用流程&#xff0c;以及一…

JVM理论(二)类加载子系统

类加载流程 类加载流程 类加载器子系统负责从文件系统或者网络中加载class文件,class文件的文件头有特定的文件标识(CAFEBABE是JVM识别class文件是否合法的依据)classLoader只负责文件的加载,而执行引擎决定它是否被执行加载类的信息存放在运行时数据区的方法区中,方法区还包括…

【终端增强工具】这次,我把Terminal(终端)也接入ChatGPT了...

大家好&#xff0c;我是萌新程序员豆小匠。 为terminal&#xff08;终端&#xff09;增加自定义命令这个想法从开始学编程的时候就有了&#xff0c;但是一直没有付诸行动。 这次&#xff0c;终于抽时间完成了&#xff0c;且代码开源&#xff01; 实现的功能 先说下实现的功能…

Idea社区版创建SpringBoot

一 下载Spring Initalizr and Assistant插件 选择左上角的File->Settings->Plugins&#xff0c;在搜索框中输入Spring&#xff0c;出现的第一个Spring Boot Helper插件&#xff0c;点击Installed&#xff0c;下载插件。&#xff08;这里已经下载&#xff09; 二 创建Spr…

Python学习笔记【01-基础语法】

文章目录 第一章输入输出print转义字符字符编码标识符和保留字变量数据类型整形浮点型布尔类型字符串类型数据类型转换 注释 第二章input()函数运算符算术运算符赋值运算符比较运算符布尔运算符(逻辑运算符)位运算符运算符的优先级 第三章顺序结构对象的布尔值单分支结构双分支…

dSYM文件是什么 ?

Overview 概述 dSYM的全称是debug symbol, 所以dSYM文件就是debug symbol (dSYM) file, dSYM文件中的每个 debug symbol, 对应了一个关系, 这个关系一端是源代码里真实的符号(类名称、全局变量以及方法和函数名称), 另一端则是其定义的文件号和行号. Apple Documentation 苹果…

word批量替换时使用通配符

替换这个好操作&#xff0c;打开替换功能&#xff1a; 弹出窗口中分别输入要替换的对象和替换的结果&#xff1a; 这里主要介绍特殊的替换&#xff1a; 可以看到点击“更多”之后&#xff0c;会出现下面的选项 像是【区分大小写】、【全字匹配】、【同音】、【查找单词的所有形…

三张表学会MySQL的单表操作!

表单一信息 1、查询表中所有学生的信息 mysql> select * from student; 2、 查询表中所有学生的姓名和英语成绩 mysql> select name,english from student; 3、过滤表中的重复数据 mysql> select DISTINCT * from student; 4、统计每个学生的总分 mysql> sele…

大语言模型高效训练基础知识:优化器AdamW和Adafator

Prerequsite:Adam优化算法 Adam优化算法很长一段时间都是比较主流的参数更新算法&#xff0c;也有很多变种&#xff0c;本文介绍在大模型训练过程中使用的AdamW和Adafator AdamW 原论文&#xff1a;Decoupled Weight Decay Regularization AdamW指的是Adam Weight Decay&#…

Mysql中默认自动事务autocommit关闭和开启方式、rollback回滚恢复数据的使用方法

文章目录 autocommit自动提交事物一、查看autocommit状态二、修改autocommit 状态的方式1、第一种方式2、修改mysql配置文件my.cnf 二、rollback回滚1、autocommit 开启1、autocommit 关闭 autocommit自动提交事物 MySQL 默认开启事务自动提交模式&#xff0c;每条 SOL 语句都…

深度神经网络知识蒸馏算法基础理论

知识蒸馏作为一种压缩方法&#xff0c;与剪枝、量化中直接在原模型上进行参数的剪枝或数据位宽的降低来压缩不同&#xff0c;知识蒸馏方法往往是通过将大模型上的精度转移到一个相对更小的模型上来完成对大模型的压缩。此处所说的大模型即知识蒸馏中的教师模型&#xff0c;而相…

【C语言】深入学习数组

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前正在回炉重造C语言&#xff08;2023暑假&#xff09; ✈️专栏&#xff1a;【C语言航路】 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你…

进程间通信方法——匿名管道

什么是管道&#xff1f; 管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道” 什么是匿名管道 就是一个没有名字的管道。 #include <unistd.h> 功能:创建一无名管道 原型 int pipe(int fd[2]); 参数 fd&#xff1a;文…

韩信谋反解密-使用命名空间

开机故事&#xff1a; 另一个韩信 什么是命名空间 c里面也有这种困扰&#xff0c;名字的冲突&#xff0c; 特意使用命名空间&#xff0c;开发的时候都要给函数命名 就会有名字相同的时候。这样就会出现混乱 区分相同名字相同函数的这样一个功能 用法1. #include<string…

openGauss学习笔记-04 openGauss极简版单机主备安装部署

文章目录 openGauss学习笔记-04 openGauss极简版单机主备安装部署4.1 获取安装包4.1.1 下载对应平台的安装包4.1.2 解压安装包4.1.3 查看目录结构 4.2 准备软硬件安装环境4.2.1 硬件环境要求4.2.2 软件环境要求4.2.3 软件依赖要求 4.3 单机主备安装部署4.3.1 安装前准备4.3.2 单…

【网络安全带你练爬虫-100练】第11练:xpath快速定位提取数据

目录 一、目标1&#xff1a;使用etree解析数据 二、目标2&#xff1a;使用xpath爬取指定数据 三、目标3&#xff1a;提取指定数据 四、网络安全小圈子 一、目标1&#xff1a;使用etree解析数据 其余的不用过多介绍&#xff0c;前面的练习都给大家已经过了一遍 def get_page…

【沐风老师】3DMAX砖石墙地面生成工具插件使用方法详解

3dMax砖石墙地面生成工具插件&#xff0c;收集了一些用于创建石墙、石头路面和不规则石头图案的实用工具&#xff0c;以模拟墙壁和地面。脚本会自动烘焙法线贴图、AO贴图和高度贴图以供实时使用。 【主要特点】 1.可以生成真实的石墙、地面、不规则石块及石灰墙面&#xff0c;是…

4.5 x64dbg 探索钩子劫持技术

钩子劫持技术是计算机编程中的一种技术&#xff0c;它们可以让开发者拦截系统函数或应用程序函数的调用&#xff0c;并在函数调用前或调用后执行自定义代码&#xff0c;钩子劫持技术通常用于病毒和恶意软件&#xff0c;也可以让开发者扩展或修改系统函数的功能&#xff0c;从而…

oracle启动/关闭/查看监听+启动/关闭/查看数据库实例命令

启动oracle第一步启动监听&#xff0c;第二步启动数据库实例 &#xff08;1&#xff09;输入su oracle进入oracle用户状态 &#xff08;2&#xff09;这里的密码是你的root密码 1 启动/关闭/查看监听命令 &#xff08;1&#xff09;启动监听—— lsnrctl start &am…

C#学习之路-封装

封装 被定义为"把一个或多个项目封闭在一个物理的或者逻辑的包中"。在面向对象程序设计方法论中&#xff0c;封装是为了防止对实现细节的访问。 抽象和封装是面向对象程序设计的相关特性。抽象允许相关信息可视化&#xff0c;封装则使开发者实现所需级别的抽象。 C…