模板:并查集
什么是并查集?
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;
}
}