珂朵莉树学习笔记

今天学了这种抽象的数据结构,发一篇学习笔记(其实是题解)

先上一道题:Physical Education Lessons

题意

给定一个一开始都是 1,长度为 n 的序列,有 q 次操作,每次可能是区间抹 0 或区间抹 1,求最后序列有多少个 1

思路

这题正解是动态开点线段树,歪解就是珂朵莉树

其实珂朵莉树就是和分块、莫队一样的优雅的暴力

虽然这题的 n 很大,但是 q 只达到了 105 级别。根据类似离散化的思路,我们可以发现其实这个序列里面有很多 10 的连续段

所以我们开一个数据结构维护每一个连续的段,而 set 因为有自动排序的特性成为了首选

具体来说,每个段都有三个属性 l,r,vall,r 表示区间的左右端点,val 表示这个连续段里每个数的值。一开始数列里面都是 1,这一段就可以表示成 1,n,1。我们假设 n=1000,那么整个序列长这样:

图炸了

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

图片炸了

注意:切完之后的两个相邻的连续段之间是不重叠的,这一点很重要

再操作 600,800,0 后序列就长这样(我只标每个段的 l 了,上一个段的 r 就是这个段的 l1):

图片炸了

(可能框框长度的比例有点不太对,能理解就行)

再操作 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) 函数就是切分的函数,每次切分会把 pospos1 这一段切开并返回切完了之后右边的那一段的迭代器

split 函数先找到 pos 位置处于哪个段中,把这一段删除,再把 [l,pos1][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 行解释:seterase 有一个用法是 erase(const_iterator __first, const_iterator __last),就是左闭右开删除一个区间内的元素。

split 第 14 行解释:setinsert 操作有返回值:一个 pair。这个 pairfirst 存这个元素的迭代器,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(珂朵莉树的爸爸,操作都是暴力)

总结

珂朵莉树的优点就是大部分操作都可以暴力(很容易想),而且随机数据可以吊打线段树,缺点就是非随机数据容易被卡掉

我永远喜欢珂朵莉(逃

posted @   xlaser  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示