并查集

本文介绍了并查集这一数据结构的基本概念及其核心操作——合并与查找。并查集主要用于处理不相交集合的合并及查询问题,文章详细阐述了其实现方式,并通过一个具体的例子展示了如何运用并查集解决实际问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

并查集的定义

并查集,望文生义,通过扩展两个词“合并”和“查找”就大致明白这个数据结构主要的用途。它是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。 进行快速规整。

两个主要操作:合并和查找

合并

合并两个不相交集合 操作很简单:先设置一个数组Father[x],表示x的“父亲”的编号。 那么,合并两个不相交集合的方法就是,找到其中一个集合最父亲的父亲(也就是最久远的祖先),将另外一个集合的最久远的祖先的父亲指向它。
这里写图片描述
上图为两个不相交集合,合并后Father(b):=Father(g)

public void union(int x, int y)
{
    //get_father()后面会有
    father[get_father(x)] = get_father(y); //指向最祖先的祖先
}

查找

查找两个元素,是不是在同一个集合中。
本操作可转换为寻找两个元素的最久远祖先是否相同。可以采用递归实现。

public boolean is_same(int x, int y)
{
    return get_father(x) == get_father(y);
}

效率上的优化

路径压缩

寻找祖先时,我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次都是O(n)的复杂度。为了避免这种情况,我们需对路径进行压缩,即当我们经过”递推”找到祖先节点后,”回溯”时顺便将它的子孙节点都直接指向祖先,这样以后的复杂度就变成O(1)了,如下图所示。可见,路径压缩方便了以后的查找。
这里写图片描述

public void init()
{
    //初始化即认为每个元素都是一个独立的集合
    for (int i=0; i<MAX; i++)
        father[i] = i;
}
public int get_father(int v)
{
    if (father[v] != v)
        father[v] = get_father(father[v]);
    return father[v];
}

按秩合并

合并时将元素少的集合合并到元素多的集合中。

一个应用的栗子

题目:有100个用户的关系列表(为了方便表达,把名字变成一个整数,这100个用户的名字分别为1-100,每行有两个名字,代表这两人互为好友),需要使一个广告让这100个用户都看到,请问初始至少将这条广告传播个几个人?(这条广告编写得很精美,因此用户们看到一定会转发在他的朋友圈让他的朋友看到的)。

举例:下面是7个人的关系列表,问小明若要使A信息完全覆盖这7个人,初始至少要传播给几个人?
假设7个人A,B,C,D,E,F,G
A和B是好友
B和C是好友
C和D是好友
E和F是好友
G由于性格怪癖没有朋友。

public class UFSet {
    public static void main(String[] args) {

        try {
            FileReader fr = new FileReader(new File("files/hotspot2.txt"));
            BufferedReader br = new BufferedReader(fr);
            WeightedQUWithPathCompression uf;

            String[] parts;
            parts = br.readLine().split(" ");
            // based on 1, not 0
            uf = new WeightedQUWithPathCompression(100001);
            // construct the uf
            String str = "";
            while ((str = br.readLine()) != null) {
                parts = str.split(" ");
                uf.union(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
            }
            System.out.println(uf.count-1);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    static class WeightedQUWithPathCompression {
        private int count;
        private int[] id;//
        private int[] size;

        public WeightedQUWithPathCompression(int N) {
            this.count = N;
            this.id = new int[N];
            this.size = new int[N];

            for (int i = 0; i < this.count; i++) {
                id[i] = i;
                size[i] = 1;
            }
        }

        private int find(int p) {
            while (p != id[p]) {
                id[p] = id[id[p]]; // 路径压缩,会破坏掉当前节点的父节点的尺寸信息,因为压缩后,当前节点的父节点已经变了
                p = id[p];
            }

            return p;
        }

        public void union(int p, int q) {
            int pCom = this.find(p);
            int qCom = this.find(q);

            if (pCom == qCom) {
                return;
            }
            // 按秩进行合并
            if (size[pCom] > size[qCom]) {
                id[qCom] = pCom;
                size[pCom] += size[qCom];
            } else {
                id[pCom] = qCom;
                size[qCom] += size[pCom];
            }
            // 每次合并之后,树的数量减1
            count--;
        }

        public int count() {
            return this.count;
        }
    }
}

最后调用count方法就得到了结果

注意事项

根据问题的具体特性,本题同时采用了两种优化策略,即按秩合并以及路径压缩。因为问题本身对合并的先后关系以及子树的秩这类信息不敏感。然而,并不是所有的问题都这样,对合并的先后顺序有要求的时候,就不能随意用优化手段了。

参考资料:
董的博客
dm_vincent的专栏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值