带权/拓展域并查集
带权/拓展域并查集:
定义:
根据并查集的定义:并查集实际上是一个若干棵树组成的森林。
因此,我们可以在树中的每一条边上记录一个权值,即:
维护一个数组 \(d\),用 \(d[x]\) 保存节点 \(x\) 到父节点 \(fa[x]\) 之间的边权。
在每次路径压缩之后,每个访问过的节点都会直接指向树根。
如果我们同时更新这些节点的 \(d\) 值,就可以利用路径压缩过程来统计每个节点到树根的路径上的信息。
例题:
题意:
有 \(n\) 列,\(n\) 个物品,\(i\) 个物品放在第 \(i\) 列,有 \(m\) 个指令:
M i j
表示让 \(i\) 物品对应一列放在 \(j\) 物品一列的后面。
C i j
查询 \(i,j\) 是否处于同一列,输出两者距离。
分析:
正常的并查集合并都好理解,但是怎么搜索路径呢?考虑额外开一个数组 \(d[x]\) 记录战舰 \(x\) 和 \(fa[x]\) 之间的边的权值。
在路径压缩把 \(x\) 直接指向树根的同时,把 \(d[x]\) 更新从 \(x\) 到树根的路径上的所有边权之和。
所以,\(get\) 函数更改成这样:
int get(int x){
if(x==fa[x]) return x;
int root=get(fa[x]); d[x]+=d[fa[x]];
return fa[x]=root;
}
两个操作变成这样:
- 合并并查集。
- 查询是否属于同一个并查集,如果存在,则输出 \(|d[j]-d[i]|-1\),因为保存了 \(j\) 前面的物品的数量和 \(i\) 前面的物品的数量。
代码:
// P1196 [NOI2002] 银河英雄传说
#include<bits/stdc++.h>
using namespace std;
const int N=3e4+5;
int fa[N],n,t,d[N],sizes[N];//size为记录一列上有几个飞船
int get(int x){
if(x==fa[x])//查询到了根节点,则返回根节点
return x;
int root=get(fa[x]);//递归计算集合代表 //root为树根
d[x]+=d[fa[x]];//维护d数组,对边权求和
return fa[x]=root;////直接指向树根
}
void merge(int x,int y){
x=get(x),y=get(y);
fa[x]=y,d[x]=sizes[y];//排在x前面是a,d[x]的大小是前面有多少个飞船
sizes[y]+=sizes[x];//合并,算总体飞船数量
}
int main(){
scanf("%d",&t);
for(int i=1;i<=30000;i++) fa[i]=i,sizes[i]=1;//fa表示排在x号战舰前面的那个战舰的编号
while(t--){
int i,j; char ch[5]; scanf("%s",ch);
scanf("%d%d",&i,&j);
if(ch[0]=='M') merge(i,j);
else{
if (get(i)==get(j)) cout<<abs(d[i]-d[j])-1<<endl;
else puts("-1");
}
}
system("pause");
return 0;
}
进一步探索:
如果在某些问题中,传递关系不只是一种,而且这些传递关系能够相互导出,此时可以使用拓展域或者边带权的并查集解决。
拓展域并查集:
拓展域并查集常常用来维护多组关系的集合合并问题,主要方法为:
将一个点拆分成多个点,在不同的关系中使用,维护多个关系。
例题:
[NOIP2010 提高组] 关押罪犯
题意:
给定 \(m\) 对罪犯之间的怨气值和 \(n\) 个监狱,监狱的怨气值是其中怨气值最大的对的怨气值。合理分配关监狱方案,使得怨气值的最大值最小。
分析:
我们设 \([1,n]\) 是人的并查集, \([n+1,2n]\) 是监狱的并查集,根据怨气从大到小排列后,尽量排到相对立的监狱,也就是 \(i\rightarrow j+n,j\rightarrow i+n\).
然后找到在已经在同一个监狱的对立情况,该怨气值即是答案。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
struct node{
int a,b,c;
}t[N];
bool cmp(node a,node b){
return a.c>b.c;
}
int n,m;
int fa[N];
int get(int x){
if(x==fa[x]) return x;
return fa[x]=get(fa[x]);
}
signed main(){
cin>>n>>m;
for(int i=1;i<=(n<<1);i++) fa[i]=i;
for(int i=1;i<=m;i++){
scanf("%lld%lld%lld",&t[i].a,&t[i].b,&t[i].c);
}
sort(t+1,t+m+1,cmp);
for(int i=1;i<=m;i++){
int x=get(t[i].a),y=get(t[i].b);
if(x==y) {printf("%d\n",t[i].c);system("pause"); return 0;}
fa[y]=get(t[i].a+n); fa[x]=get(t[i].b+n);//加到对方属于的集合。s
}
cout<<0<<endl;
return 0;
}