珂朵莉树

Forward_Star

2019-09-25 17:53:03

Personal

## ~~末日三问~~ ### Q:什么是珂朵莉树? A:珂朵莉树是一种存储区间信息的数据结构;简单地说,是把整个区间划分为一个个互不相交的小区间,通过将含相同元素的区间合并为一段区间,可以提高某些询问与操作的效率。 ### Q:珂朵莉树有什么用途? A:只要是涉及到区间赋值操作的题,就可以用珂朵莉树处理几乎任何关于区间信息的询问。另外,也可以帮助我们更好的研究珂学~~(说句闲话,研究珂学的最好方式是……)~~。 ### Q:什么情况下可以用珂朵莉树? A:首先要明确的是,珂朵莉树本质上是一种暴力的算法。这很容易理解,既然她的高效建立在区间合并操作的基础上,那么如果构造一组数据使得其几乎不含区间赋值操作,区间合并派不上用场,珂朵莉树会被轻易卡掉。 因此,珂朵莉树要求题目数据具有**高度随机性**。 ## 前置知识 珂朵莉树需要用到结构体和stl库的set(不知道有没有大佬会手写平衡树去代替set维护区间%%)。网上的一些详解主要是对珂朵莉树操作的解释,但却没有对于结构体和set操作的说明,对于我等蒟蒻不甚友好。 ### 结构体&&珂朵莉树定义 前面说到,珂朵莉树是存储区间信息的,因此我们可以定义结构体来表示区间。 ```cpp struct node { int l,r; //表示区间左右端点 mutable int v; //表示该区间中每个数的值 node(int L,int R=-1,bool V=0) //这个比较玄学,大概是赋一个初值,没有的话会在后续求lower_bound(某个set操作)时编译错误 { l=L; r=R; v=V; }; bool operator<(const node&next) const //重载运算符 { return l<next.l; } }; //注意结束的大括号后要打分号 ``` 结构体,可以理解为一个集合(这里仅指高一数学课本上定义的集合,不是指stl库的set)。在结构体中定义的变量就是这个集合的元素。 定义结构体后我们可以把它看作一种数据类型,直接用来定义变量。 ```cpp node a; //这样,就定义了一个数据类型为node(前文定义的结构体)的变量 a.l = 1; a.r = 10; a.v = 0; //调用结构体内某个变量时,只需要"a.(结构体内变量名)"即可 ``` 以上是结构体的基本定义及使用。然后大家可能有疑问:为什么定义结构体内$v$的时候用了$mutable$? 这是因为在调用函数时,往往只传参,但是函数内操作不会更改其实际的值。而珂朵莉树往往涉及需要修改$v$的操作,如果是一般的数据类型,我们会在变量前加上&,但对于结构体,就需要在定义时加上$mutable$。 可能稍有难度的是结构体内的重载运算符,这是因为结构体内涉及到几个元素,所以一般的运算符是不能直接用于结构体之间的运算或比较的。重载运算符相当于给运算符“设定规则”,比如对于我们上文定义的$node$,重载运算符后两个$node$类型之间的$<$就是只比较其$l$的大小了。 ### stl::set的一些操作 以下是珂朵莉树需要用到的一些set操作: 1、定义set: ```cpp #include<set> set<node> Chtholly_tree; ``` 需要用到头文件set,将某个变量定义为set的语句为"set<set存放数据类型> 变量名"。对于珂朵莉树,我们要定义一个存放$node$($node$定义见前文)的set。 2、插入(insert): set也可以看成一个集合,不过它支持多种操作而且有序。既然是集合,那它肯定不止一个元素,因此不需要再套上一个数组。 加入元素可以用一行代码解决: ```cpp Chtholly_tree.insert(node(l,r,v)); ``` 这样就可以插入一个区间,由于重载运算符后$<$号是比较区间左端点大小,所以set中的区间是从左到右排列的。 3、删除(erase): 删除操作有两种: ```cpp Chtholly_tree.erase(it); //删除it指代的区间 Chtholly_tree.erase(left,right); //删除left到right(不包含right)之间的所有区间 ``` 其中$it,left,right$都是迭代器,可以指代set中的元素;它大致类似于数组的下标,在珂朵莉树中迭代器用于指代区间。 获取迭代器的信息可以用"->",比如: ```cpp it->l //区间左端点 it->r //区间右端点 it->v //区间中数字的值 ``` 4、lower_bound: 学过平衡树(或者微积分)的同学应该很清楚这个概念:这是指大于等于某个数的所有数中最小的数。 那么要查找左端点离$pos$最近的区间,我们就可以用下面这一行代码解决: ``` Chtholly_tree.lower_bound(node(pos)); ``` 它返回的是一个迭代器,指代对应的区间。 ## 珂朵莉树的实现 明白了上述的操作之后,珂朵莉树的实现就非常简单了。 珂朵莉树有两个关键操作:split和assign_val。 ### split $split(pos)$将返回一个左端点为$pos$的迭代器(区间)。 但是set中并不一定有现成左端点为$pos$的区间,所以顾名思义,我们把左端点最接近$pos$的区间$split$(分裂)一下,分成$[l,pos-1],[pos,r]$两部分,其中$l,r$分别是该区间左、右端点。然后,返回$[pos,r]$的那段区间。 ```cpp set<node>::iterator split(int pos) { set<node>::iterator it=Chtholly_tree.lower_bound(node(pos)); //见上述set操作,此为查找离左端点pos最近且大于pos的区间 if (it!=Chtholly_tree.end()&&it->l==pos) return it; //如果有现成左端点为pos的区间则直接返回 it--; //因为找到的区间在pos右边,所以取上一个区间 int left=it->l; int right=it->r; int temp=it->v; Chtholly_tree.erase(it); Chtholly_tree.insert(node(left,pos-1,temp)); return Chtholly_tree.insert(node(pos,right,temp)).first; } ``` ### assign_val 这个操作叫区间推平,和$split$相反,它并不是要分裂某个区间而是要把某几个区间合并,主要用于区间赋值。 ```cpp void assign_val(int l,int r,int temp) { set<node>::iterator itr=split(r+1); //+1的原因是使得分裂后得到两个区间中左边的区间右端点能刚好取到r set<node>::iterator itl=split(l); //先分裂最右边的区间,再分裂左边区间是防止分裂完左边区间后得到的区间包含右端点r,从而分裂右区间时该区间被再一次分裂 Chtholly_tree.erase(itl,itr); //相当于把[l,r]段清空 Chtholly_tree.insert(node(l,r,temp)); //然后重新插入[l,r]段,实现区间赋值 } ``` ## 完整地写出珂朵莉树 珂朵莉树思想并不复杂,但是对于初学者而言可能会被其各种set的操作绕晕。 下面我们通过[一道例题](https://www.luogu.org/problem/CF915E),来尝试写一下珂朵莉树的代码。 题目的操作无非就是把一段区间全部变为工作日或非工作日,那么我们可以用bool类型,0表示非工作日,1表示工作日,然后利用珂朵莉树的assign_val进行区间赋值即可。 那么,首先定义珂朵莉树,写好split,assign_val,然后对于每个操作,执行assign(l,r,k-1). 只需要复制一下上面的代码,再加上几行代码统计一下工作日,就可以拼成一个正解,很简单吧! ```cpp #include<cstdio> #include<set> using namespace std; struct node { int l,r; mutable bool v; node(int L,int R = -1,bool V = 0) { l = L; r = R; v = V; } bool operator <(const node&next) const { return l < next.l; } }; set<node> Chtholly_tree; int n,q,ans; set<node>::iterator split(int pos) { set<node>::iterator it = Chtholly_tree.lower_bound(node(pos)); if (it != Chtholly_tree.end() && it->l == pos) return it; it --; int l = it->l; int r = it->r; int v = it->v; Chtholly_tree.erase(it); Chtholly_tree.insert(node(l,pos-1,v)); return Chtholly_tree.insert(node(pos,r,v)).first; } void assign_val(int l,int r,int temp) { set<node>::iterator itr = split(r+1); set<node>::iterator itl = split(l); set<node>::iterator it = itl; for (;it != itr;it ++) ans -= it->v * (it->r-it->l+1); Chtholly_tree.erase(itl,itr); Chtholly_tree.insert(node(l,r,temp)); ans += (r - l + 1) * temp; } int main() { scanf("%d%d",&n,&q); ans = n; Chtholly_tree.insert(node(1,n,1)); for (int i = 1;i <= q;i ++) { int l,r,k; scanf("%d%d%d",&l,&r,&k); assign_val(l,r,k-1); printf("%d\n",ans); } return 0; } ``` ## 总结 最后的最后,还是提醒大家一点:珂朵莉树终究只是暴力!除了区间推平操作可以在随机数据下保证她的高效性,其它操作都是用最简单粗暴的方法实现的。所以,在欣赏她的这种暴力美学的同时,也不要过分依赖珂朵莉树。