珂朵莉树
珂朵莉树的主要操作是区间覆盖,即将区间\([l,r]\)全部染色为\(c\)。
EXAMPLE
EXAMPLE 1
给出一个长度为\(n\)的序列,一共\(q\)次询问,每次询问给出\(m\)个区间,求这些区间并集的权值和。
\(n \leq 10^5,\sum m \leq 10^5\)
SOLUTION 1
显然能用珂朵莉树做
珂朵莉树是一种基于std::set
的暴力数据结构。
这个set
维护若干区间,这些区间没有交集,且按左端点从小到大有序。
struct node {
int l, r, c;
int operator<(const node& a) const { return l < a.l; }
};
set<node> S;
typedef set<node>::iterator IT;
珂朵莉树的基本操作有两个:
split(pos)
这个操作把包含了\(pos\)位置的区间分成两段,分别为\([l,pos-1]\)和\([pos,r]\),且返回\([pos,r]\)的指针。
写法也很简单:
IT split(int po) {
IT it = S.lower_bound((node){po, -1, 0});
if (it != S.end() && it->l == po) return it;
--it;
int l = it->l, r = it->r, c = it->c;
S.erase(it);
S.insert((node){l, po - 1, c});
return S.insert((node){po, r, c}).first;
}
assign(l, r, c)
这个操作将被\([l,r]\)包含的区间全部删除,然后用颜色\(c\)覆盖区间\([l,r]\)。
代码:
void cover(int l, int r, int c) {
IT itr = split(r + 1), itl = split(l);
S.erase(itl, itr); //意思是删除[itl, itr),注意左闭右开
S.insert((node){l, r, c});
reset(c, sum[r] - sum[l - 1]);
}
那么,对于上面那题,怎么用珂朵莉树解决?
对于每个询问,我们直接cover(l, r)
,由于操作的是整个区间,答案的增减可以直接\(O(1)\)得到,代码只需要在上面的写法上变一下即可:
void cover(int l, int r, int c) {
IT itr = split(r + 1), itl = split(l), it = itl;
for (; it != itr; ++it) {
//这里可以直接根据it的左右端点增减答案
}
S.erase(itl, itr);
S.insert((node){l, r, c});
reset(c, sum[r] - sum[l - 1]);
}
EXAMPLE 2
给出一个\(n\)个节点的树以及\(m\)条树上路径,共\(q\)个询问,每次询问\([l_i,r_i]\)这些路径的并的权值。
SOLUTION 2
先离线询问,把询问\([l,r]\)挂在\(r\)。从左往右扫时,我们把第\(i\)条路径上的每个点染色为\(i\),然后遍历挂在\(i\)上的询问,设其左端点是\(l\),那么颜色\(>=l\)的点都有贡献。
树链剖分后路径染色转化为区间问题,为了统计\(c>=l\)的贡献和,我们再使用树状数组维护,用珂朵莉树解决染色问题,这题就搞定了。
复杂度
如果被覆盖的区间毫无贡献,那么可以证明珂朵莉树复杂度是\(O(nlogn)\)的。
因为每次assign
最多增加\(3\)个区间,每次增加是\(O(logn)\)的,删除区间是均摊\(O(logn)\)的,所以复杂度是\(O(nlogn)\)的。
又扩展技能树啦~