图论之寻找桥边

这篇具有很好参考价值的文章主要介绍了图论之寻找桥边。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

①基准法

②并查集

③逆向思维之标记环边

④并查集压缩路径

主函数读取文件调函数方法处理


①基准法

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

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

如图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 路径压缩文章来源地址https://www.toymoban.com/news/detail-568943.html

主函数读取文件调函数方法处理

#include <iostream>
#include<fstream>
#include"benchmark.h"
#include"disjoint.h"
#include"lowestCommonAncestor.h"
#include<chrono>
using namespace std;
int main() {

//    fstream file(R"(C:\Users\Yezi\Desktop\C++\Bridge\small.txt)");
//    fstream file(R"(C:\Users\Yezi\Desktop\C++\Bridge\mediumDG.txt)");
    fstream file(R"(C:\Users\Yezi\Desktop\C++\Bridge\largeG.txt)");
    if(!file.is_open()){
        cout<<"File Open Error!"<<endl;
        return 0;
    }
    int edgeNumber;
    int vertexNumber;
    int head;
    int tail;
    file>>vertexNumber>>edgeNumber;
//    Benchmark::Map map(edgeNumber,vertexNumber);
//    Disjoint::Map map(edgeNumber,vertexNumber);
    LCA::Map map(edgeNumber,vertexNumber);
    while(!file.eof()){
        file>>head>>tail;
        map.addEdge(head,tail, true);
    }
    auto start=chrono::high_resolution_clock::now();
    map.findBridge();
    auto end=chrono::high_resolution_clock::now();
    auto consume=chrono::duration_cast<chrono::microseconds>(end-start);
    cout<<consume.count()<<"microseconds"<<endl;
    map.showBridge();
    return 0;
}

到了这里,关于图论之寻找桥边的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 图论之邻接表

    路径规划算法综述 图论基础介绍 图论之邻接矩阵 目录  路径规划系列文章目录 一、邻接表 二、邻接表实现 2.1 链式前向星实现 2.2 链表实现 三、总结         由于对于稀疏图来说,使用邻接矩阵进行存储显然会使得空间利用率较低,因此利用邻接表来存储图,可以克服

    2024年02月04日
    浏览(39)
  • C++图论之强连通图

    什么是连通性? 连通,字面而言,类似于自来水管道中的水流,如果水能从某一个地点畅通流到另一个地点,说明两点之间是连通的。也说明水管具有连通性,图中即如此。 无向图和有向图的连通概念稍有差异。 无向图连通性 如果任意两点间存在路径,称此图具有连通性

    2024年02月03日
    浏览(37)
  • 图论之dfs与bfs的练习

    dfs--深度优选搜索 bfs--广度优先搜索 迷宫问题--dfs 问题: 给定一个n*m的二维迷宫数组其中S是起点,T是终点,*是墙壁(无法通过), .是道路 问从起点S出发沿着上下左右四个方向走,能否走到T点?能输出\\\"YES\\\",否则输出\\\"NO\\\"。 8 8 *****... *.S...** *****.** *****..* *T..**.* .**.**.* ..*...

    2024年02月20日
    浏览(30)
  • [数学建模]图论之最短路径问题

    目录 一、引入图论  二、图的基本概念与数据结构 1.基本概念  2.图与网络结构 1.邻接矩阵表示法  2.稀疏矩阵表示法 三、最短路径问题 1、迪杰斯特拉(Dijkstra)算法 2、贝尔曼-福特(Bellman-Ford)算法 3、弗洛伊德(Floyd)算法         图论起源于18世纪,近几十年来,计

    2024年02月06日
    浏览(46)
  • 8.3.1 蓝桥杯图论之Floyd&Dijkstra

    在蓝桥杯等算法竞赛中,图论问题占据了重要地位,其中路径查找是一个经常出现的主题。Floyd-Warshall算法和Dijkstra算法是解决图中最短路径问题的两种经典算法。本篇博客将介绍这两种算法的原理和实现,以及它们的应用场景。 Floyd-Warshall算法是一种计算图中所有最短路径的

    2024年02月19日
    浏览(35)
  • U4_1:图论之DFS/BFS/TS/Scc

    由点(vertices)和边(edges)组成 G = ( V , E ) G=(V,E) G = ( V , E ) , ∣ V ∣ = n |V|=n ∣ V ∣ = n , ∣ E ∣ = m |E|=m ∣ E ∣ = m (这里默认有向图,无向图用 G G G = = = { V V V , E E E }表示 顶点的度是关联在其上的边的数量。满足 ∑ d e g r e e ( v ) = 2 ∣ E ∣ sum degree(v)=2|E| ∑ d e g ree ( v ) = 2∣

    2024年02月05日
    浏览(36)
  • 图论算法基础:单源最短路径Dijkstra算法分析

    在 有向带权图 中给定一个起始顶点(源点),Dijkstra算法可以求出 所有其他顶点 到源点的最短路径,Dijkstra算法 不能用于同时含有正负权值的边的图 Source 顶点集合:已经确定 到源点的最短路径 的顶点就会加入 Source 集合中, Source 集合初始时只有源点 dist 数组:用于记录每个顶点到

    2024年02月11日
    浏览(42)
  • 代码随想录| 图论04 查并集 ●查并集理论知识 ●1971.寻找图中是否存在路径 ●684.冗余连接 ●685.冗余连接II

    #查并集理论知识   并查集用处:解决连通性问题 将两个元素添加到一个集合中。 判断两个元素在不在同一个集合 思路:将三个元素A,B,C (分别是数字)放在同一个集合,其实就是将三个元素连通在一起,如何连通:只需要用一个一维数组来表示,即:father[A] = B,fathe

    2024年02月16日
    浏览(42)
  • 【算法分析与设计】算法概述

    数据结构+算法(+设计模式)=程序   理解算法的概念。   掌握算法的计算复杂性概念。   掌握算法复杂性的渐近性态的数学表述。   了解NP类问题的基本概念。   顾名思义,计算(求解)的方法   算法(Algorithm):对特定问题求解步骤的一种描述,是 指令的有

    2024年02月07日
    浏览(36)
  • 算法设计与分析--迭代算法

    一、迭代算法简介 二、设计工作步骤 三、迭代--递推法 题目及运行 四、迭代--倒推法 题目及运行 五、总结 算法语言--C语言 迭代算法也称 “辗转法” ,是一种不断用变量的 旧值递推出新值 的解决问题的方法。 迭代算法一般用于数值的计算,是读者早就熟悉的一种算法策

    2024年02月09日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包