并查集

模板:并查集

什么是并查集?

LeetCode官方解释:

在计算机科学中,并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(Union-find Algorithm)定义了两个用于此数据结构的操作:

  • Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
  • Union:将两个子集合并成同一个集合。
  • 由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(Union-find Data Structure)或合并-查找集合(Merge-find Set)。

为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x)Find(x) 返回 xx 所属集合的代表,而 Union 使用两个集合的代表作为参数。

一篇写的不错的文章:

https://blog.csdn.net/qq_41593380/article/details/81146850

个人理解:

并查集适合处理多个连通分量,而且可以动态的合并两个连通分量。虽然抽象结构是树状型,但是我们完全可以使用数组表示,每一个元素都记录它的父节点,而根节点则记录其自身。

所谓find函数,就是找到当前节点的根节点,如果我想知道两个节点是否是属于同一个连通分量的,那么我只要知道他们的根节点是否是一样的就行了。然后,为了在查找时加快速度,我们可以使用路径压缩。相当于把一颗树结构的深度变为2,把它们的父节点直接连在根节点上,这样下次查到时不用一级一级往上找了。

union函数,将两个连通分量合并为一个。操作很简单,只要将其中一个根节点变为另一个节点的父节点,就合并完成了。因为如果一个根节点的父节点指向其它节点,那么它就不再是根节点了,所以调用find函数就会继续往上找。因此,union函数首先要分别调用两个数的find找到各自的根节点,如果根节点一样就不用操作了,否则就要改变其中一个根节点的父节点使其不再是根节点。那么问题来了,改变哪个更合适呢?其实无论改哪个都可以。但是,比如一个连通分量是1e9个数,另一个里只有1个数,你改动了前者的根节点,假设前者的根节点最大深度为2,已经是最优的情况,可是现在根节点被改动,导致10亿个节点需要重新压缩。而如果我选择后者,一次都不用重新压缩。因此我们可以设置权重,改动权重小的,而权重的值就是连通分量内的节点数。

模板:

  • 加权并查集
int[] father;
int[] sz;
int num;

public int find(int p) {
    if (p != father[p]) {
        p = find(father[p]);
    }
    return p;
}
public void union(int p, int q) {
    int i = find(p);
    int j = find(q);
    if (i == j) return;
    num -= 1;
    if (sz[i] < sz[j]) {
        father[i] = j;
        sz[j] += sz[i];
    } else {
        father[j] = i;
        sz[i] += sz[j];
    }
}

public void initUF(int n) {
    father = new int[n];
    sz = new int[n];
    num = n;
    for (int i = 0; i < n; i++) {
        father[i] = i;
        sz[i] = 1;
    }
}
  • 递归的下的find时间复杂度为O(n)

  • 证明:因为T(n) = T(n - 1) + O(1)

  • 假设T(n) <= cn

  • T(n) = c(n - 1) + O(1)

  • = cn - c + c <= cn

  • 所以find最坏时间复杂度为O(n)

  • union也为O(n)

  • 加权 + 路径压缩(递归) + 并查集

int[] father;
int[] sz;
int num;
public int find(int p) {
    if (p != father[p]) {
        father[p] = find(father[p]);
    }
    return father[p];
}
public void union(int p, int q) {
    int i = find(p);
    int j = find(q);
    if (i == j) return;
    num -= 1;
    if (sz[i] < sz[j]) {
        father[i] = j;
        sz[j] += sz[i];
    } else {
        father[j] = i;
        sz[i] += sz[j];
    }
}

public void initUF(int n) {
    father = new int[n];
    sz = new int[n];
    num = n;
    for (int i = 0; i < n; i++) {
        father[i] = i;
        sz[i] = 1;
    }
}
  • 路径压缩后,我们发现,只要union或者find的操作次数大于n(n为数组的长度),那么时间复杂度find或者union的时间复杂度就可以均摊到O(1),因为union最多只会增加树的高度为1,而一次路径压缩就可把树的高度变为2。

  • 加权 + 路径压缩(迭代) + 并查集

int[] father;
int[] sz;
int num;
public int find(int p) {
    int son = p, tmp;
    while (father[p] != p) {
        p = father[p];
    }
    
    while (father[son] != p) {
        tmp = father[son];
        father[son] = p;
        son = tmp;
    }
    
    return p;
}
public void union(int p, int q) {
    int i = find(p);
    int j = find(q);
    if (i == j) return;
    num -= 1;
    if (sz[i] < sz[j]) {
        father[i] = j;
        sz[j] += sz[i];
    } else {
        father[j] = i;
        sz[i] += sz[j];
    }
}

public void initUF(int n) {
    father = new int[n];
    sz = new int[n];
    num = n;
    for (int i = 0; i < n; i++) {
        father[i] = i;
        sz[i] = 1;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值