关于带标记并查集的一些想法
一粒夸克
2021-04-04 10:12:47
### 灵感来自于[[SCOI2011]棘手的操作](https://www.luogu.com.cn/problem/P3273)
最开始的时候,我看见这个题目,感觉可以不用左偏树
因为题目中说的是 **“将。。。值增加v”**
所以这些点权只会越来越大
所以只要用并查集维护联通,每次增加点权时更新一下最大值就好了
[直到我把代码交上去之后,我才意识到 v 可以小于 0](https://www.luogu.com.cn/record/48945355)(居然还过了一个点。。。)
所以这篇博客就和这题没什么关系了
![](https://cdn.luogu.com.cn/upload/image_hosting/84lxojui.png)
------------
### 考虑如何给一个并查集加标记
为了算法的效率,打标记的操作最好在 O(1) 时间内完成,不然这种算法和其他算法相比可能就没有价值
紧接着要考虑标记的下传
首先想到的思路是在向上找父亲时,把父亲的标记加在自己身上
而并查集与左偏树、线段树之类的二叉树最大的不同在于:
**并查集中一个节点的儿子数量是不确定的,所以它不能像线段树那样 $pushdown$ 一次后直接把标记清空**
那么问题就是,我要如何防止一个标记被反复加到同一个点上
### 想了两种思路:
------------
### 1.
我的第一种思路:
![](https://cdn.luogu.com.cn/upload/image_hosting/7toyu9cn.png)
如图,我访问到了蓝色节点,而它的父亲正好有一个标记(红色)
那么我把标记加到蓝色节点的 $val$ 上,然后把蓝色节点提到它的父亲节点上面,蓝色节点的子节点接到黑色节点上
这样就可以保证标记内的所有点都会且只会只会被标记更新一次了
![](https://cdn.luogu.com.cn/upload/image_hosting/8yek8cu5.png)
(蓝色节点的父亲节点不是根节点的情况)
问题在于 **“把蓝色节点提到它的父亲节点上面,蓝色节点的子节点接到黑色节点上”** 这个操作不太好实现
伪代码:
```cpp
int fa[300005],w[300005],type[300005];
int find(int x){
if(fa[x]==x)return x;
int y=find(fa[x]);
if(type[fa[x]){
w[x]+=type[fa[x]];
这里需要干点什么来把 x 的儿子转移到 fa[x] 上
fa[fa[x]]=x;
}
return fa[x]=y;
}
```
如果有人会写的话欢迎在评论区下方留言
![](https://cdn.luogu.com.cn/upload/image_hosting/84lxojui.png)
------------
### 2.
第二种思路:
还以这张图为例
![](https://cdn.luogu.com.cn/upload/image_hosting/7528uk3q.png)
### 先考虑蓝色节点的父亲不是根节点的情况
我们在拿蓝色节点的父亲节点的标记更新蓝色节点的 $val$ 时,也把标记加到蓝色节点的标记上
这样就可以保证标记内的所有点都会且只会只会被标记更新一次了
### 再考虑蓝色节点的父亲是根节点的情况
![](https://cdn.luogu.com.cn/upload/image_hosting/36jf0pxd.png)
该返回返回,什么也别干就行了
### 并查集+$pushdown$ :
```cpp
int find(int x){
if(fa[x]==x)return x;
int y=find(fa[x]);
if(y^fa[x]){
w[x]+=type[fa[x]];
type[x]+=type[fa[x]];
}
return fa[x]=y;
}
```
### 最后查询某个点的 $val$ 时再把根节点的 type 加上:
```cpp
void query(){
long long a;
scanf("%lld",&a);
int y=find(a);
printf("%lld\n",w[a]+type[y]);
}
```
### 该合并合并:
```cpp
void merge(){
long long a,b;
scanf("%lld%lld",&a,&b);
a=find(a);
b=find(b);
type[a]-=type[b];
w[a]+=type[a];
fa[a]=b;
}
```
### 对一个联通块打标记:
```cpp
void add(){
long long a,b;
scanf("%lld%lld",&a,&b);
int y=find(a);
type[y]+=b;
}
```
![](https://cdn.luogu.com.cn/upload/image_hosting/84lxojui.png)
有人问,这种并查集可以维护什么一般化的操作和信息?
我的理解是,这篇博客解决了上文中所说的 :
**“并查集中一个节点的儿子数量是不确定的,所以它不能像线段树那样 $pushdown$ 一次后直接把标记清空”**
这一问题
所以凡是线段树可以维护的标记,这种并查集应该都可以维护
线段树可以对一个区间干什么,这种并查集也就可以对一个集合干什么
![](https://cdn.luogu.com.cn/upload/image_hosting/84lxojui.png)
------------
### 最后,附上[[SCOI2011]棘手的操作](https://www.luogu.com.cn/problem/P3273)的十分代码
```cpp
#include<bits/stdc++.h>
using namespace std;
int fa[300005],n,q;
long long w[300005],type[300005];
long long a,b,tot,mx[300005],alltyp;
int find(int x){
if(fa[x]==x)return x;
int y=find(fa[x]);
if(y^fa[x]){
w[x]+=type[fa[x]];
type[x]+=type[fa[x]];
}
return fa[x]=y;
}
char c[3];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%lld",&w[i]);
fa[i]=i;
mx[i]=w[i];
tot=max(tot,w[i]);
}
scanf("%d",&q);
for(int i=0;i<q;i++){
scanf("%s",c);
if(c[0]=='U'){
scanf("%lld%lld",&a,&b);
a=find(a);
b=find(b);
if(a==b)continue;
type[a]-=type[b];
w[a]+=type[a];
fa[a]=b;
mx[b]=max(mx[b],mx[a]+type[a]);
}
else if(c[0]=='A'){
if(c[1]=='1'){
scanf("%lld%lld",&a,&b);
int y=find(a);
w[a]+=b;
mx[y]=max(mx[y],w[a]);
tot=max(tot,w[a]+type[y]);
}
else if(c[1]=='2'){
scanf("%lld%lld",&a,&b);
int y=find(a);
type[y]+=b;
tot=max(tot,mx[y]+type[y]);
}
else {
scanf("%lld",&a);
alltyp+=a;
}
}
else if(c[0]=='F'){
if(c[1]=='1'){
scanf("%lld",&a);
int y=find(a);
printf("%lld\n",w[a]+type[y]+alltyp);
}
else if(c[1]=='2'){
scanf("%lld",&a);
int y=find(a);
printf("%lld\n",mx[y]+type[y]+alltyp);
}
else {
printf("%lld\n",tot+alltyp);
}
}
}
return 0;
}
```