简单启发式合并
概述:有 N 个集合, 每个集合大小为 1, 只有合并两个集合的操作, 若每次合并两个集合把小集合的元素拿出来插入到大集合里, 那么对于小集合里的元素, 所在集合的大小翻倍了。
所以所有元素被插入的次数是 \(O(\log N)\) 的。
P3302
对于森林的每棵树, 用倍增数组维护 LCA, 维护到根的可持久化线段树。
于是查询的时候直接查, 连边的时候重构较小的那棵树。
代码不难。
P5290
好好读题分析的时候就不会痛苦了。
很多连续动作用 for 语句表示会很好看。
比如:
for (cnt = 0; q[y].size(); q[x].pop(), q[y].pop()) {
o[cnt++] = std :: max(q[x].top(), q[y].top());
}
【记录】
P3224
用维护整数集合的东西启发式合并即可。
复习一下 treap。
一个 update siz 的小函数少加了一个 1 就调了这么长时间啊……
【记录】
struct node {
int siz, val, rnd, c[2];
node() {}
node(int s, int v) : siz(s), val(v) {
rnd = rand();
c[0] = c[1] = 0;
}
} t[N];
int tot, root;
inline void upd (int me) {
t[me].siz = t[t[me].c[0]].siz + t[t[me].c[1]].siz + 1;
}
void ro (int & me, int dir) {
int y = t[me].dir;
t[me].c[dir] = t[y].c[dir ^ 1], t[y].c[dir ^ 1] = me;
upd(me), upd(y);
me = y;
}
void ins (int x, int & me) {
if (!me) { t[me] = node(1, x); return; }
++ t[me].siz;
int dir = x > t[me].val;
ins (x, t[me].c[dir]);
if (t[me].c[dir][t].rnd > t[me].rnd) ro (me, dir);
}
void del (int x, int & me) {
if (!me) return ;
if (t[me].val != x) {
--t[me].siz;
del (x, t[me].c[x > t[me].val]);
} else {
if (!t[me].c[0] || !t[me].c[1]) me = t[me].c[0] | t[me].c[1];
else {
int dir = t[t[me].c[1]].rnd > t[t[me].c[0]].rnd;
ro (me, dir);
--t[me].siz;
del (x, t[me].c[dir ^ 1]);
}
}
}
P3402【可持久化并查集】
【记录】来看我清晰直观的代码!
CF600E【树上启发式合并】
直接用这题介绍了, 算法过程:
- 计算所有轻儿子贡献,并撤回所有插入
- 计算重儿子贡献,不对数据结构做任何操作
- 将轻儿子全部插入数据结构
- 回答当前子树询问,回溯
一个点作为轻儿子子树的点的次数是 \(O(\log N)\) 级别的。
【记录】
可以用 【DSU ON TREE】这种 coding style, 即依据 dfs 序的子树内连续的性质通过遍历 dfs 序来遍历子树中的结点。 会让生活变得轻松
CF741D
好长的题面耶。
刚开始读成了排序而不是重新排列?????????草
由于启发式合并也有一个一个插入轻儿子的过程, 所以统计这个东西就很轻松了。
【记录】
不小心加了点蛇足,不过影响不大。