图中连通块的个数:并查集

图的连通性与并查集

图的连通性问题

在地图上有若干城镇(点),已知所有有道路直接相连的城镇对。要解决整幅图的连通性问题。比如,随意给你两个点,让你判断它们是否连通;或者问你整幅图一共有几个连通块,也就是被分成了几个互相独立的块。
修路工程问题会问还需要修几条路才能将所有城镇连通起来,实质就是求有几个连通块。如果只有1个连通块,说明整幅图上的点都连起来了,不用再修路了;如果是2个连通块,则只要再修1条路,从两个分支中各选一个点,把它们连起来,那么所有的点都连通了;如果是3个连通块,则只要再修2条路……
所以,若存在n个连通块,只要修n-1条路,就能把所有点连通。
实际给定的城镇有几百个,路有若干条,而且可能存在回路。 这时就要用到并查集。
PS:
其实求连通子图个数,除了并查集,用DFS和BFS也能够实现。
DFS:每次从某一点出发,遍历完与它相连的所有点,子图数num+1;当遍历完所有点后,num即为所求。

并查集

并查集的概念

并查集由两个函数(并、查函数)和一个整型数组(集)构成。

  • 函数join是合并;
  • 函数find是查找;
  • 数组pre记录了每个点的前导点是什么;

并查集的局限及改进

单纯的并查集只能保证得到独立子图的个数,但是,(即使经过了路径压缩)它不能保证同属于一个独立子图的节点的前导节点都相同。
在这种情况下,如果还要判断两个节点是否属于同一个子图,还要再进行一次find(i)操作。
换句话说,经过一次对每个节点的遍历的find()操作,可以保证同一子图中的节点的前导节点都相同。

并查集的实现

用一个有趣的方式解释并查集的实现:
江湖上的大侠有师父和徒弟、下属,构成了许多树结构。连通块的个数可以认为是门派的个数。
这里写图片描述

pre数组

pre[1000]这个数组记录了每个大侠的上级是谁。

pre[15]=3

表示15号大侠的上级是3号大侠。如果一个人的上级就是他自己,那说明他就是掌门了,查找到此为止。也有孤家寡人自成一派的,比如欧阳锋,那么他的上级就是他自己。每个人都只认自己的上级。比如胡青牛只知道自己的上级是杨左使,而不认识张无忌。要想知道自己的掌门是谁,只能一级级查上去。

find函数和路径压缩

路径压缩算法是指在每次查询上级的同时,都进行优化处理,所以整个树的层数都会维持在比较低的水平上。
这里写图片描述
find函数以及压缩路径可以用一个递归函数简单实现:

int find(int a){  
    if(pre[a]!=a)  
        pre[a]=find(pre[a]);//路径压缩,本结点更新为根结点的子结点  
    return pre[a];  
} 

join函数

再来看看join函数,就是在两个点之间连一条线,这样一来,原先它们所在的两个块的所有点就都连通了。
用pre[]数组,该如何实现连线呢? 很简单,将一个门派的掌门指定为另一个门派掌门的下属。

//让虚竹和周芷若做朋友
void join(int x,int y) {
    int fx=find(x),fy=find(y);//虚竹的老大是玄慈,周芷若的老大是灭绝
    //玄慈和灭绝显然不是同一个人
    //于是让前者成为后者的下属                                                  
    if(fx!=fy) pre[fx]=fy;                                                                        
}

完整代码实现

#include<iostream>  
using namespace std;  

int  pre[1050];     //保存节点的直接父节点

//查找x的根节点 
int find(int a){  
    if(pre[a]!=a)  
        pre[a]=find(pre[a]);//路径压缩,本结点更新为根结点的子结点  
    return pre[a];  
} 
//连接两个连通块
void join(int x,int y) {  
    int fx=Find(x),fy=Find(y);  
    if(fx!=fy) pre[fy]=fx;  
}   

int main() {  
    int N,M,a,b,i,j,ans=0;  
    while(scanf("%d%d",&N,&M) && N) {
        //初始化pre数组
        for(i=1;i<=N;i++) pre[i]=i;  
        //根据连通情况,构建pre数组
        for(i=1;i<=M;i++) {  
            scanf("%d%d",&a,&b);  
            join(a,b);
        }  

    for(i=1;i<=N;i++) 
        if(pre[i]==i) ans++; //计算连通子图的个数ans

    cout<<ans;
    return 0;  
}

求小岛个数

给定一个由1和0组成的二维字符数组,1代表陆地,0代表水。问被水包围的连通陆地区域的个数。
这题可以用DFS的递归着色来解,也可以用并查集来做。

class Solution {
public:
    vector<int> pre;
    int count=0;

    int numIslands(vector<vector<char>>& grid) {
        if(grid.size()==0 || grid[0].size()==0)
            return 0;

        int row = grid.size();
        int col = grid[0].size();
        pre.resize(row*col+1,0);

        //对pre数组进行初始化
        for(int i=0;i<row;i++)
            for(int j=0;j<col;j++)
            {
                int num = col*i+j;
                pre[num] = num;
            }

        //遍历图中的每个点
        for(int i=0;i<row;i++)
            for(int j=0;j<col;j++)
            {
                if(grid[i][j]=='1')
                {
                    int down=i+1,right=j+1;
                    if(down<row && grid[down][j]=='1')
                        join(col*i+j,col*down+j);
                    if(right<col && grid[i][right]=='1')
                        join(col*i+j,col*i+right);
                }
            }

        //再遍历一次,计算islands的个数
        int ans = 0;
        for(int i=0;i<row;i++)
            for(int j=0;j<col;j++)
            {
                int num = col*i+j;
                if(pre[num] == num && grid[i][j]=='1')
                    ans++;
            }
        return ans;
    }

    //并,将联通的点的pre设为同一个值
    void join(int x,int y){
        int fx=find(x);
        int fy=find(y);
        if(fx != fy)
            pre[fx] = fy;
    }

    //找到a的祖先,并且路径压缩
    int find(int a){
        if(pre[a] != a)
            pre[a] = find(pre[a]);
        return pre[a];
    }
};
<think>嗯,用户问的是如何统计并查集中每个连通分量的大小。首先,我需要回忆并查集的结构。并查集通常用父节点组来跟踪每个节点的根,还可能用到路径压缩和按秩合并来优化。那统计每个连通分量的大小,应该需要一个额外的组来记录每个根对应的分量大小。 用户提供的引用里,引用[1]提到了使用size组,初始化时每个节点的大小都是1,合并时将小树合并到大树下,同时更新size组。这说明size组在合并时会被更新。那具体是怎么操作的呢?比如,当两个根节点合并时,较大的根会作为父节点,而size组的值会被累加到父节点的size中。 比如,假设节点A的根是rootA,size[rootA] = 5;节点B的根是rootB,size[rootB] = 3。当合并时,如果rootB的size较小,就把rootB的父设为rootA,然后size[rootA] += size[rootB]。这样,每个连通分量的大小就保存在根节点的size组中。 用户可能想知道如何实现这个size组,以及在初始化、查找和合并时的具体步骤。需要详细说明如何初始化size组,在合并时如何判断大小,并更新对应的size值。同时,要确保路径压缩不会影响到size组的正确性,因为路径压缩只改变父节点,而size组只在根节点处维护分量的大小。 另外,用户可能还关心如何访问每个连通分量的大小。比如,当需要知道某个节点所在连通分量的大小时,可以通过find找到根节点,然后返回size[root]。这样就能正确获取该分量的大小了。 总结来说,解决方案是在并查集中维护一个size组,初始化为每个节点的大小为1。在合并时,根据两个根的大小决定合并方向,并更新size组。查找时使用路径压缩,不影响size组。这样每个连通分量的大小就可以通过根节点的size值获取。</think>### 统计并查集连通分量大小的实现方法 要统计并查集(Union-Find)中每个连通分量的大小,可以通过以下步骤实现: 1. **初始化阶段** 增加一个数组(如 `size[]`),初始时每个节点单独构成一个连通分量,因此每个节点的分量大小为1。 ```java size = new int[n]; Arrays.fill(size, 1); // 初始每个分量的节点为1 ``` 2. **合并操作优化** 在合并两个连通分量时,将较小分量的根节点连接到较大分量的根节点下,并更新分量大小: ```java public void union(int x, int y) { int rootX = find(x); int rootY = find(y); if (rootX != rootY) { if (size[rootX] < size[rootY]) { // 小树合并到大树下 parent[rootX] = rootY; size[rootY] += size[rootX]; // 更新合并后的分量大小 } else { parent[rootY] = rootX; size[rootX] += size[rootY]; } count--; // 总连通分量减1 } } ``` 3. **查询分量大小** 通过根节点直接访问 `size[]` 组: ```java public int getComponentSize(int x) { int root = find(x); // 找到该节点的根 return size[root]; // 根节点存储当前分量的大小 } ``` #### 关键点说明 - **路径压缩不影响大小统计** 路径压缩仅修改父节点指针,但分量大小始终由根节点的 `size[root]` 维护,因此不影响统计结果[^1]。 - **时间复杂度保持近似O(1)** 路径压缩和按大小合并的优化使操作时间复杂度接近常级别[^3]。 #### 示例场景 假设初始时有5个节点,每个 `size[i] = 1`。合并节点0和1后,它们的根节点大小变为2;再合并节点2和3,根节点大小变为2。最后合并根0和根2时,较大分量会吸收较小分量,最终根节点大小更新为4[^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值