基于STL库set容器实现的珂朵莉树

本文介绍了如何使用C++STL库中的set容器实现珂朵莉树,包括节点构建、操作原理(如插入、删除、分割和遍历区间),以及在区间问题中的应用,展示了其在处理数据压缩和高效操作中的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基于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:111123345566可以被表示为下图
在这里插入图片描述

每数个相同的数据项都被转化为了一段区间存储在同一节点中。

用处

拿样例分,只要不遇见特意设置的数据,保证数据为随机生成的,基于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 提供了以下几种迭代器:

  1. begin()/cbegin()
    返回指向首元素的迭代器,其中 *begin = front
  2. end()/cend()
    返回指向数组尾端占位符的迭代器,注意是没有元素的。
  3. rbegin()/crbegin()
    返回指向逆向数组的首元素的逆向迭代器,可以理解为正向容器的末元素。
  4. 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:111123345566

分割前:在这里插入图片描述

分割后:在这里插入图片描述

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));
	}

}

练习题

  1. Physical Education Lessons - 洛谷
  2. P4979 矿洞:坍塌 - 洛谷
  3. P5350 序列 - 洛谷 写了我七八个小时的题目,说多了都是泪
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值