并查集求无向图的桥

发布于 2020-06-14 01:46:02   阅读量 63  点赞 0  

一、问题描述

1. 桥的定义

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


2. 求解问题

 找出一个无向图中所有的桥。


3. 算法

  1. 基准算法
    For every edge (u, v), do following
    a) Remove (u, v) from graph
    b) See if the graph remains connected (We can either use BFS or DFS)
    c) Add (u, v) back to the graph.

  2. 应用并查集设计一个比基准算法更高效的算法。不要使用Tarjan算法。


4. 数据集


 由数据集可知数据:

  • 提供了图的节点数量、边数量、各边

  • 无重边

  • 有规律,边的格式为<编号较小点,编号较大点>



二、算法

1. 思路

 这里通过计算图中构成环的边的数量,来计算桥的数量。因为桥不在任何环内,故用无向图边数 - 环边数 = 桥即可得出无向图桥的数量。

 首先,假设图中没有边,各个点都是独立的,在遍历文件的时候逐渐在图中添加边,同时建立并查集生成树。在建立生成树时,使用并查集来判断边的两个节点是不是属于同一棵生成树(一个并查集对应一棵生成树):

  • 若两个节点不属于同一棵生成树,则合并两个生成树,并合并对于的两个并查集。

  • 若两个节点属于同一棵生成树,则添加此边将导致图中形成环,此时求出环边数量;


2. 并查集

 并查集引入了权重机制路径压缩,来提高查找操作的效率。


①建立并查集

 使用并查集的union操作来合并两个并查集,在合并时将权值较小的集合合并到权值较大的集合,能够有效地提高查找操作的效率(其中使用一个数组weight来保存某集合下属点的个数)。

void Union(int m, int n) {
    int p = Find(m);
    int q = Find(n);
    if(weight[p]>weight[q]){
        set[q] = p;
        weight[p] += (weight[q] + 1);       // 更新权值
    }else{
        set[p] = q;
        weight[q] += (weight[p] + 1);       // 更新权值
    }
}


②查找操作

 向上回溯直到顶点(即以集合标识等于自己的点)

int Find(int p) {
    while(set[p]!=p){   // set[m] = 0,即某个元素的父节点为其本身
        p = set[p];
    }
    return p;
}


③路径压缩

 在查找操作时,将查找点的所属集合设置为查找结果:

如:


在对节点 9 进行一次查找操作(路径压缩)后:

/* 在逐层往上查找时,要减去对应节点的权值 */
int Find(int p) {
    while(set[p]!=p){   // set[m] = 0,即某个元素的父节点为其本身
        if(set[set[p]]!=set[p]){        // 且若父亲节点等于祖父节点,说明当前节点的父亲节点是根节点,此时不能直接减去
            weight[set[p]] -= (weight[p]+1);   // 将当前点的父亲节点权重减掉当前节点的权重
        }
        set[p] = set[set[p]];         // 将当前点的父亲节点改为其祖父节点
        p = set[p];
    }
    return p;
}



3. 生成树

 使用数据结构:

struct ChildNode{
    int position;
    ChildNode* next;
    ChildNode():position(-1),next(nullptr){}
    explicit ChildNode(int position):position(position),next(nullptr){}
};

class TreeNode{
public:
    int father;
    int depth;
    ChildNode *firstChild;
    TreeNode();
    ~TreeNode();
    void addChild(int position);
    void removeChild(int position);
};


①合并生成树

 在合并生成树时,会将较小的生成树合并到较大的生成树上(提高效率),且在合并的时候需要反转较小树的父子关系,并更新节点的深度(在生成树中回溯时需要用到节点的深度)。

 生成树的大小通过并查集根节点的权值判断(因为weight标识的是并查集中某集合下属的点的多少,故可以用节点所属集合的根节点weight值来判断其所在生成树有多少个节点)。

合并时向上反转父子关系:


②回溯找环边

 若边的两个节点属于同一棵生成树(同一个并查集),则将该边加入图会产生环,此时需要从两个节点开始,向上回溯至公共祖先(LCA问题),然后记录经过的边,回溯经过的所有边都是环边:

如将边 <6,7> 加入图中,由于点6、7属于同一棵生成树,此时会在图中形成环,而其环边为:<6,7><4,7><1,2><1,3><3,6>:


环边重复问题:由于某些边可能存在于多个环中,故在回溯统计环边数量时,还需记录访问过的环边,避免重复计数。如下图中,将边 <5,6><6,7> 添加进图时,都会形成环,且两个环拥有公共边 <1,2><1,3><3,6>



Last Modified : 2020-09-06 21:40:09