并查集-集合-求无向图的所有连通子图

并查集有三种操作:

1>make(x).用于初始化集合,将每个元素的父节点设置为他本身。即表示当前一个元素为一个集合,互相没有联系

2>union(x,y)合并x,和y所在的集合。即把y的代表设置为x所在集合的代表。

3>find(x),;返回x所在集合的代表。

并查集——求无向图的所有连通子图
  求解无向图的连通子图,有两种方法,一种是DFS或BFS,也就是对图遍历,另一种方法就是使用并查集。对图的遍历非常常见,而并查集的概念就不如遍历那么熟悉。其实如果仅是找连通子图,用DFS对所有节点遍历一遍就可以,而用并查集则需要遍历两遍。我们不考虑算法效率问题,仅仅是通过这个问题让我们对并查集有所认识,并了解其原理,下面主要说一下并查集。 
  首先说一下,并查集是一种设计非常好的数据结构,也是一种检索算法。说它是数据结构,因为使用并查集的最终结果是生成一个森林,里面包括一个或多个树。说它是一种算法,是因为通过并查集,我们很容易判断图中任意两个节点是否具有连通性,同时也可以求解出所有连通子图,也就是即将说到的内容。

问题分析
  现在来看问题:求无向图的所有连通子图,可以分解成两步,首先,将所有连通的节点,都放到一起,最终分成几个连通分量组;然后,找出属于同一连通分量组的节点及边,也就是各个连通子图。 
  第二步非常容易,假设第一步生成一个字典,里面存放着所有节点所对应的连通分量组号,那么再对原有的图遍历一遍,从字典中查出节点所属的组并且根据组进行区分,就可以得到所有连通子图。查字典的复杂度是O(1),没有什么计算开销。那么问题主要变为第一步中该如何生成那个字典。 
   
  看上面这4个点,c1、c2有连接,我们需要在字典里建立两个值,{c1:Gc},{c2:Gc},Gc代表他们的连通分量组号,这样通过分别查找c1的组号,c2的组号,所得结果一样,我们可以判断出c1、c2属于同一组,具有连通性。同样的,我们在字典中又加入了两个值{c3:Gc},{c4:Gc},我们可以很easy的知道,c3和c4属于同一个连通分量,具有连通性,虽然它们之间没有直接的边相连。对于b开头的3个点,我们在字典中加入它们的组号{b1:Gb},{b2:Gb},{b3:Gb}。根据字典,我们知道,c3和b3不属于同一组,它们之间不连通。 
  那么问题来了,我们怎么设置字典中的那个组号呢?我认为这就是并查集的精髓所在。

实现过程
  1、把节点编号当做组号。 
  因为要建立字典,我们需要对整个图扫一遍,使字典中的内容覆盖到每一个节点。 
  上面的那个组号Gc和Gb是人为造的,实际中我们需要按照规则设定组号。 
  这个规则的基本思想就是,选择节点编号当做组号: 
  a) 因为一条边有两个节点,选择一条边任意一个节点编号当做这条边上两个节点的组号; 
  b) 如果字典中已经有了节点的组号,那么选择字典中的,如果没有,则按照上面规则选择节点组号; 
  对于上述的图,我们按照(c1,c2)–>(c1,c4)–>(c2,c3)–>(b1,b2)–>(b2,b3)的顺序扫这个图。按照上面的规则,假设都选择小编号作为组号, 
  扫到第1条边(c1,c2)时,建立字典 {c1:c1, c2:c1}, 
  扫到第2条边(c1,c4)时,建立字典 {c1:c1, c2:c1, c4:c1}, 
  扫到第3条边(c2,c3)时,建立字典 {c1:c1, c2:c1, c3:c2, c4:c1}, 
  扫到第4条边(b1,b2)时,建立字典 {c1:c1, c2:c1, c3:c2, c4:c1, b1:b1, b2:b1}, 
  扫到第5条边(b2,b3)时,建立字典 {c1:c1, c2:c1, c3:c2, c4:c1, b1:b1, b2:b1, b3:b2}。 
  2、找组号的组号,直到找到祖先。 
  在第一步中我们初步建立了一个字典,里面包括每一个节点。可以看到,节点c3的组号为c2,和节点c1、c2、c4不一致。按照当前的结果,c3被认定为与其它3个c节点都不连通。所以目前还没有完成分组。为了解决这个问题,当我们再次遍历字典时,需要对每个节点得到的组号再次进行寻找组号的操作,直到得到的组号是它自己。对应的代码就是:

def find(key):
    while parent[key] != key:  //parent就是得到的那个字典
        key = parent[key]
    return key
  代码非常简短,对于c3来说,从字典中得到组号是c2,和c3不等,那就继续找c2的组号,得到c1,和c2还不等,那就找c1的组号,这次得到c1,和自身相等,返回c1,也就是c3的组号。经过不断的查找,终于c3和其它c节点拥有了相同的组号,被划分为了一组。 
  3、压缩路径 
  从第2步中看到,为了找c3的组号,有点费劲啊,先找到c2,再找到c1,如果下次还想找c3的组号,还需要这么折腾一次。能不能一下就给出c3的组号是c1呢?没问题,当我们用find方法找c3的时候,得到的组号不是自己的编号,那么我就知道c3的组号一定是它的组号c2的组号,那么我们就把c3的组号直接设定为c2的组号就可以了。只需要在find中改一行就OK了。

def find(key):
    while parent[key] != key:  //parent就是得到的那个字典
        parent[key] = parent[parent[key]]  //把c3的组号设定为c2的组号
        key = parent[key]
    return key
  4、合并家族 
  比如我们的图又扩大了,在原有基础之上有加了几个节点。如图: 
  
  新加了两条边,(c5, c6),(c5, c1),按照这个顺序,当扫完(c5, c6)时,会在字典中加入{c5:c5, c6:c5}两个值。再扫到(c5, c1)时,由于c5有自己的组号,c1也有自己的组号,各自为营,但它俩又有连接。一山不容二虎,那就选一个当老大。在这里就是随便选一个作为另一个的组号。咦,这个怎么又回到的1的问题。。。对的,其实1和4是一个问题,只是1是初始化时的选择方式,4是遍历到中途过程中的选择方式,它俩合并到一起的代码就是:

def init(key):
    if key not in parent:
        parent[key] = key

def join(key1, key2):
    p1 = find(key1)
    p2 = find(key2)
    if p1 != p2:
        parent[p2] = p1
  当我们对图的边进行遍历时,就先执行init,看节点是否有组号,没有组号就赋值为自己。再执行join,合并边上的两个节点为同一个组。 
  假设选c1的组号作为c5的组号,那么目前的字典内容就如下图所示: 
  {c1:c1, c2:c1, c3:c1, c4:c1, c5:c1, c6:c5, b1:b1, b2:b1, b3:b2}。 
  对应的数据结构就是: 
   
  这就是我们最终得到的结果,一个森林,里面包括了两棵树,每棵树代表连通的节点组,树中节点的父节点代表自己的组号。 
  根据森林中的树,我们就可以找到图中的各个连通子图了,当然,还需要根据这个森林中的内容,也就是我们得到的字典,再遍历一遍图,找到各个连通子图的边,不过已经完美解决了需要的问题。 
  再回过头来看,其实代码非常的简短,就三个函数,完整代码如下: 
  

代码
class UnionSet(object):
    def __init__(self):
        self.parent = {}

    def init(self, key):
        if key not in parent:
            self.parent[key] = key

    def find(self, key):
        self.init(key)
        while self.parent[key] != key:  
            self.parent[key] = self.parent[self.parent[key]] 
            key = self.parent[key]
        return key      

    def join(self, key1, key2):
        p1 = self.find(key1)
        p2 = self.find(key2)
        if p1 != p2:
            self.parent[p2] = p1
应用
  除了求图的连通子图(连通分量)可以用到并查集外,在用Kruskal方法求图的最小生成树,也用到了并查集,掌握了并查集,那么再去看Kruskal的方法,就会轻而易举了。
--------------------- 
作者:wangyibo0201 
来源:CSDN 
原文:https://blog.csdn.net/wangyibo0201/article/details/51998273 
版权声明:本文为博主原创文章,转载请附上博文链接!

 

import java.util.Scanner;
public class Bingchaji {
    public static void main(String args[]){
        Scanner in=new Scanner(System.in);
        int n=in.nextInt();
        int m=in.nextInt();
        int father[]=new int[n];
        for(int i=0;i<n;i++){
            father[i]=i;
        }
        for(int i=0;i<m;i++){
            int x=in.nextInt();
            int y=in.nextInt();
            combine(x-1,y-1,father);
        }
        int count=0;
        for(int i=0;i<n;i++){
           if(father[i]==i)
               count++;
        }
        System.out.println(count-1);
    }
    public static int Find(int x,int father[]){
        while(father[x]!=x){
            x=father[x];
        }
        return x;
    }
    public static void combine(int x,int y,int father[]){
        int fx=Find(x,father);
        int fy=Find(y,father);
        father[fx]=fy;
    }
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要获得网络的最大连通子图,你可以使用称为 "Kosaraju 算法" 的算法。它的基本思想是: 1. 使用深度优先搜索算法出图中的所有强连通分量。 2. 将图的所有边反向,再出所有的强连通分量。 3. 最大的强连通分量就是原图的最大连通子图。 下面是具体的步骤: 1. 使用深度优先搜索算法出图中的所有强连通分量。 2. 将图的所有边反向,再出所有的强连通分量。 3. 最大的强连通分量就是原图的最大连通子图。 ### 回答2: 要获得网络的最大连通子图,首先需要了解网络的拓扑结构以及节点之间的连接关系。以下是一种方法,以获得无向图的最大连通子图为例: 首先,我们可以使用深度优先搜索算法(DFS)或广度优先搜索算法(BFS)来遍历整个图。从某个节点开始,通过搜索与该节点直接或间接相连的所有节点,并将它们标记为已访问。再选择另一个未访问的节点作为起始点,重复此过程,直到所有节点都被访问。 在搜索的过程中,我们记录每个连通子图的节点数,并使用一个变量来保存当前最大的连通子图节点数以及对应的子图。在搜索完成后,最大连通子图就是记录的节点数最多的连通子图。 如果需要获得有向图的最大连通子图,可以先将有向图转换为无向图。具体做法是遍历有向图中的所有边,将每条边的起始节点和结束节点之间增加一条双向边。之后再按照上述方法寻找无向图的最大连通子图。 除了上述方法,还有其他算法可以用于获得网络的最大连通子图,如最小生成树算法、Kruskal算法和Prim算法等。根据实际情况和需选择适合的算法。 总之,获得网络的最大连通子图可以通过搜索算法遍历网络节点,并记录每个连通子图的节点数来获得。具体选择哪种算法取决于网络的性质和需。 ### 回答3: 获得网络的最大连通子图的方法有一下几种: 1. BFS(广度优先搜索)算法:从一个节点开始,利用广度优先搜索的方式逐层遍历网络中的节点,并将遍历到的节点标记为已访问。通过这种方式,可以获得从起始节点出发可到达的所有节点,即最大连通子图。 2. DFS(深度优先搜索)算法:从一个节点开始,利用深度优先搜索的方式递归地遍历网络中的节点,并将遍历到的节点标记为已访问。通过这种方式,同样可以获得从起始节点出发可到达的所有节点,即最大连通子图。 3. 并查集算法:首先初始化每个节点为一个独立的集合,然后依次遍历网络中的边,将边连接的两个节点合并成为一个集合,直到遍历完所有边。最终,每个集合中的节点即为最大连通子图中的节点。 4. 最小生成树算法:使用最小生成树算法(如Prim算法或Kruskal算法)可以获得连接网络中所有节点的最小权重的边集合,这些边所连接的节点即为最大连通子图中的节点。 需要注意的是,网络中可能存在多个最大连通子图,以上方法得到的是其中之一。如果需要找到所有最大连通子图,可以通过对网络中的所有节点逐个应用以上方法,并使用递归或迭代的方式来获取所有的最大连通子图

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值