有一类删除叫做惰性删除,其特点是,这个点仍然在集合中但不去统计它。
对于并查集,一般来说想要实现删除是有些困难的,不过如果用一些黑科技就会非常简洁。
首先先引入按秩合并并查集的概念。用路径压缩的并查集,复杂度虽然是
O(nα(n))
,但是会破坏原本的结构,这是比较令人不爽的。
但是朴素的并查集在最坏的情况下查询会被卡成
O(n)
,这是无法接受的慢。所以我们需要让并查集尽量平衡。实现也很简洁:
void uni(int x, int y) {
int xx = find(x), yy = find(y);
if(xx == yy) return;
if(rank[xx] > rank[yy]) fa[yy] = xx;
else if(rank[xx] < rank[yy]) fa[xx] = yy;
else ++rank[xx], fa[yy] = xx;
}
其中rank数组维护的是深度,我们尽量将深度小的合并到深度大的上面去,如果深度一样就加一下深度随便合并了。这样树是几乎平衡的,所以查询就是
O(log n)
的。
然后来思考一下如何删除。直接删除固然是不可能的,考虑到我们需要处理其子孙的关系,所以可以建一个虚点。
void del(int x) {
pid[x] = ++N;
fa[pid[x]] = pid[x];
rank[pid[x]] = 1;
}
看上去不是很好懂的样子,其实原理很简单。
用一个动态变化的pid
数组来维护编号
i
的实际位置
我们对某个点
再考虑原来的点,好像没什么影响,如果是对儿子的操作的话其父亲指向并不会变,同样
唯一有影响的是对
i
<script type="math/tex" id="MathJax-Element-400">i</script>进行查找操作的时候会找错集合,所以我们需要对查找函数进行一些修改:
int find(int x) {
int id = pid[x];
while(fa[id] != id) id = fa[id];
return id;
}
当然还有一些配套的操作,比如开一个栈维护加入过哪些集合以实现规模较小的可持久化(如果要实现真正的可持久化请移步主席树),或是加一个vis
数组来标志某个数是否被删除。
好像就是这样来着…