29.并查集
并查集
普通并查集与带权并查集,快速实现元素集合的合并与查询。
普通并查集
每个集合都有一些点,两个集合之间的任意点连接,则认为两个集合合并
1 | 集合A: 集合B: |
此时想知道 \(4\) 和 \(8\) 是否在一个集合中,就不得不 DFS 一下,很费时间。
已在同个集合的两点不再连边,则每个集合都可以看作一个树形结构。
并查集通过路径压缩树变得"扁平"
1 | 1 |
1 | void Init() { |
int fa(int x) {return p[x] = x == p[x] ? x : fa(p[x]);}
这行代码是并查集的精髓,拆解分析:
p[x] =
是在为x
设置父节点,p[x]
设为谁,谁就是x
的直接父节点x == p[x] ?
三元运算符,如果x
自己就是p[x]
,说明x
是所在集合树的根节点fa(p[x])
如果x != p[x]
,则递归调用fa(p[x])
,即从x
父节点继续向上递归找根节点- 这个递归会返回根节点,从而前面的
p[x] =
赋值就是根节点的编号 fa(p[x])
会继续递归处理x
的父节点p[x]
,那么p[p[x]]
也会赋值为根节点,p[p[p[[x]]
也会……- 从而完成路径压缩——
x
到根的一路的节点都把父节点设为了根节点
- 这个递归会返回根节点,从而前面的
带权并查集
普通并查集的图,边只表示“是同一个集合”。而有一类表达关系的图,“边”带有关系属性。
例:食物链
有 \(N\) 个元素,编号为 \(1∼N\)。每个元素属于 \(A\)、\(B\)、\(C\) 三种类型之一(比如石头剪刀布),但具体类型未知。
给定 \(K\) 条信息,每条信息有两种形式:
- \(1\) \(X\) \(Y\):表示 \(X\) 和 \(Y\) 是同一类型
- \(2\) \(X\) \(Y\):表示 \(X\) 吃 \(Y\)(\(A\) 吃 \(B\),\(B\) 吃 \(C\),\(C\) 吃 \(A\),形成环形)
判断这 \(K\) 条信息中有多少条是假信息。一条信息在以下情况下为假:
- 与之前的信息冲突(\(A\)吃\(B\),\(B\)吃\(C\),但\(A\)又吃\(C\),不符合循环,就是一种冲突)
- \(X\) 或 \(Y\) 大于 \(N\)(出现了不存在的编号)
- \(X\) 吃 \(X\)(自相矛盾)
输入:第一行 \(N\) \(K\)(\(N\) 个元素,\(K\) 条信息),接下来 \(K\) 行每行三个整数 \(D\) \(X\) \(Y\)(\(D\) 为 \(1\) 或 \(2\))。
1 | 100 7 |
输出格式:一行,假信息的数量。
1 | 3 |
对于石头剪子布,\(X\)与\(Y\) 有 3 种边:\(X\) 吃 \(Y\)、\(Y\) 吃 \(X\)、\(X\)和\(Y\)同类,用集合表示“知道它们两两之间的关系”这个概念,还用这个图来理解:
1 | 集合A: 集合B: |
\(1,2,3,4\) 知道两两相克关系,\(5,6,7,8,9\) 知道两两相克关系,但是两个集合之间相克关系为止。
当某一刻告知了 \(2\) 吃 \(6\),或 \(6\) 吃 \(2\),或 \(2\) 与 \(6\) 同类,那么\(1\sim 9\) 的两两相克关系就全都能知道了。
这很像并查集的模型,但又不能简单地用父子节点的树来表达。需要让边带权,在路径压缩的时候也要压缩这个权值。
权值定义为:子节点与父节点的“顺时针”距离。
在石头剪子布里,距离就定义为:
- 从石头到剪子、从剪子到布、从布到石头的距离都是 \(1\)
- 从石头到布、从剪子到石头、从布道剪子的距离都是 \(2\)。
有这个顺时针距离,就可以确定相克关系。
路径压缩时,仍然是把树“扁平化”,因为维护了任意子节点到父节点的“食物链顺时针”距离,那么就可以压缩出节点到根节点的“食物链顺时针”距离。
用 \(A-B\) 表示计算从 \(B\) 出发到 \(A\) 的距离,来演练一下这个合并:
设 \(A\)、\(AR\) 是一个集合,\(AR\) 是根节点, \(B\)、\(BR\) 是一个集合,\(BR\) 是根节点。
1 | AR BR |
当告知 \(A\) 与 \(B\) 的相克关系时,集合合并。先路径压缩,让\(A\)称为\(AR\)的直接子节点,\(B\) 也称为 \(BR\) 的直接子节点
1 | AR BR |
1 | int fa(int x) { |
代码中 p[x]
维护 x
的父节点,
d[x]
维护 x
到父节点的“食物链顺时针”距离。
当得知 \(A\) 与 \(B\) 相克关系时,进行类似合并集合操作,将 \(AR\) 作为 \(BR\) 的子节点:
1 | BR |
$B-A = (B - BR) + (BR - AR) + (AR-A) $
这里 \(AR-A\) 就是代码中的
d[A]
,BR-AR
是
d[AR]
,BR-B
(即-(B-BR)
) 是
d[B]
因为集合合并是 将 \(AR\) 作为 \(BR\) 的子节点,所以在集合合并前后, \(A\)与\(B\)
的直接父节点不变(这一刻\(A\)暂且没有进一步路径压缩),从而d[A]
、d[B]
不变。
d[AR]
本来作为根节点是0
,合并时只有它会变,要根据 \(A\)与\(B\)
的关系,在成为\(BR\)
的子节点后发生变化,变换之前的等式得到:
\((BR-AR) = (BR-B) - (AR-A) + (B-A)\)
对应代码中的
d[AR] = d[B] - d[A] + <A到B的食物链顺时针距离>
- 当\(A\)与\(B\)是同类,则\(B-A=0\),即
d[AR]= d[B] - d[A] + 0
- 当 \(A\)吃\(B\),则\(B-A=1\),
d[AR]= d[B] - d[A] + 1
从而集合基于 \(A\) 与 \(B\) 的关系合并,在两个集合各自路径压缩后,只需要 \(2\) 步:
- 把 \(AR\) 接到 \(BR\) 作为子节点,就是更新
p[AR]
- 确定 \(AR\) 到 \(BR\) 的距离,就是更新
d[AR]
参考代码
1 |
|