ZR并查集专题

时间:2022-10-29 20:45:29

ZR并查集专题

并查集,作为一个基础算法,对于初学者来说,下面的代码是维护连通性的利器

return fa[x] == x ? x : fa[x] = getf(fa[x]);

所以,但是这对并查集的理解还仅仅停留在维护连通性上,如果能够对并查集的理解继续深刻,就会接触到维护元素之间的关系

主要表现在边带权和扩展域的用法

我们将从一道例题引出这两种写法

noi2001食物链(Luogu2024)

扩展域:

这种关系具有传递性,同时要维护多种关系的时候,一般的合并很难做到这一点

这时候我们要引出扩展域

所谓扩展域,顾名思义,就是将并查集维护的东西进行扩展

此题中,我们不但要维护同类.还要维护吃与被吃

那么我们就把节点\(x\)拆成三个点\(x_{self}\)同类域,\(x_{eat}\)捕食域以及\(x_{enemy}\)天敌域

每次读进来一个同类操作\(x,y\)

我们如果发现\(x_{self}\)和\(y_{eat}\)或者\(x_{self}\)和\(y_{enemy}\)在同一集合

那么这次操作显然不合法

否则

将x,y三个集合一一合并

同理

如果是读入的捕食关系

如果\(x_{self }\)与\(y_{self}\)或者\(x_{enemy}\)与\(y_{self}\)在同一集合

那么不合法

否则,x吃的就是y,x的天敌就是y吃的,x是y的天敌一一合并

代码

边带权

边带权并查集好恶心啊

这是这道题的第二种做法,不需要对并查集进行扩展,我们利用带权并查集解决这个问题

首先,我们并查集维护一下距离

\(x,y\)的距离定义为\(dis_x - dis_y\)我们定义这个值在模\(3\)意义下为\(0\)则代表\(x,y\)是同类,为\(1\)代表\(x\)被\(y\)吃,为\(2\)则代表\(x\)吃\(y\)

我们接下来考虑这种情况

默认大家已经会带权并查集的基本写法

对于\(1\)询问如果\(x,y\)已经在同一集合,那么他们的关系我们可以简单计算出来,并且判断是否合法,如果\(x,y\)不在统一集合,也就是我们要确定他们的关系

ZR并查集专题

也就是说

我们已经知道\(a,b\)现在要把\(f_y\)接到\(f_x\)上

也就是要满足\(a-b-c \equiv 0 \pmod {3}\)

这个还是比较简单的方程

注意一定是把\(f_y\)接到\(f_x\)上

因为如果把\(f_x\)接到\(f_y\)上就不是这个方程了

同理,吃的操作我们也可以这么做

式子变一下就好了

如果理解了上面这东西,这里应该改也不难了

上面可以说是并查集的进一步扩展,能够维护敌对或者同类等多样的关系

那如果我们再加深一下对于并查集的理解,请看下面的题

ZR855

给定一棵 \(n\) 个点的树,每个点有点权 \(w_i\),定义一条链 \((a,b)\) 的权值 \(V(a,b)\) 为:这条链上的所有点的最大点权

现在你需要对于每个点 \(x\) 求:所有点到他的链的最大点权之和,也就是 ​\(\sum_{i = 1}^ n V(i,x)\)

长得非常淀粉质的样子,但这确确实实是一道并查集的题目

首先,观察题目性质,我们可以发现一条边的边权我们可以定义为他链接的两个点的点权的最大值

那么就有一个比较套路的想法,把所有的边按照边权排序之后一条一条地加入到树中,并用并查集维护每一次操作的贡献,如果这道题目要求的是求所有路径的权值之和,那么想到这里就完成了.但是如果要求对于给个数,都要输出到其他点的距离之和,我们就要把贡献具体到点上,而不是统一计算

接下来考虑,如何计算这个东西,考虑一下并查集合并的过程

ZR并查集专题

\(v\)合并到\(u\)上的时候,发现\(u\)子树和\(v\)子树中都多了\(w(u,v)\)的贡献,我们就类似线段的tag每次在\(u\)处记录下\(v\)的贡献,\(v\)同理,但是\(v\)合并到\(u\)之后,之前的\(tag\)是不能共享的,那我们就把这一部分减去

请注意:如果是用这种方法,一定要只按秩合并 ,只按秩合并 ,只按秩合并!!!!!

因为路径压缩维护这个东西会非常麻烦

我们按秩合并能够保证最终并查集的高度不会超过\(\log\),对于查询操作,我们可以暴力的从他的位置向上跳,一路统计贡献.

特别的,如果查询都是在合并完成之后,我们可以最终dfs整棵树得到某个节点的值

那如果我们非要路径压缩怎么办?

想一想,刚才路径压缩为什么维护tag会特别麻烦?

因为压缩之后tag会重复计数,主要原因是根导致的

那么我们就借鉴克鲁斯卡尔重构树的思想

每次合并的时候我们建立一个虚点,然后把两个点都连在虚点上,这样我们就可以 肆无 忌 惮 的路径压缩了,而且由于\(v\)不是\(u\)的儿子,所以也不会有tag多余的部分,只需要简单的统计贡献即可

给出这道题的两种做法

路径压缩:

#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<ctime>
#include<cmath>
#include<set>
#include<map>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 1e6 + 3;
struct edge{
int from;
int to;
int data;
}e[N << 1];
int head[N],fa[N],size[N];
int w[N];LL val[N];
int n;
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
inline bool cmp(edge x,edge y){
return x.data < y.data;
}
inline int getf(int x){
// printf("%d %d\n",x,fa[x]);
if(fa[x] == x) return x;
int f = getf(fa[x]);
val[x] += val[fa[x]];
fa[x] = f;
return fa[x];
}
int main(){
n = read();
for(int i = 1;i < n;++i){
e[i].from = read();
e[i].to = i + 1;
}
for(int i = 1;i <= n;++i) fa[i] = i,w[i] = read(),size[i] = 1;
for(int i = n + 1;i <= 2 * n;++i) fa[i] = i,size[i] = 0;
for(int i = 1;i < n;++i) e[i].data = max(w[e[i].from],w[e[i].to]);
sort(e + 1,e + n,cmp);
// for(int i = 1;i < n;++i) printf("%d %d %lld\n",e[i].from,e[i].to,e[i].data);
int cnt = n;
for(int i = 1;i < n;++i){
int x = getf(e[i].from),y = getf(e[i].to);
val[x] += 1ll * size[y] * e[i].data;
val[y] += 1ll * size[x] * e[i].data;
size[++cnt] = size[x] + size[y];
fa[x] = cnt;
fa[y] = cnt;
}
for(int i = 1;i <= n;++i){
getf(i);
printf("%lld ",val[i] + w[i]);
}
return 0;
}

按秩合并:

#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#define LL long long using namespace std;
const int N = 4e5 + 311;
int n,m,tot;
LL ans[N];
int fa[N],size[N];
LL w[N];
vector <int> G[N];
struct edge{
int from;
int to;
LL data;
}e[N << 1];
inline int getf(int x){
return x == fa[x] ? x : getf(fa[x]);
}
inline bool cmp(edge x,edge y){
return x.data < y.data;
}
inline int read(){
int v = 0,c = 1;char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') c = -1;
ch = getchar();
}
while(isdigit(ch)){
v = v * 10 + ch - 48;
ch = getchar();
}
return v * c;
}
inline void dfs(int x,int f){
ans[x] += ans[f];
for(int i = 0;i < (int)G[x].size();++i){
int y = G[x][i];
if(y == f) continue;
dfs(y,x);
}
}
int main(){
n = read();
for(int i = 2;i <= n;++i){
int x = read();
e[++tot] = (edge){i,x,0};
}
for(int i = 1;i <= n;++i) w[i] = read();
for(int i = 1;i <= n + 13;++i) fa[i] = i,size[i] = 1;
for(int i = 1;i <= tot;++i) e[i].data = max(w[e[i].from],w[e[i].to]);
// for(int i = 1;i <= tot;++i){
// printf("%d %d %lld\n",e[i].from,e[i].to,e[i].data);
// }
sort(e + 1,e + tot + 1,cmp);
for(int i = 1;i <= tot;++i){
int x = getf(e[i].from),y = getf(e[i].to);
if(size[x] > size[y]) swap(x,y);
if(x == y) continue;
// printf("%d %d %lld %lld\n",x,y,ans[x],ans[y]);
G[y].push_back(x);
ans[y] += e[i].data * size[x]; ans[x] += e[i].data * size[y];
ans[x] -= ans[y];
size[y] += size[x];
fa[x] = y;
}
dfs(getf(1),0);
for(int i = 1;i <= n;++i) printf("%lld ",ans[i] + w[i]);
return 0;
}

总结:如果时间复杂度不是特别卡,那么建虚点的做法还是非常优秀的,实测虽然点数多以一倍,但是跑的并不劣于按秩合并的做法