等价关系
关系R定义在集合S上。对于S中的a,b元素,如果aRb为true,那么就说ab有关系。 等价关系是满足下面三个条件的关系:
- 自反性aRa
- 对称性aRb当且仅当bRa
- 传递性若aRb,bRc则aRc。 例如≤不是等价关系,因为他不满足对称性; 导线连通则是一个等价关系。
动态等价行问题
给定一个等价关系~,考虑对于任意的a,b,是否有a~b?
一个元素的等价类是S的一个子集,它包含所有与a有(等价)关系的元素。 有了这个定义,我们只需要确定a,b是否在一个等价类中。 那么如何形成等价类?
- find,判断a,b的等价类名字是否相同
- union,检查a,b是否在一个等价类中,如果没有添加关系a~b
那么最直观的做法就是,把集合映射为一个int型数组,find(x)返回x的根,union(x,y)操作始终把x作为y的根。
class UF
{
private:
vector<int> s;
public:
int find(int x);
void union(int root1,int root2);
UF(int n):s(n, -1){};
};
int UF::find(int x) const{
if(s[x]<0)
return x;
else
return find(s[x]);
}
void UF::union(int root1,root2){
s[root2] = root1;
}
但是这种算法是会建立深度为N-1的树,执行N次find操作的时间复杂度是O(N)。那么M次混合操作(union,find)的时间复杂度就是O(MN)。 那么我们如何减小树的深度,以获得更灵巧的union算法?
* 按大小合并
* 按秩合并
按大小合并
我们总是让较小的树成为较大的树的子树。 实现起来的话我们让根节点保存节点数目的负值,然后每次合并之前比较大小。初始时只有一个节点,所以初始值设为-1。
void UF::unionBySize(int root1,root2){
if(s[root2] > s[root1]) //因为是负值,所以说明root1更大
s[root1] += s[root2];
s[root2] = root1;
else {
s[root2] += s[root1];
s[root1] = root2;
}
}
按秩合并
此时我们追踪每棵树的高度而不是大小,并总让浅的树成为深的树的子树。可以看出只有两颗树的高度相同时树的高度才会增加。当然我们实际上也是存储树的高度的负值。
void UF::unionByRank(int root1,root2){
if(s[root2] < s[root1]) //因为是负值,说明root2更深
s[root1] = root2;
else {
if(s[root1] == s[root2])
--s[root1]; //高度加1
s[root2] = root1;
}
}
这样M次操作的时间平均下来就是线性的O(M),但是最坏的情况O(MlogN)依然可能发发生,例如把集合放到队列里并让前两个元素反复出队入队。这样无论如何都又可能生成最坏的树,也就是说union算法没有更多改进的空间了。
所以我们考虑改进find方法。
路径压缩(改进find)
就是每次执行find(x)操作都让他的父节点变成他祖父节点,也就是离根更近一次。
int UF::find(int x) {
if(s[x]<0)
return x;
else
return s[x] = find(s[x]);
}
下面我们来看一个我写的例子:畅通工程问题