并查集

简单的前置知识。

并查集

1.基本操作

并查集维护的是一种关系,这种关系具有传递性,并且一般具有对称性。

1)merge: 将两个集合合并

2)find: 查询代表元素

1
2
3
4
5
6
7
8
9
10
11
int find(int x)
{
if (p[x]==x) return p[x];
return p[x]=find(p[x]);
}

void merge(int a,int b)
{
int root_a=find(a),root_b=find(b);
p[root_b]=root_a;
}

易得,并查集是树形结构。

2.优化

1)路径压缩:$O(\log(n))$

2)按秩合并:$O(\log(n))$

一般来说,只需要用第一个即可。

由于常数很小,实际用时,可以看做 $O(1)$

3.扩展

1)记录集合大小:一般保存到代表元素。

2)每个点到根节点的距离:绑定到每一个元素。

比如说 [NOI2001] 食物链

题目传送门 Luogu

题目传送门 AcWing

有两种处理方式:边带权和扩展域。

我们后面再讲。

4.例题

T1:格子游戏

来自 《信息学奥赛一本通》(但多方都没有找到)

将连边的两个点合并到一个并查集即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#define get(i,j) (i-1)*n+j
using namespace std;

const int N=205*205;
int n,m,p[N];

int find(int x)
{
if (p[x]==x) return p[x];
return p[x]=find(p[x]);
}

void merge(int a,int b)
{
int root_a=find(a),root_b=find(b);
p[root_b]=root_a;
}

int main()
{
cin>>n>>m;
char op[5];
int a,b;
for (int i=1;i<=n*n;++i) p[i]=i;
for (int i=1;i<=m;++i)
{
scanf("%d%d%s",&a,&b,op);
int x=get(a,b);
int y;
if (op[0]=='D') y=get(a+1,b);
else y=get(a,b+1);
x=find(x);y=find(y);
if (x==y)
{
printf("%d",i);
return 0;
}
merge(x,y);
}
puts("draw");
return 0;
}

T2:搭配购买

题目传送门 Luogu

注意维护根节点的价钱和价值。

最后是一个 01 背包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;

const int N=1e4+10;

int p[N],v[N],w[N],n,m,f[int(1e7)],tot;

int find(int x)
{
if (p[x]==x) return p[x];
return p[x]=find(p[x]);
}

int main()
{
cin>>n>>m>>tot;
for (int i=1;i<=n;++i) p[i]=i;
for (int i=1;i<=n;++i) scanf("%d %d",v+i,w+i);
int a,b,x,y;
while (m--)
{
scanf("%d %d",&a,&b);
x=find(a);y=find(b);
if (x!=y)
{
w[x]+=w[y];
v[x]+=v[y];
p[y]=x;
}
}
for (int i=1;i<=n;++i)
{
if (p[i]!=i) continue;
for (int j=tot;j>=v[i];--j)
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[tot]<<endl;
return 0;
}

T3:[NOI2015] 程序自动分析

题目传送门 Luogu

题目传送门 AcWing

考虑由于等于具有传递性,而不等于不具有传递性,所以将相互等于的数维护一个并查集即可。

数据范围较大,需要离散化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<unordered_map>
using namespace std;

const int N=1e5+10;
unordered_map <int,int> S;
int n,m,p[N*2];

struct Q{
int a,b,e;
}q[N];

int get(int x)
{
if (S.count(x)==0) S[x]=++n;
return S[x];
}

int find(int x)
{
if (x==p[x]) return x;
return p[x]=find(p[x]);
}

int main()
{
int t,x,y,e;cin>>t;
while (t--)
{
n=0;
S.clear();
cin>>m;
for (int i=1;i<=m;++i)
{
scanf("%d %d %d",&x,&y,&q[i].e);
q[i].a=get(x),q[i].b=get(y);
}
for (int i=1;i<=n;++i) p[i]=i;
for (int i=1;i<=m;++i)
if (q[i].e) p[find(q[i].a)]=find(q[i].b);
bool flag=1;
for (int i=1;i<=m;++i)
if (!q[i].e)
if (find(q[i].a)==find(q[i].b))
{
flag=0;
break;
}
puts(flag?"YES":"NO");
}
return 0;
}

T4:[NOI2002] 银河英雄传说

题目传送门 Luogu

题目传送门 AcWing

如果没有询问距离,怎么办?

很简单,直接并查集即可。

可以发现,从 $x$ 到 $y$ 的距离,可以与 $x$,$y$ 到根节点的距离有关。

每当合并时,将被合并的点加一个前面的 $size[x]$。

前面讲到,因为是一个树形结构,所以它的子节点要路径压缩时,必定会经过被合并的点,并加上该点的 $d[x]$。

同时可以得到:

注意特判 $x=y$ 的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;

const int N=3e5+10;
int p[N],n,d[N],s[N];

int find(int x)
{
if (p[x]==x) return x;
int rt=find(p[x]);
d[x]+=d[p[x]];
return p[x]=rt;
}

int main()
{
for (int i=0;i<N;++i) p[i]=i,s[i]=1;
cin>>n;
char op[2];
int a,b;
while (n--)
{
scanf("%s%d%d",op,&a,&b);
int x=find(a),y=find(b);
if (op[0]=='M')
{
if (x==y) continue;
d[x]=s[y];
s[y]+=s[x];
p[x]=y;
}
else
{
if (x!=y) puts("-1");
else printf("%d\n",max(abs(d[a]-d[b])-1,0));
}
}
return 0;
}

T5:奇偶游戏

题目传送门 AcWing

观察到 $N$ 很大,而 $M$ 相对小得多。

考虑离散化。

记录前缀和 $s[i]=\sum_{j=1}^{i}a[j]$

如果 $l-r$ 中有奇数个,等价于 $s(r)$ 与 $s(l-1)$ 奇偶性不同。

每次将其奇偶性得到,如果出现矛盾,则不行。

但是,如果没有矛盾,一定可以吗?( )

答案是肯定的。

直接使 $a[i]=|s[i]-s[i-1]|$ ,就一定满足条件。

现在,我们要维护有关联的点,作为一个并查集。

我们先用边带权做一次。

$d[x]$ 表示 $x$ 到根节点的奇偶性,则 $dis(x,y)=d[x]+d[y]$。

每当插入一个关系时,我们应该先判断是否合法。

如果不在一个集合,那么我们连接 $px$ 与 $py$,边权为 $d[px]=d[x]$ ^ $d[y]$ ^ $ans$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<unordered_map>
using namespace std;

const int N=2e5+10;
unordered_map <int,int> S;
int n,m,tot,p[N],d[N];

int get(int x)
{
if (S.count(x)==0) S[x]=++n;
return S[x];
}

int find(int x)
{
if (p[x]==x) return p[x];
int rt=find(p[x]);
d[x]^=d[p[x]];
return p[x]=rt;
}

int main()
{
char op[10];
int a,b;
cin>>tot>>m;
for (int i=0;i<N;++i) p[i]=i;
for (int i=1;i<=m;++i)
{
scanf("%d %d %s",&a,&b,op);
a=get(a-1);b=get(b);
int x=find(a),y=find(b);
if (op[0]=='e')
{

if (x!=y) p[x]=y,d[x]=d[a]^d[b];
else
{
if (d[a]^d[b])
{
cout<<i-1<<endl;
return 0;
}
}
}
else if (op[0]=='o')
{
if (x!=y) p[x]=y,d[x]=d[a]^d[b]^1;
else
{
if (d[a]^d[b]==0)
{
cout<<i-1<<endl;
return 0;
}
}
}
else while (1);
}
cout<<m<<endl;
return 0;
}

扩展域先鸽着。