基于STL库set容器实现的珂朵莉树
简介
珂朵莉树,起源于codeforce896C,是一种将值相同的区间合并在同一节点的从而进行操作的算法。
例: 序列
a
n
:
1
、
1
、
1
、
1
、
2
、
3
、
3
、
4
、
5
、
5
、
6
、
6
a_n:1、1、1、1、2、3、3、4、5、5、6、6
an:1、1、1、1、2、3、3、4、5、5、6、6可以被表示为下图
每数个相同的数据项都被转化为了一段区间存储在同一节点中。
用处
拿样例分,只要不遇见特意设置的数据,保证数据为随机生成的,基于set
实现的珂朵莉树上的相关操作都能做到
O
(
n
l
o
g
l
o
g
n
)
O(nloglogn)
O(nloglogn)级别的复杂度,详细证明可看珂朵莉树的复杂度分析 - 知乎 。
俗话说的好,遇见动规用贪心,区间问题珂朵莉,只要出题人没有刻意为难,都可以通过部分测试用例,妥妥的拿分神器,蓝桥杯必备。
*ps:*如果想正经解决区间问题,还是把线段树学好。
构建方法
创建节点
#include<iostream>
#include<set>
using namespace std;
struct node//构建节点
{
int _left, _right;//存储左右边界
mutable int _val;//使用mutable关键字标注_val值可更改
node(int L = 0, int R = -1, int V = 0) :_left(L), _right(R), _val(V) {}//构造函数,为节点默认赋值
bool operator < (const node& x) const //重载<运算符,使节点在set容器存储时使用左边界进行比较,维护区间
{
return _left < x._left;
}
};
set<node>s;//使用set容器来对珂朵莉树进行存储和维护
set容器简介
set
是使用红黑树来实现的一种关联式容器。平衡二叉树的特性使得 set
非常适合处理需要同时兼顾查找、插入与删除的情况。
和数学中的集合相似,set
中不会出现值相同的元素。
插入与删除操作
insert(x)
当容器中没有等价元素的时候,将元素 x 插入到set
中。erase(x)
删除值为 x 的 所有 元素,返回删除元素的个数。erase(pos)
删除迭代器为 pos 的元素,要求迭代器必须合法。erase(first,last)
删除迭代器在(first,last)
范围内的所有元素。clear()
清空set
。
迭代器
(可以粗略的认为是指针)
set
提供了以下几种迭代器:
begin()/cbegin()
返回指向首元素的迭代器,其中*begin = front
。end()/cend()
返回指向数组尾端占位符的迭代器,注意是没有元素的。rbegin()/crbegin()
返回指向逆向数组的首元素的逆向迭代器,可以理解为正向容器的末元素。rend()/crend()
返回指向逆向数组末元素后一位置的迭代器,对应容器首的前一个位置,没有元素。
以上列出的迭代器中,含有字符 c
的为只读迭代器,你不能通过只读迭代器去修改 set
中的元素的值。如果一个 set
本身就是只读的,那么它的一般迭代器和只读迭代器完全等价。只读迭代器自 C++11 开始支持。
查找操作
count(x)
返回set
内键为 x 的元素数量。find(x)
在set
内存在键为 x 的元素时会返回该元素的迭代器,否则返回end()
。lower_bound(x)
返回指向首个不小于给定键的元素的迭代器。如果不存在这样的元素,返回end()
。upper_bound(x)
返回指向首个大于给定键的元素的迭代器。如果不存在这样的元素,返回end()
。empty()
返回容器是否为空。size()
返回容器内元素个数。
分割操作
作为珂朵莉树中的核心操作,其他操作离不开分割操作。
例如:分割位置7
。位置7
在节点
的区间之中,将该位置分割分割,节点
就被分割为了
序列 a n : 1 、 1 、 1 、 1 、 2 、 3 、 3 、 4 、 5 、 5 、 6 、 6 a_n:1、1、1、1、2、3、3、4、5、5、6、6 an:1、1、1、1、2、3、3、4、5、5、6、6
分割前:
分割后:
auto split(int pos)
{
auto it = s.lower_bound(node(pos));//使用lower_bound寻找蕴含目标位置的节点的下一个节点,并创建一个迭代器it来存储这个节点
//注:由于我们在node中重载了<操作符,所以查找的目标是拥有最小大于等于目标值的左边界的节点,例如目标值是7定位到的就左边界为8的节点
//注:如果迭代器指向end(),说明当前容器中不存在目标位置
if (it != s.end() && it->_left == pos)//如果迭代器指向的节点的左边界与目标位值相同,例如pos=6时,说明这个位置已经被分割,直接返回当前迭代器就行
return it;//返回迭代器方便后续操作
it--;//如果目标位置未被分割,将迭代器前推,拿到包含目标位置的节点
int l = it->_left;//使用l存储左边界
int r = it->_right;//使用r存储右边节
int v = it->_val;//使用v存储节点的值
s.erase(it);//先将含有目标位置的节点删除
//再将两个拆分好的节点插入
s.insert(node(l, pos - 1, v));//使用预先存好的l作为左边界,pos-1作为右边界,v为节点值存入set中,分割原节点后的前半部分节点就存入了
return s.insert(node(pos, r, v)).first;//后半部分同理,使用pos作为左边界,r作为右边界,v为节点值存入set中,并使用.first返回分割原节点后的后半部节点的迭代器
}
推平操作
作为珂朵莉树中的核心操作,可以把将指定区间中的所有值更改。
void tp(int l, int r, int v)
{
auto itR = split(r + 1);//将指定右边界先进行分割,并使用迭代器itR存储分割后的后半节点,即非目标区间的第一个节点
//注:一定要先切割右边界,再切割左边界,否则如果遇见左边界在第一个节点且右边界也在第一个节点的情况,先切割左节点将导致迭代器指向错误
auto itL = split(l);//将指定的左边界进行切割,并使用迭代器itL存储分割后的后半部分,即目标区间的第一个节点
s.erase(itL, itR);//先将左右边界之间的所有节点删除
s.insert(node(l, r, v));//然后把指定值的节点插入,该节点的左边界就是l,右边界就是r,值为v。
}
指定区间的遍历
基本上其他对于区间的操作都是基于指定区间的遍历完成的
void ergodic(int l, int r)
{
auto itR = split(r + 1);//切割右边界
auto itL = split(l);//切割左边界
for (auto it = itL; it != itR; ++it)//创建迭代器it指向目标区间中的第一个节点,不断对迭代器迭代,直到指向非目标区间的第一个节点
{
fun();//将要实现的功能
}
}
基于指定区间的遍历完成的操作
指定区间求和
求目标区间所有值之和
int ask(int l, int r)
{
int ans = 0;//注:计算区间和的时候要注意数据范围,一般都会使用long long来存储答案
auto itR = split(r + 1);
auto itL = split(l);
for (auto it = itL; it != itR; ++it)
{
ans += (it->_right - it->_left + 1) * it->_val;//计算区间长度并乘以区间的值,添加到答案中
}
return ans;
}
指定区间加值
给指定区间的节点加上指定值
void add(int l, int r,int v)
{
auto itR = split(r + 1);
auto itL = split(l);
for (auto it = itL; it != itR; ++it)
{
it->_val += v;
}
}
指定区间的交换
交换两个区间
//开两个节点数组a和b用来存储节点,方便交换时使用,就像我们最开始写交换函数一样,使用一个中继器来存储需要交换的数据
node a[N];
node b[N];
void swapInterval(int l1, int r1, int l2, int r2)
{
if (l1 > l2)//判断l1是否大于l2,大于则交换一下,防止切割顺序出错,我在卡了三四个小时,血的教训
{
swap(l1, l2);
swap(r1, r2);
}
int len1 = 0, len2 = 0;//使用len1和len2来存储目标区间1和目标区间2的节点数
auto itR1 = split(r1 + 1);
auto itL1 = split(l1);
for (auto it = itL1; it != itR1; ++it)//遍历区间1将节点存入数组a中
{
a[++len1]._left = it->_left;
a[len1]._right = it->_right;
a[len1]._val = it->_val;
}
auto itR2 = split(r2 + 1);
auto itL2 = split(l2);
for (auto it = itL2; it != itR2; ++it)//遍历区间2将节点存入数组b中
{
b[++len2]._left = it->_left;
b[len2]._right = it->_right;
b[len2]._val = it->_val;
}
itR1 = split(r1 + 1);//由于迭代器被迭代了,所以需要归位一下,又是几个小时,说多了都是泪
itL1 = split(l1);
s.erase(itL1, itR1);//删除区间1
itR2 = split(r2 + 1);
itL2 = split(l2);
s.erase(itL2, itR2);
for (int i = 1; i <= len2; ++i)//遍历数组b,将数组中的节点依次插入区间1中
{
s.insert(node(b[i]._left - l2 + l1, b[i]._right - l2 + l1, b[i]._val));//原边界加上区间1和区间2的边界差就是交换后的边界位置。
}
for (int i = 1; i <= len1; ++i)
{
s.insert(node(a[i]._left - l1 + l2, a[i]._right - l1 + l2, a[i]._val));
}
}
练习题
- Physical Education Lessons - 洛谷
- P4979 矿洞:坍塌 - 洛谷
- P5350 序列 - 洛谷 写了我七八个小时的题目,说多了都是泪