表示互斥集合(disjoint set)时,经常会使用另一种具有独特形态的树结构--并查集(union-find)数据结构。
互斥集合:
假设有n名客人参加聚会,主持人要求相同生日的人组成一队。话音刚落,客人们立刻开始组队。刚开始时,因为不知道哪位客人的生日与自己的生日相同,所以大家只能单独徘徊。不过,只要找到1名相同生日的客人,两人就会开始结伴移动。若发现另一个相同生日的队伍,就会与这支队伍合并。
应该使用何种数据结构表示这种情况呢?对参加聚会的全体客人的集合而言,队伍是将些集合分割为若干部分的子集合。而每个队由生日相同的客人组成,所以每名客人都不可能属于一个以上的队伍。这种没有共同元素的子集合称为互斥子集合,保存这种信息并对其进行操作的数据结构就是并杳集数据结构。
为了表示这种情况,首先需要把客人表示为0到n-1之间的元素,然后生成只包含1个元素的n个集合。两名客人a和b的生日相同时,合并包含二者的集合。为了实现该过程,需要如下3种运算。
1、初始化:初始化为n个元素被包含于各自集合的形式
2、并集(union)运算:给出两个元素a和b时,合并两个元素所在集合
3、查找(find)运算:给出了某个元素a时,返回此元素所在集合
利用数组表示互斥集合:
利用一维数组就能非常简单地表示互斥集合。生成如下形式的数组belongsTo。
belongsTo[i] = 第i个元素所属的集合序号
此方式并集运算耗时,虽然查找O(1)
利用树结构表示互斥集合:
同属于一个集合的元素绑定到一个树结构。
struct NaiveDisjointSet {
vector<int> parent;
NaiveDisjointSet(int n) : parent(n) {
for (int i=0; i<n; i++) parent[i] = i;
}
int find(int u) const {
if (u == parent[u]) return u;
return find(parent[u]);
}
void merge(int u, int v) {
u = find(u); v = find(v);
if (u == v) return;
parent[u] = v;
}
};
互斥集合的优化:
如果最后生成高度为n-1的树,则是链表,那么合并和查找运算都会耗费O(n)的运算时间。
最简单的方法是,合并两个树时,把高度更低的树添加到高度更高的树,以限制树高的增加
struct OptimizedDisjointSet {
vector<int> parent, rank;
NaiveDisjointSet(int n) : parent(n) , rank(n, 1) {
for (int i=0; i<n; i++) parent[i] = i;
}
int find(int u) const {
if (u == parent[u]) return u;
return parent[u] = find(parent[u]);
}
void merge(int u, int v) {
u = find(u); v = find(v);
if (u == v) return;
if (rank[u] > rank[v]) swap(u, v);
parent[u] = v;
if (rank[u] == rank[v]) ++rank[v];
}
};
时间复杂度:并集为O(lgn),查找为O(lgn)
另一种优化方法是路径压缩优化
示例:判断图的连通性
示例:追踪最大集合