并查集主要用于解决一些元素分组的问题,管理一系列不相交的集合,并支持两种操作:
**合并(union):**把两个不相交的集合合并为一个集合。
**查询(find):**查询两个元素是否在同一个集合中。
并查集引入
并查集的重要思想在于,用集合中的一个元素代表集合。
并查集的初始化中,每个元素都是一个集合。在进行合并时,如下图所示,将要合并的节点指向另一个节点
一番合并后,产生下图形式:
假设2号要与6号合并,首先去找到2号的根节点,再去找到6号的根节点,根据根节点是否相同来判断它们是否已经属于同一集合:相同则属于同一集合,不相同则将两个集合合并——将6的根节点4指向2的根节点1。
并查集的结构其实是一个树状结构,根节点是集合的代表元素。
路径压缩
在并查集的合并过程当中,可能会形成上图一样的长链,增加找到根节点的时间。因此希望每个元素到根节点路径尽可能短:
要实现这种结构,只需在查询的过程当中把沿途的每个节点的父节点都设为根节点即可。下一次再查询时,我们就可以省很多事。这用递归的写法很容易实现。
合并的比较方法
应当将简单的树向复杂的树上合并,从而使合并后到根节点距离变长的节点个数比较少。
并查集的时间复杂度
查询次数+合并次数 N或以上则平均下来单次查询或合并的平均时间复杂度O(1)
并查集代码
package basic_class_04;
import java.util.HashMap;
import java.util.List;
public class Code_09_UnionFind {
public static class Node {
// whatever you like
}
public static class DisjointSets {
public HashMap<Node, Node> fatherMap;
public HashMap<Node, Integer> rankMap;
public DisjointSets() {
fatherMap = new HashMap<Node, Node>();
rankMap = new HashMap<Node, Integer>();
}
public void makeSets(List<Node> nodes) {
fatherMap.clear();
rankMap.clear();
for (Node node : nodes) {
fatherMap.put(node, node);
rankMap.put(node, 1);
}
}
public Node findFather(Node n) {
Node father = fatherMap.get(n);
if (father != n) {
father = findFather(father);
}
fatherMap.put(n, father);
return father;
}
public void union(Node a, Node b) {
if (a == null || b == null) {
return;
}
Node aFather = findFather(a);
Node bFather = findFather(b);
if (aFather != bFather) {
int aFrank = rankMap.get(aFather);
int bFrank = rankMap.get(bFather);
if (aFrank <= bFrank) {
fatherMap.put(aFather, bFather);
rankMap.put(bFather, aFrank + bFrank);
} else {
fatherMap.put(bFather, aFather);
rankMap.put(aFather, aFrank + bFrank);
}
}
}
}
public static void main(String[] args) {
}
}
例题
亲戚
题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。
输入格式
第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。
以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有亲戚关系。
接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。
输出格式
P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。
可以建立模型,把所有人划分到若干个不相交的集合中,每个集合里的人彼此是亲戚。为了判断两个人是否为亲戚,只需看它们是否属于同一个集合即可。因此,这里就可以考虑用并查集进行维护了。
#include <cstdio>
#define MAXN 5005
int fa[MAXN], rank[MAXN];
inline void init(int n)
{
for (int i = 1; i <= n; ++i)
{
fa[i] = i;
rank[i] = 1;
}
}
int find(int x)
{
return x == fa[x] ? x : (fa[x] = find(fa[x]));
}
inline void merge(int i, int j)
{
int x = find(i), y = find(j);
if (rank[x] <= rank[y])
fa[x] = y;
else
fa[y] = x;
if (rank[x] == rank[y] && x != y)
rank[y]++;
}
int main()
{
int n, m, p, x, y;
scanf("%d%d%d", &n, &m, &p);
init(n);
for (int i = 0; i < m; ++i)
{
scanf("%d%d", &x, &y);
merge(x, y);
}
for (int i = 0; i < p; ++i)
{
scanf("%d%d", &x, &y);
printf("%s\n", find(x) == find(y) ? "Yes" : "No");
}
return 0;
}
奶酪问题
**解题思路:**把所有空洞划分为若干个集合,一旦两个空洞相交或相切,就把它们放到同一个集合中。我们还可以划出2个特殊元素,分别表示底部和顶部,如果一个空洞与底部接触,则把它与表示底部的元素放在同一个集合中,顶部同理。最后,只需要看顶部和底部是不是在同一个集合中即可。这完全可以通过并查集实现。
#include <cstdio>
#include <cstring>
#define MAXN 1005
typedef long long ll;
int fa[MAXN], rank[MAXN];
ll X[MAXN], Y[MAXN], Z[MAXN];
inline bool next_to(ll x1, ll y1, ll z1, ll x2, ll y2, ll z2, ll r)
{
return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2) <= 4 * r * r;
//判断两个空洞是否相交或相切
}
inline void init(int n)
{
for (int i = 1; i <= n; ++i)
{
fa[i] = i;
rank[i] = 1;
}
}
int find(int x)
{
return x == fa[x] ? x : (fa[x] = find(fa[x]));
}
inline void merge(int i, int j)
{
int x = find(i), y = find(j);
if (rank[x] <= rank[y])
fa[x] = y;
else
fa[y] = x;
if (rank[x] == rank[y] && x != y)
rank[y]++;
}
int main()
{
int T, n, h;
ll r;
scanf("%d", &T);
for (int I = 0; I < T; ++I)
{
memset(X, 0, sizeof(X));
memset(Y, 0, sizeof(Y));
memset(Z, 0, sizeof(Z));
scanf("%d%d%lld", &n, &h, &r);
init(n);
fa[1001] = 1001; //用1001代表底部
fa[1002] = 1002; //用1002代表顶部
for (int i = 1; i <= n; ++i)
scanf("%lld%lld%lld", X + i, Y + i, Z + i);
for (int i = 1; i <= n; ++i)
{
if (Z[i] <= r)
merge(i, 1001); //与底部接触的空洞与底部合并
if (Z[i] + r >= h)
merge(i, 1002); //与顶部接触的空洞与顶部合并
}
for (int i = 1; i <= n; ++i)
{
for (int j = i + 1; j <= n; ++j)
{
if (next_to(X[i], Y[i], Z[i], X[j], Y[j], Z[j], r))
merge(i, j); //遍历所有空洞,合并相交或相切的球
}
}
printf("%s\n", find(1001) == find(1002) ? "Yes" : "No");
}
return 0;
}
参考:
https://zhuanlan.zhihu.com/p/93647900