[总结] 并查集相关
[总结] 并查集相关
数据结构进阶
优化技巧
路径压缩
实现本质是把当前结点接到爷爷上面:
- 递归版:从根往下跑。
- 迭代版:从叶子往根跑。
按秩合并
核心是启发式合并,将小的挂到大的上面,从而达到保持形态和复杂度的目的。
- 按照深度按秩合并
有点像长链剖分,可以被卡成 \(\text O(\sqrt{n})\) 的,不建议使用。
但是如果是可撤销的,写起来比较方便(深度变化至多为 \(1\))。
- 按照大小按秩合并
类似于重链剖分,是均摊 \(\text O(logn)\) 。
边带权并查集
边带权并查集是通过维护儿子结点与父亲结点的可传递性关系转化为儿子结点与根节点的关系来体现题目要求的。
例题
一共有 \(n\) 列,开始时每一列 \(i\) 有一个编号为 \(i\) 的战舰,以后会有 \(T\) 个操作:
- 将第 \(i\) 列的所有战舰按照原来顺序加到第 \(j\) 列的所有战舰后面。
- 询问两个战舰之间的战舰个数是多少。
采用边带权并查集的思想,设 \(d[x]\) 表示 \(x\) 到根节点的边的数量,则答案为 \(\abs {d[x]-d[y]}-1\) 。
容易发现,这是有传递性的。
- 路径压缩时的处理
int find(int x){
if(fa[x]==x)return x;
int rt=find(fa[x]);
d[x]+=d[fa[x]];
return fa[x]=rt;
}
一次路径压缩时会丢掉父亲到根的那一段信息,或者说本来这个信息是需要暴力跳 \(father\) 求和的,但是由于路径压缩当前结点 \(x\) 接到了根节点上,所以直接求和。
合并时因为是放到第 \(j\) 列后面,需要维护第 \(j\) 列并查集的 \(sz\) 大小。
inline void merge(int x,int y){
x=find(x);y=find(y);
if(x!=y){
fa[x]=y;d[x]+=sz[y];
sz[y]+=sz[x];
}
}
Alice 和 Bob 在玩一个游戏:他写一个由 \(0\) 和 \(1\) 组成的序列。Alice 选其中的一段(比如第 \(3\) 位到第 \(5\) 位),问他这段里面有奇数个 \(1\) 还是偶数个 \(1\)。Bob 回答你的问题,然后 Alice 继续问。Alice 要检查 Bob 的答案,指出在 Bob 的第几个回答一定有问题。有问题的意思就是存在一个 \(01\) 序列满足这个回答前的所有回答,而且不存在序列满足这个回答前的所有回答及这个回答。
像这种让你求解矛盾数量的问题,称之为悖论问题。
首先第一个转化,如果一段区间 \([l,r]\) \(1\) 的个数为奇数个,说明 \(sum[l-1]\) 与 \(sum[r]\) 奇偶性不同。
反之,\(sum[l-1]\) 与 \(sum[r]\) 奇偶性相同。
于是我们研究的问题就变成了 \(sum\) 奇偶性是否相同。
这是带边权并查集的最经典应用,设 \(d[x]\) 表示 \(x\) 与父亲结点奇偶性是否相同,那么就和上一道题一样了。
运算改为异或即可。
int get(int x) {
if (x == fa[x]) return x;
int root = get(fa[x]);
d[x] ^= d[fa[x]];
return fa[x] = root;
}
总结
边带权并查集的做题步骤基本如下:
- 找到可传递性关系,即父亲与儿子的关系唯一表示。
- 考虑优化:路径压缩 \(or\) 按秩合并(保证不会磨灭原数据)。
- 类似地列出状态之间的真值表,找到合法运算。
分类并查集
同样是悖论问题,但是由于状态数由 \(2\) 变为 \(3\),考虑分类并查集。
换句话说,分类并查集和带边权并查集本质上是一样的,只是传递信息的方式不同,思路大体一致。
把它们分为三个域:同类域,捕食域,天敌域。
比如说,\(X\) 吃 \(Y\) 可以转化为 \(X_{eat}\) 和 \(Y_{self}\) 之间的关系。
总结
并查集就是关系,表示各个点之间能否发生转化。