并查集
并查集是一种树形的数据结构,用于处理一些不交集(不相交的两个集合)的合并及查询问题。
它支持两种操作:
查找:确定某个元素处于哪个子集。
合并:将两个子集合并成一个集合。
思想
新学期学校组织春游,要求每个人必须和同班同学结伴游玩。因为是新生大家都还不熟悉,无法确认对方是否是自己同学,因此只能询问对方班主任姓名,来进行确认。
并查集就是在这种思想下产生的数据结构。
上例只需要查找一层就能获得想要的结果。实际情况下,往往需要往上查找好几层,才能找到想要的结果。
并查集包含了多个树形集合。每个集合的根节点被选取作为代表,用来表示整个集合。就如同班主任一样。
查找操作返回元素所属集合的代表;合并操作是对于两个集合而言的,它将某个代表设置为另一个代表的子节点。
基础操作
初始化
设置单元素集合。
func makeSet(x int) {
fa[x] = x
return
}
查找
查找某个元素的祖先。
func find(x int) int {
if fa[x] == x { // 如果是祖先则返回
return x
}
return find(fa[x]) // 如果自己不是祖先,则问上级祖先是谁
}
合并
只需要将一个集合设置为另一个集合的子节点,就能实现合并。
func union(x, y) {
xRoot := find(x)
yRoot := find(y)
if xRoot == yRoot {
return
}
fa[xRoot] = yRoot
}
优化方法
路径压缩
每次进行查找操作,都需要一层一层往上询问,效率比较低。
所谓“路径压缩”,就是在进行“查找”时,将路径上的每个节点直接连到根节点,使得树更加扁平化。
“路径压缩”可以使以后的”查找“操作更加快速。
func find(x int) int {
if fa[x] != x {
fa[x] = find(fa[x])
}
return fa[x]
}
按秩合并(启发式合并)
在进行合并操作时,无论将哪一个集合连接到另一个集合下面,总能得到正确的结果。但是采用不同的连接方法,对于查找操作,将会存在不同的时间复杂度。
将一颗节点数和深度都较小的集合树连接到另一颗集合树下,相比于另一种方案,执行查找操作的用时会更少。
但是不可能总能得到节点数和深度都较小的集合树,因此可以选择两者之一作为衡量指标。
现在讨论使用深度作为衡量指标。节点数的讨论类似。
路径压缩会改变树的高度,如果使用深度作为衡量指标,每次压缩时都要重新计算树的深度,查找算法复杂度会变高。初始化集合时,可以使用”秩“代替”深度“。将秩初始化为0,两个秩同为r的集合树合并时,它们的秩r+1,否则秩不变。如果没有路径压缩,秩和深度则总是相等的。
func makeSet(x int) {
fa[x] = x
rank[x] = 0
return
}
// ... 省略查找算法
func union(x, y) {
xRoot := find(x)
yRoot := find(y)
if xRoot == yRoot {
return
}
if rank[xRoot] < rank[yRoot] {
fa[xRoot] = yRoot
} else if rank[xRoot] > rank[yRoot] {
fa[yRoot] = xRoot
} else {
fa[xRoot] = yRoot
rank[yRoot]++
}
}
补充
带权并查集
带权并查集的节点之间,具有一定的权重。比如一个部门中,副部长的职级高,他和部长之间联系的紧密程度,要高于普通成员和部长之间的联系,权重就比较高。此时的合并规则,既可以使用秩,又可以自己设定。
Last updated
Was this helpful?