带权并查集详解:
首先讨论一下带权并查集的主要功能。并查集主要是维护不同元素之间传递关系的数据结构,如果不带权,那么维护的就是不同元素是否属于同一个集合,即连通关系。如果带权,那么除了维护连通性之外,还会维护同一个集合中各个元素之间的关系。
对于带权并查集的具体实现,最重要的维护元素是 ,表示元素 到根节点 的偏移量,即 ,而这个偏移量是由我们自己定义的,取决于具体题意。
接下来我们通过一道例题, 来仔细讲解这个数据结构。
题意:
一个长度为 个序列,一共 个询问。每次询问给出 ,表示这个序列中区间 的 和。 问一共有几组询问与前面为真的询问不符,如果该组询问不符合事实,忽略这组询问。
思路:
很明显,这题主要维护序列上每个节点之间的连通关系,以及节点之间的偏移量。我们定义 ,即区间 的和。这里有一个需要注意的地方,我们不能令 区间和,因为 初始化时为 ,而如果是 的区间和,那么初始化时 区间和,而不是,因此会发生错误。所以我们维护的是 。
然后我们再来考虑并查集的路径压缩操作。并查集的本质是一颗森林,带权并查集增加的操作就是增加了边权。
现在我们来考虑下面的这个图,执行 的过程,,就可以将 连向 ,但是我们需要更新 ,,而在路径压缩之前,。因此路径压缩时,,也可以理解为边权相加。
int find(int x){
if(fa[x]==x) return x;
int root = find(fa[x]);
d[x] += d[fa[x]], fa[x] = root;
return fa[x];
}
现在我们开始做这道题目。首先初始化,,然后对于每个 ,我们先判断 与 是否在同一个集合内。
如果不在同一个集合内,则说明 与 没有偏移关系,因此我们需要将这两个集合进行合并,存入他们的偏移关系,令。,而此处的 就是合并之后, 对于 的偏移量。
如果在同一个集合中,则判断 是否等于 ,如果不等于,则为假。
到此,这题就可以解决了。最后回顾一下带权并查集,带权并查集的核心就是维护多个元素之间的连通以及偏移关系,甚至可以维护多个偏移关系,而偏移的东西可以理解为当前节点到根节点的边长之和。
代码:
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define LOG1(x1,x2) cout << x1 << ": " << x2 << endl;
#define LOG2(x1,x2,y1,y2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << endl;
typedef long long ll;
typedef double db;
const db EPS = 1e-9;
using namespace std;
const int N = 2*1e5+1000;
int fa[N],d[N],n,m;
int find(int x){
if(fa[x]==x) return x;
int root = find(fa[x]);
d[x] += d[fa[x]], fa[x] = root;
return fa[x];
}
int main()
{
while(~scanf("%d%d",&n,&m))
{
rep(i,0,n) fa[i] = i, d[i] = 0;
int ct = 0;
rep(i,1,m){
int a,b,c; scanf("%d%d%d",&a,&b,&c); a--;
int f1 = find(a), f2 = find(b);
if(f1 == f2){
if(d[a]-d[b] != c) ct++;
}
else{
fa[f1] = f2, d[f1] = c+d[b]-d[a];
}
}
printf("%d\n",ct);
}
return 0;
}