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

并查集有三种操作:

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
    评论
最大连通子图可以使用并查集解。 首先,将每个节点都初始化成一个独立的集合,然后遍历所有边。对于每条边,如果该边所连接的两个节点不属于同一个集合,就将它们合并到同一个集合中。最终,所有节点都被划分到不同的集合中,每个集合就代表着一个连通子图。 为了找到最大连通子图,我们可以按照集合大小从大到小排序,然后依次将每个集合中的节点加入到图中,直到加入一个节点会导致图不再连通为止。这样就可以得到最大的连通子图。 以下是一个简单的 Python 代码实现: ```python class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.size = [1] * n def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): px, py = self.find(x), self.find(y) if px == py: return if self.size[px] < self.size[py]: px, py = py, px self.parent[py] = px self.size[px] += self.size[py] def max_connected_component(n, edges): uf = UnionFind(n) for x, y in edges: uf.union(x, y) components = [[] for _ in range(n)] for i in range(n): components[uf.find(i)].append(i) components.sort(key=len, reverse=True) result = [] for component in components: if not component: break if len(result) + len(component) > n: break result.extend(component) return result ``` 其中,`UnionFind` 类实现了并查集数据结构,`max_connected_component` 函数接受节点数 `n` 和边列表 `edges` 作为输入,返回最大连通子图的节点列表。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值