珂朵莉树学习笔记
0x00 什么是珂朵莉树
所谓珂朵莉树,就是颜色段均摊。它并不是树形数据结构,而是线性数据结构。
珂朵莉树的本质是合并一些有着相同信息的区间,以达到节省时间的效果。这里的信息一般是颜色,但也有例外。
珂朵莉树的修改与询问都是暴力,这使得珂朵莉树几乎无法处理区间赋值之外的区间修改或查询,但是也让珂朵莉树有非常广泛的用途。
0x01 常见的珂朵莉树写法
珂朵莉树通常将区间转化为值,并使用 std::set 来维护每个区间,同一个区间里的值是相同的。代码如下:
struct node {
int l, r; mutable int v;
node(int L, int R = -1, int V = 0): l(L), r(R), v(V) {}
bool operator < (const node& o) const { return l < o.l; }
};
typedef set<node>::iterator IT;
set<node> s;
代码中的 node 是维护了一个 \([l,r]\) 的区间,这个区间里的数值全为 \(v\)。
珂朵莉树有两个基本操作:
- split 裂块
IT split(int pos) {
IT it = s.lower_bound(node(pos));
if (it != s.end() && it->l == pos) return it;
int L = (-- it)->l, R = it->r, V = it->v;
s.erase(it), s.insert(node(L, pos - 1, V));
return s.insert(node(pos, R, V)).first;
}
-
split的作用是将 \(pos\) 所在的块 \([l,r]\) 分裂为 \([l,pos-1],[pos,r]\) 两块,时间复杂度 \(\mathcal{O}(\log s)\),\(s\) 是当前std::set内区间的个数。 -
assign 区间赋值
void assign(int l, int r, int val = 0) {
IT itr = split(r + 1), itl = split(l);
s.erase(itl, itr);
s.insert(node(l, r, val));
}
- 随着
split,珂朵莉树中的区间个数会越来越多,导致复杂度爆炸,我们需要区间赋值操作来减少区间个数。区间赋值将 \([l,r]\) 中的区间删除,重新合并为一段,时间复杂度 \(\mathcal{O}(s'\log s)\),\(s'\) 是删除的区间个数。
来模拟一下?
设原先区间是 \([l_1,r_1],[l_2,r_2],\cdots,[l_s,r_s]\),且 \(l_1\leq l\leq r_1,l_s\leq r\leq r_s\),代码中 assign 操作先是通过 split 将区间变成 \([l_1,l-1],[l,r_1],[l_2,r_2],\cdots,[l_s,r],[r+1,r_s]\);然后通过 erase,删除了 \([l,r]\) 之间的所有区间,将区间变成 \([l_1,l-1],[r+1,r_s]\);最后插入区间 \([l,r]\),将区间变成了 \([l_1,l-1],[l,r],[r+1,r_s]\),完成了区间赋值。
初始化只需插入 \([1,1],[2,2],\cdots,[n,n]\) 即可。
0x02 珂朵莉树的复杂度与应用
珂朵莉树复杂度是基于均摊的:
- 初始插入 \(n\) 个区间,\(q\) 次
assign每次最多增加两个区间(两端的split),总共最多出现 \(n+2q\) 个区间。 - 每个区间只会被删除一次,故所有
split与assign操作的总复杂度为 \((n+2q)\log n\),均摊每次 \(\mathcal{O}(\log n)\)。 - 同时,如果珂朵莉树的区间询问与区间赋值绑定,则区间询问的时间复杂度也是 \(\mathcal{O}(s'\log s)\),均摊 \(\mathcal{O}(\log n)\)。
基于均摊,珂朵莉树可以以非常优秀的复杂度完成暴力才能维护的操作,这使得它有非常多的应用。例题:
LOJ #6284. 数列分块入门 8
区间询问等于一个数 \(c\) 的元素,并将这个区间的所有元素改为 \(c\)。
区间询问与区间修改绑定,可以使用珂朵莉树暴力处理询问。时间复杂度 \(\mathcal{O}((n+q)\log n)\)。
洛谷 T314507 猫猫的烟花易冷 2
区间赋值,区间查第 \(k\) 小数。
这题询问可与修改不绑定,好像与珂朵莉树没有任何关系!
先思考只有单点修改怎么做,这显然是 P2617 Dynamic Rankings。考虑这题的分块做法,维护每种颜色的块前缀和。观察一下,发现不仅支持单点修改,还支持区间同一颜色的修改。那么用珂朵莉树维护相同颜色的区间,assign 时对每个删除的区间修改前缀和即可。散块的话需要再用一个支持区间修改单点查询的分块维护。时间复杂度 \(\mathcal{O}(n+q\sqrt{n})\)。
类似地,用 P3332 [ZJOI2013] K大数查询 的树套树代替分块可以做到 \(\mathcal{O}((n+q)\log^2n)\),不过笔者写了一下之后发现常数太大,反而跑不过分块。
在随机数据下,珂朵莉树内的区间个数是 \(\mathcal{O}(\log n)\) 级别的。利用这一特性,珂朵莉树在不与区间赋值绑定的情况下,也可以进行区间操作。详细的证明可以参考这篇。例题:
CF896C Willem, Chtholly and Seniorious
区间加、区间赋值、区间查询 \(k\) 小值、区间查询 \(k\) 次方和。数据随机。
直接跑暴力就做完了。
0x03 珂朵莉树的拓展
维护的信息大多数情况是颜色,但也可以不是颜色。这类题和前面的珂朵莉树比较像的,也有搭配根号重构完全不像的。还是看例题吧:
[QwQOI2020] III
区间从小到大或从大到小排序,单点查询
用珂朵莉树维护每个排好序的区间(从小到大、从大到小),那么每次排序即推平。
每个区间维护一棵线段树,用线段树分裂、合并维护就行了。
时间复杂度 \(\mathcal{O}(q\log n)\)。
洛谷 P3391 【模板】文艺平衡树
\(q\) 次区间翻转,最后输出整个序列。
区间翻转,维护区间的话可以做到一次 \(\mathcal{O}(s)\),\(s\) 是区间个数,每次最多会增加 \(2\) 个区间。
设立一个阈值 \(B\),当 \(s\geq B\) 时重构序列,这样 \(s\) 就重新变为 \(1\)。
翻转一次 \(\mathcal{O}(B)\),重构一次 \(\mathcal{O}(n)\),总复杂度为 \(\mathcal{O}(qB+\frac{nq}{B})\),取 \(B=\sqrt{n}\),时间复杂度为 \(\mathcal{O}(q\sqrt{n})\)。
例题 2 重构的思想还可以继续拓展,可以看很厉害的这篇:ODT的映射思想的推广。不过这篇后面讲的带插区间众数用珂朵莉树好像没有什么必要。

浙公网安备 33010602011771号