并查集的定义
并查集,望文生义,通过扩展两个词“合并”和“查找”就大致明白这个数据结构主要的用途。它是一种树型的数据结构,用于处理一些不相交集合(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的专栏