并查集路径压缩_[力扣547] 并查集及其回滚

30957cd97a7727a3ee627cec12b06db3.png

题目链接

547. 朋友圈

题目描述

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

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

样例

示例 1:
输入:
[[1,1,0],
[1,1,0],
[0,0,1]]
输出: 2
说明:已知学生0和学生1互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回2。
示例 2:
输入:
[[1,1,0],
[1,1,1],
[0,1,1]]
输出: 1
说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1。

数据范围

1. N 在[1,200]的范围内。
2. 对于所有学生,有M[i][i] = 1。
3. 如果有M[i][j] = 1,则有M[j][i] = 1。

算法

即求邻接矩阵中的连通分量数。完全照搬并查集模板,背谱就可以。

并查集支持两个操作,same(x,y) 和 union(x,y)

same(x, y): 问 x 和 y 是否属于同一个集合
union(x, y): 将 x 和 y 所属的集合合并成同一个集合

两个操作都要用到一个内部操作, find(x)

find(x): 问 x 属于那个集合,返回集合id

这些组成各个集合的元素以森林的形式维护,每棵树代表一个集合,同一个集合的元素在同一个树上,每棵树的根是集合的id。find(x) 操作需要从 x 节点向上找父节点直到树根,树根的编号就x所属集合的id。这里需要维护一个 father 数组,保存 x 的父节点用于从 x 向上找根。

两个优化

路径压缩:每次 find(x) 时,向上查找过程中会经过一些节点,把这些节点的父节点都设为 find(x) 找到的根,以后的查找再经过这个点的时候就可以一次找到根。
按秩合并:额外维护一个 rank 数组,rank[x] 表示以 x 为根的子树的高度,合并时把较矮的树的树根连接到较高的树的树根上。

主要解决的问题:

加边连通性:不断加边,同时问某两个点是否连通
连通分量个数:本题就是
Kruskal 最小生成树

代码(c++)

主要流程:建图(本题输入就是邻接矩阵) -> 确定建并查集之后要算的值(本题是集合个数, 代码中的set_size) -> 根据图建立并查集(merge) -> 返回要算的结果

class UnionFindSet {
public:
    UnionFindSet(int n)
    {
        _set_size = n;
        _father = vector<int>(_set_size, -1);
        _rank = vector<int>(_set_size, 0);
        for(int i = 0; i < _set_size; ++i)
            _father[i] = i;
    }

    ~UnionFindSet(){}

    bool same(int x, int y)
    {
        return _find(x) == _find(y);
    }

    void merge(int x, int y)
    {
        x = _find(x);
        y = _find(y);
        if(x == y) return;
        // 此时 x, y 是所在树的根
        if(_rank[x] < _rank[y])
            _father[x] = y;
        else
        {
            _father[y] = x;
            if(_rank[x] == _rank[y])
                ++_rank[x];
        }
        --_set_size;
    }

    int set_size()
    {
        return _set_size;
    }

private:
    vector<int> _father;
    vector<int> _rank;
    int _set_size;

    int _find(int x)
    {
        if(_father[x] == x)
            return x;
        else
            return _father[x] = _find(_father[x]); // 路径压缩
    }
};

class Solution {
public:
    int findCircleNum(vector<vector<int>>& M) {
        int m = M.size();
        if(m == 1) return 1;
        UnionFindSet unionfindset(m);
        for(int i = 0; i < m - 1; ++i)
            for(int j = i + 1; j < m; ++j)
                if(M[i][j] == 1)
                    unionfindset.merge(i, j);
        return unionfindset.set_size();
    }
};

引申:并查集的回滚

如果并查集要支持回滚操作,就是要把已经合并成一棵树的若干子树,恢复成此前的若干子树。需要开栈保存每次合并操作时,被合并的哪一个根(高度较小的那一个根)。后续通过栈里的信息回滚。

支持回滚操作就不能使用路径压缩了。

stack<int> st; // 保存每次合并操作时被合并的根

void merge(int x, int y)
{
    x = _find(x);
    y = _find(y);
    if(x == y) return;
    // 此时 x, y 是所在树的根
    if(_rank[x] < _rank[y])
    {
        _father[x] = y;
        st.push(x); // x 被合并
    }
    else
    {
        _father[y] = x;
        if(_rank[x] == _rank[y])
            ++_rank[x];
        st.push(y); // y 被合并
    }
    --_set_size;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值