珂朵莉树学习笔记
今天学了这种抽象的数据结构,发一篇学习笔记(其实是题解)
先上一道题:Physical Education Lessons
题意
给定一个一开始都是 \(1\),长度为 \(n\) 的序列,有 \(q\) 次操作,每次可能是区间抹 \(0\) 或区间抹 \(1\),求最后序列有多少个 \(1\)
思路
这题正解是动态开点线段树,歪解就是珂朵莉树
其实珂朵莉树就是和分块、莫队一样的优雅的暴力
虽然这题的 \(n\) 很大,但是 \(q\) 只达到了 \(10^{5}\) 级别。根据类似离散化的思路,我们可以发现其实这个序列里面有很多 \(1\) 或 \(0\) 的连续段
所以我们开一个数据结构维护每一个连续的段,而 set 因为有自动排序的特性成为了首选
具体来说,每个段都有三个属性 \(l,r,val\)。\(l,r\) 表示区间的左右端点,\(val\) 表示这个连续段里每个数的值。一开始数列里面都是 \(1\),这一段就可以表示成 \(1,n,1\)。我们假设 \(n=1000\),那么整个序列长这样:

区间修改成一个数的操作(推平操作)也有三个属性,任然是 \(l,r,val\)。\(l,r\) 表示修改的区间,\(val\) 表示要改成的值。由于推平操作会把 \(l\) 到 \(r\) 的所有数字都改成一样的,所以我们先把 \(l\) 和 \(r\) 切开,在把中间合并成一个属性为 \(l,r,val\) 的连续段。比如第一次操作是 \(200,500,0\),那么这次操作完后的序列应该长这样:

注意:切完之后的两个相邻的连续段之间是不重叠的,这一点很重要
再操作 \(600,800,0\) 后序列就长这样(我只标每个段的 \(l\) 了,上一个段的 \(r\) 就是这个段的 \(l-1\)):

(可能框框长度的比例有点不太对,能理解就行)
再操作 \(100,550,1\) 后序列长这样:

此时虽然第 \(1,2,3\) 段都是 \(1\),但是它们没有合并,因为每次推平操作只把推平范围内的段全部合并,但此次操作两边切的并不会自己合并(当然你也可以判断然后合并)
推平操作的代码十分简单(ans 维护 \(1\) 的数量):
void update(int l,int r,int val)
{
auto itr=split(r+1);
auto itl=split(l);
for(auto it=itl;it!=itr;it++)
{
ans-=it->val*(it->r-it->l+1);
}
s.erase(itl,itr);
s.insert(node(l,r,val));
ans+=val*(r-l+1);
}
其中 split(pos) 函数就是切分的函数,每次切分会把 \(pos\) 到 \(pos-1\) 这一段切开并返回切完了之后右边的那一段的迭代器
split 函数先找到 \(pos\) 位置处于哪个段中,把这一段删除,再把 \([l,pos-1]\) 和 \([pos,r]\) 加入就可以了
实现仍然很简单
set<node>::iterator split(int pos)
{
auto it=s.lower_bound(pos);
if(it!=s.end() && it->l==pos)
{
return it;
}
it--;
int l=it->l;
int r=it->r;
bool val=it->val;
s.erase(it);
s.insert(node(l,pos-1,val));
return s.insert(node(pos,r,val)).first;
}
update 第 9 行解释:set 的 erase 有一个用法是 erase(const_iterator __first, const_iterator __last),就是左闭右开删除一个区间内的元素。
split 第 14 行解释:set 的 insert 操作有返回值:一个 pair。这个 pair 的 first 存这个元素的迭代器,second 存此次操作是否成功。
最后每次操作完输出一下 ans 就做完了。
代码
#include <iostream>
#include <set>
using namespace std;
struct node
{
int l,r;
mutable bool val;
node(int ll,int rr=-1,bool vall=false)
{
l=ll;
r=rr;
val=vall;
}
bool operator<(const node b)const
{
return l<b.l;
}
};
int n,q;
set<node> s;
int ans;
set<node>::iterator split(int pos)
{
auto it=s.lower_bound(pos);
if(it!=s.end() && it->l==pos)
{
return it;
}
it--;
int l=it->l;
int r=it->r;
bool val=it->val;
s.erase(it);
s.insert(node(l,pos-1,val));
return s.insert(node(pos,r,val)).first;
}
void update(int l,int r,int val)
{
auto itr=split(r+1);
auto itl=split(l);
for(auto it=itl;it!=itr;it++)
{
ans-=it->val*(it->r-it->l+1);
}
s.erase(itl,itr);
s.insert(node(l,r,val));
ans+=val*(r-l+1);
}
int main()
{
cin>>n>>q;
ans=n;
s.insert(node(1,n,true));
for(int i=1;i<=q;i++)
{
int l,r,k;
cin>>l>>r>>k;
if(k==1)
{
update(l,r,false);
}
else
{
update(l,r,true);
}
cout<<s.size()<<endl;
cout<<ans<<endl;
}
return 0;
}
习题
CF817F MEX Queries(多一种反转操作,暴力查询)
P2572 [SCOI2010] 序列操作(珂朵莉树会被卡掉,不过可以当练习,多了查询连续 \(1\) 的数量)
CF896C Willem, Chtholly and Seniorious(珂朵莉树的爸爸,操作都是暴力)
总结
珂朵莉树的优点就是大部分操作都可以暴力(很容易想),而且随机数据可以吊打线段树,缺点就是非随机数据容易被卡掉
我永远喜欢珂朵莉(逃

浙公网安备 33010602011771号