原理
令
p
[
x
]
p[x]
p[x]表示
x
x
x到
f
a
[
x
]
fa[x]
fa[x]的路径权值,有下式:
s
[
x
]
=
p
[
x
]
+
s
[
f
a
[
x
]
]
s[x]=p[x]+s[fa[x]]
s[x]=p[x]+s[fa[x]]
可以在路径压缩时完成这一点,这类题的关键是求出两个根节点
u
,
v
u,v
u,v的关系(即求
s
[
u
]
s[u]
s[u],默认
v
v
v为新的根节点)
A. 箱子
有n个箱子,初始时每个箱子单独为一列;
接下来有p行输入,M, x, y 或者 C, x;
对于M,x,y:表示将x箱子所在的一列箱子搬到y所在的一列箱子上;
对于C,x:表示求箱子x下面有多少个箱子;
分析:
- 求深度一定是累加,跑一次后就链接到根节点了(路径压缩的特性,本身路径长度会改变,只是点的权值不会变),根节点权值为0,故不会影响结果。如果累加式出现了常数,多次调用时一定会越加越大。
- 这道题可以直接根节点相连,虽然路径并非直接连在一起,但将路径累加在权值上即可
- 权值可以记录一些路径压缩下改变的东西,这就是它的用处
#include <cstdio>
#include <algorithm>
#include <iostream>
using namespace std;
const int MAXN = 3e4 + 5;
int n, x, y, fa[MAXN], s[MAXN], d[MAXN]; //设底部为根节点
char c;
void makeset() {
for (int i = 1; i <= 30000; i++) {
fa[i] = i;
s[i] = 1;
d[i] = 0;
}
}
int findset(int x) {
if (fa[x] != x) {
int t = fa[x];
fa[x] = findset(fa[x]);
d[x] += d[t]; //关键 (表示到父节点的距离,+1会错 )
}
return fa[x];
}
void unionset(int x, int y) {
int u = findset(x), v = findset(y);
if (u == v)
return;
else {
fa[u] = v; //父节点直接设置成根节点
d[u] += s[v]; //关键(加距离)
s[v] += s[u]; //关键(加树的总深度)
}
}
int main() {
scanf("%d", &n);
makeset();
for (int i = 1; i <= n; i++) {
cin >> c;
if (c == 'M') {
scanf("%d%d", &x, &y);
unionset(x, y);
} else {
scanf("%d", &x);
int u = findset(x);
printf("%d\n", d[x]);
}
}
}
B.食物链
分析:用带权并查集
x到根节点root的路径权值s[x]就表示了x与root的关系。若x,y在同一集合中,则s[x]和s[y]的差就是x,y的关系。
可以用0,1,2表示不同的种类,由于构成一个环,所以可以模3解决。
另外:s[x,y]=s[x,z]+s[z,y],显然可以用路径压缩。
对于x,y的根节点u,v,要合并u,v两颗树,只需要:
f
a
[
u
]
=
v
;
fa[u]=v;
fa[u]=v;
s
[
u
]
=
(
t
y
p
d
−
1
−
s
[
x
]
+
s
[
y
]
+
3
)
m
o
d
3
;
s[u]=(typd-1-s[x]+s[y]+3) mod 3;
s[u]=(typd−1−s[x]+s[y]+3)mod3;
式二可以用向量证明,不过感性理解即可。
注意并查集只能合并根节点,x,y,是不能动的。其实把u接到y后面也没错。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
using namespace std;
const int maxn=100005;
void read(int &x) {
int f=1;x=0;char c=getchar();
while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9') {x=(x<<1)+(x<<3)+(c^48);c=getchar();}
x*=f;
}
int n,q,fa[maxn],s[maxn],tot;
int find(int x) {
if(fa[x]!=x) {
int t=fa[x];
fa[x]=find(fa[x]);
s[x]=(s[x]+s[t])%3;
}
return fa[x];
}
void unionset(int x,int y) {
int u=find(x),v=find(y);
if(u==v) return;
fa[u]=v;
}
int main() {
read(n),read(q);
for(int i=1;i<=n;i++) fa[i]=i,s[i]=0;
for(int i=1;i<=q;i++) {
int x,y,typd;
read(typd),read(x),read(y);
if(x>n||y>n||(typd==2&&x==y)) {
tot++;
continue;
}
int u=find(x),v=find(y);
if(u==v&&(s[x]-s[y]+3)%3!=typd-1) tot++;
else if(u!=v) {
fa[u]=v;
s[u]=(typd-1-s[x]+s[y]+3)%3;
}
}
printf("%d",tot);
}
C.有多少答案是错误的
题目描述
现在有n个数(1~n)和m次询问,每次询问给出一个区间[a,b]以及这个区间内的价值总和,现在请你找出给出的m次查询中,有多少个询问是和前面不相互矛盾的已存在的询问相互矛盾。
输入格式
第一行包含两个整数n,m(1<=n<=200000, 1<=m<=40000),表示数据的范围以及查询的次数。
接下来每行3个数字a,b,s,表示区间[a,b]内的价值总和为s。其中0<a<=b<=n。
题目保证所有数据的s的和在int范围内。
ps:因为老师们只是讲了思路,所以我来说明一下具体怎么实现
分析:
首先,本题要明确:一个区间的和可能是负的,所以只有根据前面题目中给出的点找矛盾,如:
1 10 10
4 6 50
这个样例是正确的
本题的处理方法和上题一样:
f
a
[
u
]
=
v
;
fa[u]=v;
fa[u]=v;
s
u
m
[
u
]
=
s
−
s
u
m
[
x
]
+
s
u
m
[
y
]
;
sum[u]=s-sum[x]+sum[y];
sum[u]=s−sum[x]+sum[y];
#include<cstdio>
using namespace std;
const int MAXN=200005;
int fa[MAXN],n,m,ans,sum[MAXN];
void makeset() {
for(int i=1;i<=n;i++) {
fa[i]=i;
sum[i]=0;
}
}
int find(int x) {
if(fa[x]!=x) {
int f=fa[x];
fa[x]=find(fa[x]);
sum[x]+=sum[f];
}
return fa[x];
}
void unionset(int x,int y,int s) {
int u=find(x),v=find(y);
if(u==v) {
if(sum[y]-sum[x]!=s) ans++;
return;
}
fa[v]=u,sum[v]=sum[x]+s-sum[y];
}
int main() {
scanf("%d%d",&n,&m);
makeset();
for(int i=1;i<=m;i++) {
int x,y,s;
scanf("%d%d%d",&x,&y,&s);
unionset(x-1,y,s);
}
printf("%d",ans);
}
总结:这三道题展现了并查集问题的相似性,它不但可以维护关系,还可以维护数值,实现集合内任意两点间路径的查询。