带权并查集
带权并查集区别于普通并查集的是,普通并查集只记录集合元素与集合之间的关系,即改元素是否属于该集合。带权并查集是在普通并查集得到急促上记录每一个元素的权值(本质上是记录该元素相对于根节点的权值),这个权值可以表示集合内元素之间除了链接外的某种关系,这里我们使用 v a l ( v a l u e ) val(value) val(value)记录每个元素的权值。
在上面并查集的路径压缩中,如果对节点 C C C做 f i n d _ s e t find\_set find_set操作,最终会得到 A A A,但是查找的过程中会先经过 B B B,再通过 f i n d _ s e t ( B ) find\_set(B) find_set(B)得到 A A A,这是一个值得优化的地方,如果直接让 C C C链接到 A A A不是更好吗,这样就可以省去中间的操作,如果 C C C跟 A A A直接相隔很多节点,这个优化就极大地提升了查找的效率,也就是希望得到这样的结果:
将每个节点直接与其最终的节点链接,直接是路径压缩:
int find_set(int x){
if(x != arr[x]) arr[x] = find_set(arr[x]);//路径压缩
return arr[x];
}
与普通并查集相比,带权并查集知识在 f i n d _ s e t find\_set find_set中添加了一个赋值操作,将在查询过程中得到所有节点的根节点都设为最终得到的节点。
通过图,我们发现在合并时对于权值我们需要思考两个问题:
- 每个节点都记录的时与根节点之间的权值,那么我们在进行路径压缩过程中,权值就要进行相应的更新,因为在进行路径压缩之前,每个节点都是与其根节点相链接的。
- 在进行合并的时候,权值也要进行相应的更新,因为他们的根节点是不一样的的。
路径压缩:
int arr[M], val[M];
void init_set(){//初始化
for(int i = 0; i <= n; i++){
arr[i] = i;
val[i] = 0;
}
}
int find_set(int x){//路径压缩
if(x == arr[x]) return x;
int temp = find_set(arr[x]);
val[x] += val[arr[x]];
return arr[x] = temp;
}
合并:
先看图:现在我们已知 x x x所在并查集的根节点 p x p_x px, y y y所在并查集的根节点 p y p_y py,如果我们还知道 x , y x,y x,y之间得到关系,要将 p x p_x px合并到 p y p_y py上,大家通过观察图,有没有发现这很像我们学习的向量,如果把它看成向量,那么我们就可以很任容易算出 ? = v y + v − v x ?=v_y+v-v_x ?=vy+v−vx。所以我们就知道代码应该这样去跟新了,当然了,这里需要注意并不是每个问题都是这样更新的,具体的问题具体分析(有的会做取模之类的操作)。
void union_set(int x, int y){
int px = find_set(s[i]);
int py = find_set(t[i]);
if(px != py){
arr[px] = py;
val[px] = val[y] + v - val[x];
}
}