并查集Union-Find

关于并查集,看了好多博客,我理解的并查集是一种数据结构,主要用于解决动态连通性一类问题。用于处理一些不相加集合的合并和查询问题。在使用中常常以森林来表示。 并查集也是用来维护集合的,和前面学习的set不同之处在于,并查集能很方便地同时维护很多集合。如果用set来维护会非常的麻烦。并查集的核心思想是记录每个结点的父亲结点是哪个结点。

参考博客:https://blog.csdn.net/dm_vincent/article/details/7655764

 

 

动态连通性

假设我们输入了一组整数对,即上图中的(4, 3) (3, 8)等等,每对整数代表这两个points/sites是连通的。那么随着数据的不断输入,整个图的连通性也会发生变化,从上图中可以很清晰的发现这一点。同时,对于已经处于连通状态的points/sites,直接忽略,比如上图中的(8, 9)。
 

就动态连通性这个场景而言,我们需要解决的问题是:

  •  给出两个结点,判断他们是否连通,如果连通,需要/不需要给出具体的路径。

还有的博客给出了地图的例子,判断两个地点是否连通,也很形象。

 

现在给出一组数据,其中每个元素都是一对“点”,代表这对点之间是联通的,我们需要设计一个算法,让计算机依次读取这些数据,最后判断出其中任意两点是否连通。注意,并查集所涉及的动态连通性只是考虑“是否连通”这一二值判别问题,而不涉及连通的路径到底是什么。后者不在本文的考虑范围之内。

 

并查集模板

两种优化方法:路径压缩和按秩合并

两种常见的优化策略:一是路径压缩策略,即当调用一次find(x)时,顺带将x指向根节点;二是按秩合并,即将有较少节点的树的根指向具有较多节点的树的根

1) 初始化:初始的时候每个结点各自为一个集合,father[i]表示结点 i 的父亲结点,如果 father[i]=i,我们认为这个结点是当前集合根结点。

void init() {
    for (int i = 1; i <= n; ++i) {
        father[i] = i;
    }
}

2) 查找:查找结点所在集合的根结点,结点 x 的根结点必然也是其父亲结点的根结点。

int get(int x) {
    if (father[x] == x) { // x 结点就是根结点
        return x; 
    }
    return get(father[x]); // 返回父结点的根结点
}

3) 合并:将两个元素所在的集合合并在一起,通常来说,合并之前先判断两个元素是否属于同一集合。

void merge(int x, int y) {
    x = get(x);
    y = get(y);
    if (x != y) { // 不在同一个集合
        father[y] = x;
    }
}

4)统计个数

int getSubsetNum()
{
    int res = 0;
    for(int i = 1; i <= num; i++)
        if(father[i] == i)
            res++;
    return res;
}

5)路径压缩:我们在一次查询的时候,可以把查询路径上的所有结点的father[i]都赋值成为根结点。路径压缩在实际应用中效率很高,其一次查询复杂度平摊下来可以认为是一个常数。并且在实际应用中,我们基本都用带路径压缩的并查集,代码为:

int get(int x) {
    if (father[x] == x) { // x 结点就是根结点
        return x; 
    }
    return father[x] = get(father[x]); // 返回父结点的根结点,并另当前结点父结点直接为根结点
}

 

例题

1、朋友圈

题目:
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。

给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

解析:
典型的并查集问题,实质上是求无向图的连通分支数。只需要将原始的集合分成几个子集合,每个子集合代表一个朋友圈即可。

int findCircleNum(vector<vector<int>>& M)
{
    if (M.size() == 0)   return 0;
    num = M.size();
    init();
    for (int i = 0; i < M.size(); i++)
        for (int j = i + 1; j < M.size(); j++)
        {
            if (M[i][j])
                merge(i + 1, j + 1);
        }
    return getSubsetNum();
}

另外,也可使用dfs进行求解(参考https://www.cnblogs.com/grandyang/p/6686983.html)的代码:

int findCircleNum(vector<vector<int>>& M) {
        int n = M.size(), res = 0;
        vector<bool> visited(n, false);
        for (int i = 0; i < n; ++i) {
            if (visited[i]) continue;
            helper(M, i, visited);
            ++res;
        }
        return res;
    }
    void helper(vector<vector<int>>& M, int k, vector<bool>& visited) {
        visited[k] = true;
        for (int i = 0; i < M.size(); ++i) {
            if (!M[k][i] || visited[i]) continue;
            helper(M, i, visited);
        }
    }

2、岛屿的个数
题目:
给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。

解析:
在这里用并查集实现。先令并查集内个节点的值为0,每当遇到一块陆地时,如果它未被放入并查集,则放入并查集;然后遍历四周陆地,如果未放入并查集,也将其放入;然后union两块陆地。最后并查集中子集个数即为答案
 

class Solution {
public:
    int findFather(vector<int> &father,int a){
        int f=a;
        while(father[f]!=f){
            f=father[f];
        }
        while(father[a]!=a){
            int z=a;
            a=father[z];
            father[a]=f;

        }
        return f;
    }
    void unionFather(vector<int> &father, int a,int b){
        int fa=findFather(father,a);
        int fb=findFather(father,b);
        if(fa!=fb){
            father[fa]=fb;
        }
    }

    int numIslands(vector<vector<char>>& grid) {
        if(grid.size()==0||grid[0].size()==0) return 0;
        int n=grid.size(),m=grid[0].size(),k=n*m;
        vector<int> father(k,-1);
        for(int i=0;i<k;i++){
            father[i]=i;
        }
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                int t1=i*m+j;
                int t2=t1+1,t3=t1+m;
                if(j+1<m&&grid[i][j]==grid[i][j+1]) unionFather(father,t1,t2);
                if(i+1<n&&grid[i][j]==grid[i+1][j]) unionFather(father,t1,t3);
            }
        }
        int res=0;
        for(int i=0;i<k;i++){
            if(father[i]==i&&grid[i/m][i%m]=='1'){
                res+=1;
            }
        }
        return res;
    }
};

和第一题一样,这个也可以用dfs解决:

class Solution {
public:
    int numIslands(vector<vector<char>>& grid) {
        int count = 0;
        for(int i = 0; i < grid.size(); i ++){
            for(int j = 0; j < grid[0].size(); j ++){
                if(grid[i][j] >= '1'){
                    count ++;
                    dfs(grid, i, j);
                }
            }
        }
        return count;
    }
    
    void dfs(vector<vector<char>>& grid, int i, int j){
        if(i >= grid.size() || i < 0 || j < 0 || j >= grid[0].size() || grid[i][j] != '1') 
            return;
        grid[i][j] = '0';
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};

 


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值