带权并查集 NOI 2001 食物链
这题之前就做过,结果这次在 Coursera 上遇到又没做出来,,,
对并查集利用的思路真的很妙,所以还是记录一下
其实这题有两种方法,种类并查集 (开三倍大小的并查集) 与带权并查集
带权并查集的空间显然更优越,而且能扩展到物种食物链环长度为 \(n\) 的情况,所以这里主要介绍一下这种方法
首先这题并查集的特征很明显,问题就在于 \(a\) 吃 \(b\) 的关系不好描述
观察一条食物链 $a < b < c < d < e < f ... $ \(a\) 与 \(d\), \(b\) 与 \(e\), \(c\) 与 \(f\)... 在食物链上每两个距离为 \(3\) 的动物必然是同一物种
根据这个特征,我们为并查集上的每一个点设置一个权值 \(w\) 代表该点到父亲结点 \(f_x\) 的距离取模 \(3\)
这样,在查询两个结点的关系时,若它们在同一连通块,我们只需要比较它们到并查集根节点之间的距离就行了(若两结点在同一连通块,路径压缩后两结点必有相同的父亲:连通块根节点)
例如 \(w\) 差值为 \(0\) 时(两结点到根距离相等)它们一定属同一物种
若两结点不在同一连通块,我们根据给出的信息对它们所在的连通块进行合并
接下来是在连通块合并时与寻根时 \(w\) 的更新操作
将 \(x\) 所在的连通块与 \(y\) 所在的连通块合并时,找到它们的根 \(f_x\) 与 \(f_y\),并将 \(f_x\) 的父亲设为 \(f_y\)
在纸上画个图找向量关系:
- \(x\) 到 \(f_x\) 的距离为 \(w_x\)
- \(y\) 到 \(f_y\) 的距离为 \(w_y\)
- \(x\) 到 \(y\) 的距离为 \(0\) (若两者为同一物种) 或 \(1\) (若 \(x\) 吃 \(y\))
那么 \(f_x\) 到 \(f_y\) 的距离 \(w_{f_x}\) 即为 \(w_y - w_x + 0/1\)
另外值得一提的是路径压缩时,结点父亲改变带来的对 \(w\) 的更新
原来的 \(w_x\) 是相对于自己父亲 \(w_{fx}\) 的距离,路径压缩之后则是对于新父亲 \(w_{root_x}\) 的距离
具体的更新方式是先储存原父亲,待递归后原父亲的 \(w_{f_x}\) 被更新,此时我们只需要将自己到原父亲的距离 \(w_x\) 加上更新后原父亲到新父亲的距离 \(w_{f_x}\) 即可得到自己到新父亲的距离,也即更新后的 \(w_x\)
接下来是代码:
#include <iostream>
using namespace std;
const int MAX_N = 1e5 + 10;
int fa[MAX_N], w[MAX_N];
inline int get(int x) {
if (x == fa[x]) return x;
int tmp = fa[x];
fa[x] = get(fa[x]);
w[x] = (w[x] + w[tmp] + 3) % 3;
return fa[x];
}
int main() {
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; ++i) fa[i] = i;
int ans = 0;
while (k--) {
int op, x, y;
cin >> op >> x >> y;
if (x > n || y > n || (op == 2 && x == y)) {
++ans;
continue;
}
op -= 1;
if (get(x) == get(y)) {
if ((w[x] - w[y] + 3) % 3 != op) ++ans;
} else {
int fa_x = get(x), fa_y = get(y);
fa[fa_x] = fa_y;
w[fa_x] = (w[y] - w[x] + op + 3) % 3;
}
}
cout << ans << endl;
return 0;
}