浅谈珂朵莉树

0.前言

如有错误,欢迎指出。(什么错误都可以。)

前置芝士

1.还是 oi-wiki 上面的那句话, 会用 STL_SET 就行。(不会的话,也可以去学习一下。)

1.什么是珂朵莉树

当你在 oi-wiki 上面看到一个叫做珂朵莉树的数据结构时,你可能会很好奇,也可能会觉得 oi-wiki 非常高大尚。但是,今天我要在这里告诉你:珂朵莉树贼简单,在比赛中也很实用!

好,不多说废话,我们进入正题。

珂朵莉树起源题:CF896C,也是我们后面讲述的例题。可以发现,在这个题目中,前面三个操作我们都还可以运用主席树实现,但是当你看到第4个操作嘛,呵呵,我可以猜到你的心情。

珂朵莉树,又名 old driver tree 老司机树,(ODT树),在比赛中,一般 是用来骗分的。对于这一种数据结构,它只适用于随机数据,对于精心策划的数据将会被卡掉,但是在比赛中,卡珂朵莉树的数据毕竟少,所以可以在不知道正解的情况下骗到很多分。

为什么我们说他实用呢?因为在比赛中,通常出现这样的情况:正解是一棵长度需要想和写很久的权值线段树或者平衡树,但是,如果你采用珂朵莉树,你可以非常快速的的打完,并且还可以至少得到正解差不了太多分的分数(一般也就差10~20分,并且如果出题人忘记卡了的话,甚至可以得到满分)。更为重要的是,如果你正解代码出现了亿些错误,那么你将会死的更惨,而珂朵莉树易于调试不易写错的特点,则更能保证你在比赛中的成绩。

虽然珂朵莉树在比赛中比较实用,但是在学习 OI 的时候,最好还是不要使用它来骗分了,乖乖打正解吧!

2.珂朵莉树具体做法

珂朵莉树基于 STL_SET 实现,他之所以是一棵树也跟 SET 是有关系的,他的具体思想是:将一个权值相同的子段看作一个节点来看待。

我们先定义一个结构体来储存节点,即每个权值相同的区间:

#define ll long long//因为珂朵莉树可以存下长度很大的节点以及权值,所以开 long long
struct odt
{	
	ll l,r;//将该字段化为一个节点后,他的左端点和右端点
	mutable ll val;//该字段的权值
	bool operator <(const odt &n)const//重载小于运算符,因为要存入set就必须有小于符号的定义
	{
		return l<n.l;
	}
	//下面的在后面代码中有用。
	odt(ll a,ll b,ll c)
	{
		l=a,r=b,val=c;
	}
	odt(ll a)
	{
		l=a;
	}
};

刚刚在代码中,或许你看到了一个你并不认识的关键字 mutablemutable译为可变的。因为我们在 CF896C 当中有区间加操作,需要给改变 \(val\) 的值,而 set 中是默认不能改的,所以我们需要加上 mutable 关键字。

然后,我们在申请一个 SET 来储存这棵珂朵莉树。

set<odt> tree;

接着,我们再宏定义一下 set 的迭代器,方便我们在以后访问时使用。

#define It set<odt>::iterator

注意,接下来是重点!

在珂朵莉树中,最重要的操作无疑是 split 操作,即分裂一个节点。这时,或许你会十分疑惑,珂朵莉树都存好了,为什么还有分裂节点呢?

我们可以先想这样一个问题,假设现在只有一个节点,他表示的是区间 \([1,8]\)。在 CF896C 中,我们有区间加操作。如果我们要将区间 \([5,7]\) 全部加上 \(3\) ,那该怎么办呢。由于我们在珂朵莉树中,不存在以 \(5\)\(7\) 为开头的节点。所以我们需要将节点 \([1,8]\) 分裂成 \([1,4][5,7][8,8]\),我们才能完成操作。所以你现在明白 split 的重要性了吧。

假设我们现在需要一个以 \(x\) 为左端点的节点。我们可以现在 \(set\) 中直接二分查找,找到第一个左端点大于等于 \(x\) 的节点。如果这个左端点于 \(x\) 相等,则就说明 set 中存在以 \(x\) 为左端点的节点,我们就不需要分裂。如果不相等,我们将这个左端点所在的节点的上一个节点进行分裂。(因为上一个节点才包含我们要找的 \(x\)。)最后,我们再返回这个节点所在的迭代器。

代码:

It split(ll x)//分裂,返回以 x 为左端点的节点的迭代器
{
	It it=tree.lower_bound(odt(x));//直接进行二分查找
	if(it!=tree.end()&&it->l==x)//如果相等,且不是 set 中的尾指针
	return it;//直接返回迭代器。
	it--;//我们将当前迭代器减1,因为要找上一个。
	ll l=it->l,r=it->r,val=it->val;//我们找到上一个节点的所有信息,储存下来,以免在删除时指针失效无法访问。
	tree.erase(it);//将该节点删除
	tree.insert(odt(l,x-1,val));//先将该节点分裂为区间 [l,x-1]
	return tree.insert(odt(x,r,val)).first;//然后以 x 为左端点再分裂一个区间,由于 insert 后返回的是一个 pair 类型,所以我们需要返回 first 来返回迭代器。
}

时间复杂度的保证-Assign

显然,如果我们一直分裂下去,节点就会越来越多,渐渐退化成暴力,这样是对我们不利的。所以我们需要去改变。

现在有一个操作,assign,推平一个区间,将一个区间赋成同一个值。显然,我们可以直接取出在这个区间里面的所有节点,然后直接删除,最后在新建一个节点来表示这个区间。

这样,我们就能有效控制节点个数,保证时间复杂度了。由于本人能力有限,不会证明复杂度,所以我就在这里贴一个珂朵莉时间复杂度的证明

Assign 代码:

void assign(ll l,ll r,ll val)//将 [l,r] 赋值为 val
{
	It it2=split(r+1),it1=split(l);//我们无论是什么操作,我们都先取出右端点,因为先取左端点的话,左端点的指针可能会失效。
	tree.erase(it1,it2);//删除区间 [l,r+1)
	tree.insert(odt(l,r,val));//新建以l为左端点,r为右端点的节点
}

其他操作

一个比一个更暴力更玄学,就是把这个区间所有的节点取出来就可以了。

操作三,求区间第 \(k\) 小整数示例代码:

ll qsort(ll l,ll r,ll x)
{
	It it2=split(r+1),it1=split(l);//还是像往常一样,取出以左端点开始,和 右端点加一的 迭代器。
	vector<pair<ll,ll> > p;//为了方便排序,直接用pair
	for(It it=it1;it!=it2;it++)
	{
		p.push_back(make_pair(it->val,it->r-it->l+1));//将区间中所有节点放入p数组。
	}
	stable_sort(p.begin(),p.end());//将p数组排序
	for(ll i=0;i<p.size();++i)
	{
		x-=p[i].second;//每次减去这个结点的长度
		if(x<=0)//如果小于等于0,那显然第 x 小的整数就在这个节点上。
		return p[i].first;
	}
}

操作4,求区间每个数字的 \(x\) 次方模 \(y\) 的值的和 实例代码:

ll qpow(ll a,ll b,ll p)//快速幂 a^b%p
{
    ll ans=1;
    a%=p;
    while(b)
    {
        if(b&1)
        (ans*=a)%=p;
        (a*=a)%=p;
        b>>=1;
    }
    return ans%p;
}
ll pow_mod(ll l,ll r,ll x,ll y)
{
	It it2=split(r+1),it1=split(l);//照常取出
	ll ans=0;//答案
	for(It it=it1;it!=it2;it++)
	{
   	       //将每个节点暴力取出算快速幂就可以了。
		ll res=qpow(it->val,x,y)*(it->r-it->l+1);
		res%=y;
		ans+=res;
		ans%=y;
	}
	return ans;//返回
}

我们很早就说到,珂朵莉树只适合解决随机生成的数据,而这个题目不就是给你随机数种子自己生成数据吗,那珂朵莉树的复杂度是肯定可以承受住这个题目的了。

最后,贴上完整代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mod int(1e9+7)
#define It set<odt>::iterator
ll n,m;
ll seed,vmax;
ll a[1000005];
ll rnd()
{
	ll res=seed;
	seed=(seed*7+13)%mod;
	return res;
}
ll qpow(ll a,ll b,ll p)
{
    ll ans=1;
    a%=p;
    while(b)
    {
        if(b&1)
        (ans*=a)%=p;
        (a*=a)%=p;
        b>>=1;
    }
    return ans%p;
}
struct odt
{
	ll l,r;
	mutable ll val;
	bool operator <(const odt &n)const
	{
		return l<n.l;
	}
	odt(ll a,ll b,ll c)
	{
		l=a,r=b,val=c;
	}
	odt(ll a)
	{
		l=a;
	}
};
set<odt> tree;
It split(ll x)
{
	It it=tree.lower_bound(odt(x));
	if(it!=tree.end()&&it->l==x)
	return it;
	it--;
	ll l=it->l,r=it->r,val=it->val;
	tree.erase(it);
	tree.insert(odt(l,x-1,val));
	return tree.insert(odt(x,r,val)).first;
}
void assign(ll l,ll r,ll val)
{
	It it2=split(r+1),it1=split(l);
	tree.erase(it1,it2);
	tree.insert(odt(l,r,val));
}
void add(ll l,ll r,ll val)
{
	It it2=split(r+1),it1=split(l);
	for(It it=it1;it!=it2;it++)
	{
		it->val+=val;
	}
}
ll qsort(ll l,ll r,ll x)
{
	It it2=split(r+1),it1=split(l);
	vector<pair<ll,ll> > p;
	for(It it=it1;it!=it2;it++)
	{
		p.push_back(make_pair(it->val,it->r-it->l+1));
	}
	stable_sort(p.begin(),p.end());
	for(ll i=0;i<p.size();++i)
	{
		x-=p[i].second;
		if(x<=0)
		return p[i].first;
	}
}
ll pow_mod(ll l,ll r,ll x,ll y)
{
	It it2=split(r+1),it1=split(l);
	ll ans=0;
	for(It it=it1;it!=it2;it++)
	{
		ll res=qpow(it->val,x,y)*(it->r-it->l+1);
		res%=y;
		ans+=res;
		ans%=y;
	}
	return ans;
}
int main()
{
	scanf("%lld%lld%lld%lld",&n,&m,&seed,&vmax);
	for(int i=1;i<=n;++i)
	{
		a[i]=rnd()%vmax+1;
		tree.insert(odt(i,i,a[i]));
	}
	tree.insert(odt(n+1,n+1,0));
	while(m--)
	{
		int op=rnd()%4+1,l=rnd()%n+1,r=rnd()%n+1,x,y;
		if(l>r)
		swap(l,r);
		if(op==3)
		x=rnd()%(r-l+1)+1;
		else
		x=rnd()%vmax+1;
		if(op==4)
		y=rnd()%vmax+1;
		if(op==1)
		add(l,r,x);
		else if(op==2)
		assign(l,r,x);
		else if(op==3)
		printf("%lld\n",qsort(l,r,x));
		else
		printf("%lld\n",pow_mod(l,r,x,y));
	}
	return 0;
}

3.后记

珂朵莉树这东西好是好,简单是简单。但是我还是觉得平常练习的时候就不要用了,要是教练看到你用了,一气之下把你卡掉那可就不好玩咯。

至于有关珂朵莉树的例题嘛,还是很多的,可以参见 oi-wiki 和其他有关珂朵莉树的文章。

posted @ 2024-03-02 21:59  Saltyfish6  阅读(27)  评论(0编辑  收藏  举报
Document