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

图的连通性问题

本文为转载,点击查看原文

在地图上有若干城镇(点),已知所有有道路直接相连的城镇对。要解决整幅图的连通性问题。比如,随意给你两个点,让你判断它们是否连通;或者问你整幅图一共有几个连通块,也就是被分成了几个互相独立的块。
修路工程问题会问还需要修几条路才能将所有城镇连通起来,实质就是求有几个连通块。如果只有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
1
表示15号大侠的上级是3号大侠。如果一个人的上级就是他自己,那说明他就是掌门了,查找到此为止。也有孤家寡人自成一派的,比如欧阳锋,那么他的上级就是他自己。每个人都只认自己的上级。比如胡青牛只知道自己的上级是杨左使,而不认识张无忌。要想知道自己的掌门是谁,只能一级级查上去。

find函数和路径压缩

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


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

求小岛个数

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

class Solution {
public:
vector 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];
}

};

并查集中统计集合的数量通常是利用了这样一个事实:每一个独立的集合都有一个唯一的代表元(即该集合中所有元素通过`find`操作最终指向的那个根节点)。因此,我们可以借助维护父指针数组的同时引入计数机制,以便随时了解当前存在的不同集合数目。 ### 实现思路 1. 初始状态下,假设一共有 N 个互不相同的单元素集合,则初始集合总数设为 `count=N`. 2. 当执行一次有效的 UNION 操作时意味着把原本属于两个不同集合的东西连接起来形成了一个新的更大集合,此时总集合数应该减去1 (`count--`) 因为我们少了一个独立的小集合而多了这个大一点的新组合. 3. FIND 函数保持不变继续负责定位任一成员所在的具体哪个集合. 以下是一个稍微改进过的 Python 版本示例程序展示如何追踪和更新集合的数量: ```python class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.count = n # Initialize the number of disjoint sets as 'n' def find(self, p): root = p while root != self.parent[root]: root = self.parent[root] # Path compression for efficiency improvement while p != root: next_elem = self.parent[p] self.parent[p] = root p = next_elem return root def union(self, p, q): proot = self.find(p) qroot = self.find(q) if proot == qroot: return False self.parent[proot] = qroot self.count -=1 # Decrease count when two distinct components are united. return True def get_count(self): return self.count # Return current total number of connected components(disjoint sets). ``` 在这个版本里面增加了变量‘count’记录现有连通分支(也就是非空无环里的强联通块)的数量。每当我们成功地将两部分合并且之前他们是分离状态时候就削减全局counter值反映最新的状况变化情况。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值