带权并查集
Posted buleeyes
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带权并查集相关的知识,希望对你有一定的参考价值。
做了 cf 上一道题后发现我对并查集的理解不够深刻,顺带把带权并查集学一下。
并查集
初始化:对于一个集合 A 的所有元素,我们知道对于其中任意一个元素 i,i€A。此时,我们可以认为 i与 A 之间存在一条虚边,如果有新的元素要加入集合 A,将该元素与 A 建一条边即可。这条边我们用数组 fa[i] 表示,即点 i 和它的父亲。
加入:对于点 i,要加入点 j 所在的集合,怎么操作呢?fa[i]=fa[j] 即可。可如果我们要将两个集合合并呢?将集合 A 与集合 B 建边即可。这样做会导致什么结果呢?查询一个节点所属集合最坏可以达到O(n)的复杂度,所以我们增加 join 和 find 操作,join 就是将在两个集合之间建一条边,find 就是将集合间的边消除,让最终集合直接与点相连。
这就是并查集了。
带权并查集
假设存在多个集合,这些集合彼此之间又存在一些关系,
「带权并查集」奇偶游戏
奇偶游戏
原题链接:奇偶游戏
题目大意
给你N个区间,每个区间给你它含1的奇偶性,问你哪些询问逻辑上冲突
题目题解
一道带权并查集的题,让我对带权并查集有了更深入的理解,带权并查集可以分为两种(在这道题中)
- “边带权”并查集
- “扩展域”并查集
两种方法都是思维上的不同所造成的,其中第一种解法是最常见的,第二种解法在代码实现上是最简单的,我们先来对第一种进行探究
边带权,很明显,我们要在并查集的边上进行一个储存边权的操作,我们这里用d来表示当前节点到根节点的Xor值,什么意思,又如何维护呢?意思就是我们对于每条边储存的是连接的两点是否属于同一奇偶性,边权为0则相同,为1则不同,那么我们从x节点到根节点的唯一路径上所有的边权全部Xor起来,就能判断x节点和根节点是否为同一奇偶性了,这样的话我们就能够判断对于任意两点而言,x节点到根节点的Xor值和y节点到根节点的Xor值Xor起来得到的值,就是我们所需要的对于任意两点的Xor值,再和我们当前询问的奇偶性进行Xor,如果为1则说明冲突,如果为0则说明不冲突
那既然我们的查询解决完了,我们就要解决合并的问题了,接着上一段的最后继续说,我们当前有x, y两个点,和他们的根p, q,令连接后p为q的子节点,那么我们现在要求的就是d[p]了,d[p]怎么求?我们现在已知的信息是d[x] 和 d[y] 也就是 x~p 和 y~q,我们现在要求的是p~q,我们已知x~y是由 x~p,y~q,p~q 来组成的,那么 x y 的奇偶性 (我们这里用\\(cnt_x, y\\)来表示任意两点的奇偶性,这个奇偶性怎么来了?输入就给了) \\(cnt_x,y = d[x]Xord[y]Xord[p]\\) 那么我们就可以得到\\(d[p] = d[x] Xord[y]Xorans_x, y\\) 了,于是合并操作也就完成了。
进行完这两个之后,我们就对题本身进行分析一下,因为本题中给的是一个区间,而并非一个点,所以我们还得将区间转化为点,区间转化为点?感觉很熟悉?对,的确很熟悉 我们在 防线 这道题中用过类似的方法,就是用前缀和保存奇偶性,什么意思?详细可以看上面那道题,我这里就不再赘述,转化到本题上来,我们既然要转化到点,这个数据范围肯定是不行的,我们考虑离散化。
那么离散化之后,我们就得到了一个序列,这个序列的点是紧紧挨着的,我们这里就能够通过上面的区间转点来搞定了,比如我们现在要合并一个区间 \\(l, r\\),那实际上 \\(l,r\\) 的奇偶性已经是已知的了,(很多人卡在这里,不知道这里要怎么进行操作,也有很多人没想到用并查集来维护..,卡在套路上了),那么\\(l - 1\\) 到前面的某一个值的奇偶性一定也是已知的,那么我们就将 \\(r\\) 和 \\(l - 1\\) 用并查集维护(想想为什么?答案很显然),这样我们就维护了两个区间的奇偶性并且将两个区间合并。
根据上面的信息,我们就完成了这道题,代码如下
//#define fre yes
#include <cstdio>
#include <algorithm>
const int N = 20005;
int ele[N << 1];
struct message
int l, r, ans;
arr[N];
bool flag;
namespace Union
int par[N], d[N];
inline void init(int n)
for (int i = 1; i <= n; i++)
par[i] = i;
int find(int x)
if(par[x] == x) return par[x];
int rt = find(par[x]);
d[x] ^= d[par[x]];
return par[x] = rt;
inline void unite(int x, int y, int a, int b, int i)
par[a] = b;
d[a] = d[x] ^ d[y] ^ arr[i].ans;
inline void solve(int x, int y, int i)
if((d[x] ^ d[y]) != arr[i].ans)
printf("%d\\n", i - 1);
flag = 1;
int m;
inline void discrete(int n)
std::sort(ele + 1, ele + 1 + n * 2);
m = std::unique(ele + 1, ele + 1 + n * 2) - ele - 1;
int ask(int x)
return std::lower_bound(ele + 1, ele + 1 + m, x) - ele;
int main()
static int n, T;
scanf("%d %d", &T, &n);
for (int i = 1; i <= n; i++)
int x, y; char c[6];
scanf("%d %d %s", &x, &y, c + 1);
arr[i].l = x; arr[i].r = y;
arr[i].ans = (c[1] == 'e' ? 0 : 1);
ele[i] = x - 1; ele[i + n] = y;
discrete(2 * n);
Union::init(m);
for (int i = 1; i <= n; i++)
int x = ask(arr[i].l - 1);
int y = ask(arr[i].r);
int p = Union::find(x), q = Union::find(y);
if(p == q)
Union::solve(x, y, i);
if(flag) return 0;
else Union::unite(x, y, p, q, i);
printf("%d\\n", n);
return 0;
然后我们再来讨论第二种方法,扩展域并查集,第二种方法思维上更简单,代码上更容易实现,因为不会涉及到位运算等知识,怎么来运作的呢?
将每个节点\\(x\\)拆成两个节点,\\(x_odd\\) 与 \\(x_even\\) 分别表示sum[x]为奇数和sum[x]为偶数,我们经常也把这两个节点成为x的“奇数域”和“偶数域”。
那么就会有以下情况,设离散化后的\\(l - 1\\) 与 \\(r\\) 的值分别是 \\(x\\) 和 \\(y\\) ,设 \\(cnt\\) 为两点的奇偶性(这里只是读入的)
- 若 cnt = 0 则合并 \\(x_odd\\) 与 \\(y_odd\\) ,\\(x_even\\) 与 \\(y_even\\) 。这表示"x为奇数"与"y为奇数"可以互相推出,"x为偶数" 与 "y为偶数"也可以互相推出,它们是等价的信息 .
- 若 cnt = 0 则合并 \\(x_odd\\) 与 \\(y_even\\),\\(x_even\\) 与 \\(y_odd\\) 。 这表示"x为奇数"与"y为偶数"可以互相推出,"x为偶数" 与 "y为奇数" 也可以互相推出,它们是等价的信息。(为何能够相互推出?可以自己想想)
上述合并也维护了关系的传递性,在处理完(x, y, 0)与(y, z, 1)两个关系之后,很显然 x, z的关系也就明显了,这种做法就相当于在无向图上维护节点之间的连通情况,只是拓展了多个域来应对多种传递关系
若 x, y 对应 \\(x_odd\\) 与 \\(y_odd\\) 在同一集合内,就说明二者奇偶性相同,若 x, y 对应 \\(x_odd\\) 与 \\(y_even\\) 在同一集合内,则已知二者奇偶性不同
那么以上信息,我们就可得代码,代码如下
//#define fre yes
#include <cstdio>
#include <algorithm>
const int N = 20005;
int ele[N << 1];
struct message
int l, r, ans;
arr[N];
bool flag;
namespace Union
int par[N], d[N];
inline void init(int n)
for (int i = 1; i <= n; i++)
par[i] = i;
int find(int x)
if(par[x] == x) return par[x];
return par[x] = find(par[x]);
inline void unite(int x, int y)
int a = find(x);
int b = find(y);
if(a == b) return ;
par[a] = b;
int m;
inline void discrete(int n)
std::sort(ele + 1, ele + 1 + n * 2);
m = std::unique(ele + 1, ele + 1 + n * 2) - ele - 1;
int ask(int x)
return std::lower_bound(ele + 1, ele + 1 + m, x) - ele;
int main()
static int n, T;
scanf("%d %d", &T, &n);
for (int i = 1; i <= n; i++)
int x, y; char c[6];
scanf("%d %d %s", &x, &y, c + 1);
arr[i].l = x; arr[i].r = y;
arr[i].ans = (c[1] == 'e' ? 0 : 1);
ele[i] = x - 1; ele[i + n] = y;
discrete(2 * n);
Union::init(m * 2);
for (int i = 1; i <= n; i++)
int x = ask(arr[i].l - 1);
int y = ask(arr[i].r);
int x_odd = x, x_even = x + m;
int y_odd = y, y_even = y + m;
if(arr[i].ans == 0)
if(Union::find(x_odd) == Union::find(y_even))
printf("%d\\n", i - 1);
return 0;
Union::unite(x_odd, y_odd);
Union::unite(x_even, y_even);
else
if(Union::find(x_odd) == Union::find(y_odd))
printf("%d\\n", i - 1);
return 0;
Union::unite(x_odd, y_even);
Union::unite(x_even, y_odd);
printf("%d\\n", n);
return 0;
以上是关于带权并查集的主要内容,如果未能解决你的问题,请参考以下文章