并查集
基础并查集
算法学习
问题模型: 用于实现集合合并的数据结构。例如:朋友的朋友是朋友,判断 u , v u,v u,v 二人是否是朋友。
实质: 并查集实际上是一个树形结构,但是它没有连向子节点的边,只有连向父节点的边。 f a [ x ] fa[x] fa[x] 记录 x x x 的父亲。我们认为:属于同一祖先的两个点属于同一个集合。
算法实现
初始化: 定义 f a [ i ] = i fa[i]=i fa[i]=i 表示第 i i i 个人初始自己就是自己的父亲。即:各自属于一个集合。
寻找祖先: 不停的通过 f a [ x ] fa[x] fa[x] 找父亲,直到找到 f a [ x ] = x fa[x]=x fa[x]=x 的点即可。
合并操作: 对于朋友 x , y x,y x,y ,令 p 1 , p 2 p1,p2 p1,p2 分别为他们的祖先。令 f a [ p 1 ] = p 2 fa[p1]=p2 fa[p1]=p2 即可关联他们。
路径压缩: 对于特殊数据,数据可能会退化成一条链。此时每次找祖先的代价(不停的 f a [ x ] fa[x] fa[x] 找父亲)会退化为 O ( n ) O(n) O(n) 。可以使用路径压缩的方式:对于点 x x x ,在其找到祖先的过程中,会经过若干个点,最后找到祖先 p p p 。让经过的所有点的 f a [ ] = p fa[]=p fa[]=p 。将这条找祖先的链,之间变成一个高度为 2 2 2 的树。下次找的时候,就可以 O ( 1 ) O(1) O(1) 找到。
模板
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int fa[N];
int find(int x){
if(x==fa[x])return x;
else return fa[x]=find(fa[x]);
}
void solve(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)fa[i]=i;
while(m--){
int x,y,op;
cin>>x>>y>>op;
int p1=find(x),p2=find(y);
if(op==1){//判断关系
if(p1==p2)cout<<"属于同一个集合\n";
else cout<<"不属于同一个集合\n";
}else{//合并
if(p1!=p2){
fa[p1]=p2;
}
}
}
}
启发式并查集
算法学习
分析:
-
使用路径压缩虽然可以优化时间复杂度,但是却丢失了 x x x 到其祖先这条路径的所有信息,只保留了最后的结果。
-
使用朴素的并查集虽然可以保存路径的信息,但是却会被卡成 O ( n ) O(n) O(n) 的暴力复杂度。
-
是否可以拥有一个二者兼得的并查集???
实质:
- 并查集,实质上就是一个树。
- 没有路径压缩的并查集是一棵高度无法保证的树,有路径压缩的并查集是一棵高度为 2 2 2 的树。
优化:
- 尝试对朴素的并查集进行优化。
- 合并的过程,原来是两个树合并在一起
- 现在并查集的祖先节点有个高度,合并的时候令高度高的并查集,作为根。
- 分析高度变化:
- 两个高度一样均为 h h h 的并查集合并,得到的并查集高度为 h + 1 h+1 h+1 。
- 两个高度不一样的并查集合并,得到的并查集高度为 m a x ( h 1 , h 2 ) max(h_1,h_2) max(h1,h2) 。
- 与普通并查集想比,只需要多维护一个高度信息即可。
模板
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int fa[N],h[N];
int find(int x){
if(x==fa[x])return x;
else return find(fa[x]);
}
void solve(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)fa[i]=i;
while(m--){
int x,y,op;
cin>>x>>y>>op;
int p1=find(x),p2=find(y);
if(op==1){//判断关系
if(p1==p2)cout<<"属于同一个集合\n";
else cout<<"不属于同一个集合\n";
}else{//合并
if(p1!=p2){
if(h[p1]>h[p2])fa[p2]=p1;
else if(h[p1]<h[p2])fa[p1]=p2;
else{
fa[p1]=p2;
h[p2]++;
}
}
}
}
}
带权并查集
算法学习
前言: 之前的并查集只有父亲这一个信息,如果想要有更多信息,例如:边权。该如何去拓展它?
模型: 一维数轴上有 n n n 个点,初始不知道他们的相对距离。 q q q 次操作,每次操作得到信息 ( x , y , d ) (x,y,d) (x,y,d) ,表示从 x x x 在 y y y 的左边 d d d 距离。对于第 i i i 条消息,请问是否会与前面得到的信息冲突,若一致则输出 y e s yes yes ,若不一致则忽略该信息。
分析:
- 设 f a [ x ] fa[x] fa[x] 为 x x x 的父亲, d i s [ x ] dis[x] dis[x] 表示 f a [ x ] fa[x] fa[x] 在 x x x 左边的距离。
- 对于信息
(
x
,
y
,
d
)
(x,y,d)
(x,y,d) ,
- 若 x , y x,y x,y 属于同一个集合,显然其距离已经被确定。距离为: d i s [ y ] − d i s [ x ] dis[y]-dis[x] dis[y]−dis[x] 。
- 若
x
,
y
x,y
x,y 不属于同一个集合,合并他们即可,但是合并的时候,要同时更新
d
i
s
dis
dis 数组(在路径压缩和合并的时候,均要更新)。
- 路径压缩时: d i s [ x ] dis[x] dis[x] 表示 x x x 到此时的父亲 f a [ x ] fa[x] fa[x] 的距离,路径压缩完 d i s [ f a [ x ] ] dis[fa[x]] dis[fa[x]] 表示父亲到其父亲的距离。由于压缩后, x x x 的父亲会变为父亲的父亲,则: d i s [ x ] = d i s [ x ] + d i s [ f a [ x ] ] dis[x]=dis[x]+dis[fa[x]] dis[x]=dis[x]+dis[fa[x]] 。( x x x 到父亲的距离加上父亲到父亲的父亲的距离)。
- 合并时: f a [ p 2 ] = p 1 fa[p2]=p1 fa[p2]=p1 ,由于 d = d= d= x x x 在 y y y 左边的距离, d i s [ x ] dis[x] dis[x] 表示 p 1 p1 p1 在 x x x 左边的距离, d i s [ y ] dis[y] dis[y] 表示 p 2 p2 p2 在 y y y 左边的距离。则有: d i s [ p 2 ] − d i s [ x ] + d i s [ y ] = d dis[p2]-dis[x]+dis[y]=d dis[p2]−dis[x]+dis[y]=d 得到: d i s [ p 2 ] = d i s [ y ] − d i s [ x ] + d dis[p2]=dis[y]-dis[x]+d dis[p2]=dis[y]−dis[x]+d 。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e5+10;
int fa[N];
LL dis[N];
int find(int x){
if(x==fa[x])return x;
else {
int t=fa[x];
fa[x]=find(fa[x]);
dis[x]+=dis[t];
return fa[x];
}
}
void solve() {
int n,q;
cin>>n>>q;
for(int i=1;i<=n;i++){
fa[i]=i;
dis[i]=0;
}
while(q--){
int x,y;
LL d;
cin>>x>>y>>d;
int p1=find(x),p2=find(y);
if(p1==p2){
if(d==dis[y]-dis[x])cout<<"与前面信息一致\n";
else cout<<"与前面信息不一致\n";
}else{
fa[p2]=p1;
dis[p2]=dis[y]-dis[x]+d;
}
}
}
例题练习
例题1:P8779
题目描述:
n
n
n 个数,不知道数值。
m
m
m 个已知信息,每个信息给出
l
i
,
r
i
,
v
i
l_i,r_i,v_i
li,ri,vi 表示
∑
i
=
l
r
a
i
=
v
\sum_{i=l}^r a_i=v
∑i=lrai=v 。
q
q
q 个询问,每个询问给出
l
,
r
l,r
l,r ,询问
∑
i
=
l
r
\sum_{i=l}^r
∑i=lr ,若无法确定,输出 NOKNOWN
。
n
,
m
,
q
<
=
1
e
5
,
v
i
∈
[
−
1
e
12
,
1
e
12
]
n,m,q<=1e5,v_i\in[-1e12,1e12]
n,m,q<=1e5,vi∈[−1e12,1e12] 。
问题分析: 转化为前缀和之后,就是带权并查集模板题(注意变成了 n + 1 n+1 n+1 个点)。
int fa[N];
LL dis[N];
int find(int x){
if(x==fa[x])return x;
else{
int t=fa[x];
fa[x]=find(fa[x]);
dis[x]+=dis[t];
return fa[x];
}
}
void solve() {
int n,m,q;
cin>>n>>m>>q;
for(int i=0;i<=n;i++){
fa[i]=i;
dis[i]=0;
}
for(int i=0;i<m;i++){
int l,r;
LL v;
cin>>l>>r>>v;
int p1=find(l-1),p2=find(r);
if(p1!=p2){
fa[p2]=p1;
dis[p2]=dis[l-1]-dis[r]+v;
}
}
while(q--){
int l,r;
cin>>l>>r;
int p1=find(l-1),p2=find(r);
if(p1!=p2)cout<<"UNKNOWN\n";
else cout<<dis[r]-dis[l-1]<<'\n';
}
}
例题2:P1196
题目描述: n n n 个战舰,编号为 [ 1 , n ] [1,n] [1,n] ,初始排成一排(一排 n n n 列)。 m m m 次操作,操作 1 1 1 : ( M , u , v ) (M,u,v) (M,u,v) 表示编号为 u u u 的战舰所在列整体排到编号为 v v v 的战舰列整体的后面;操作 2 2 2 : ( C , u , v ) (C,u,v) (C,u,v) 表示如果 u , v u,v u,v 不属于同一列,则输出 − 1 -1 −1 ,否则输出两个战舰之间的战舰数量。 n = 30000 , m < = 5 e 5 n=30000,m<=5e5 n=30000,m<=5e5 。
问题分析:
-
按每列为一个集合,维护带权并查集,初始每一列独立。
-
对于操作1:
-
u u u 的根接到 v v v 的根上,并维护 d i s , s i z dis,siz dis,siz 。其中 d i s [ i ] dis[i] dis[i] 表示编号为 i i i 的点与其所属列的第一个点的位置差值。
-
具体的:设 u u u 的根为 p 1 p1 p1 , v v v 的根为 p 2 p2 p2 。则这条信息可以转化为: p 1 p1 p1 与 v v v 所属列的第一个点的位置差值为 s i z [ p 2 ] siz[p2] siz[p2] 。
-
-
对于操作 2 2 2 :
- 若属于同一个集合,则直接做差即可得到他们中间的战舰个数。
int find(int x){
if(x==fa[x])return fa[x];
else {
int t=fa[x];
fa[x]=find(fa[x]);
dis[x]+=dis[t];
return fa[x];
}
}
int main() {
int n=30000,m;
for(int i=1;i<=n;i++)fa[i]=i,siz[i]=1;
cin>>m;
while(T--){
int u,v;
char op;
cin>>op>>u>>v;
if(op=='M'){
int p1=find(u),p2=find(v);
fa[p1]=p2;
dis[p1]=siz[p2];
siz[p2]+=siz[p1];
}else{
int p1=find(u),p2=find(v);
if(p1!=p2)cout<<-1<<endl;
else cout<<abs(dis[u]-dis[v])-1<<endl;
}
}
}
例题3:TOJ1003
题目描述: 给定 n n n 表示有一个 n n n 个元素的 01 01 01 序列,初始不知道每个元素的值。 m m m 条消息,每条消息给出 ( l , r , o p ) (l,r,op) (l,r,op) ,表示区间 [ l , r ] [l,r] [l,r] 内有奇数/偶数个 1 1 1 。判断从哪句话开始,一定无法满足限制。 n < = 1 e 9 , m < = 5000 n<=1e9,m<=5000 n<=1e9,m<=5000 。
问题分析: 区间
01
01
01 个数就是区间异或和,转化为前缀异或和之后,就变成 pre[l-1]^pre[r]=op
。直接用带权并查集维护异或的关系即可。由于点数量过多,因此需要采取动态开点。
int fa[N],cnt;
map<int,int>id,dis;
void newnode(int x){
if(id.find(x)==id.end()){
id[x]=++cnt;
fa[cnt]=cnt;
dis[cnt]=0;
}
}
int find(int x){
if(x==fa[x])return fa[x];
else{
int t=fa[x];
fa[x]=find(fa[x]);
dis[x]=dis[x]^dis[t];
return fa[x];
}
}
void solve(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
int l,r,op;
cin>>l>>r>>op;
newnode(l-1),newnode(r);
int x=id[l-1],y=id[r];
int p1=find(x),p2=find(y);
if(p1==p2){
if(dis[x]^dis[y]==op)continue;
else{
cout<<i<<'\n';
return;
}
}else{
fa[p1]=p2;
dis[p1]=dis[x]^dis[y]^op;
}
}
cout<<-1<<'\n';
}