Old Driver Tree——浅谈珂朵莉树

本文转载自单南松大佬的博客,只修改了码风QAQ。

绪言:研究珂朵莉树的原因

总是看到好多大佬在研究这个十分二次元化的东西(对于我这种男性 OIer 是这个感觉),所以想深入了解一下。

珂朵莉树的起源

珂朵莉树原名老司机树(Old Driver Tree,ODT),是一种基于 std::set 的暴力数据结构,由2017年一场CF比赛中提出的数据结构,因为题目背景主角是《末日时在做什么?有没有空?可以来拯救吗?》的主角珂朵莉,因此该数据结构被称为珂朵莉树。

应用

解决各种线段树无法完成的操作。

注意珂朵莉树保持复杂度主要依靠 assign 操作,所以题目中必须有区间赋值。

还有很重要的一点:数据需纯随机。

什么时候用珂朵莉树

关键操作:推平一段区间,使一整段区间内的东西变得一样。保证数据随机。

$n$ 个数,$m$ 次操作。 $n,m \le 105$

操作:

  • 区间加;

  • 区间赋值;

  • 区间第 $k$ 小;

  • 求区间幂次和;

  • 数据随机,时限 $2s$。

构造

用一个带结构体的集合(std::set)维护序列。

集合中的每个元素有左端点,右端点,值。

下面展示该结构体的构造:

struct node
{
    int l,r;
    mutable int val;
    node(int a=-1,int b=-1,int c=0){ l=a,r=b,val=c; }
    bool operator<(const node &a)const{ return l<a.l; }
};

//mutale,意为可变的,即不论在哪里都是可修改的,用于突破C++带const函数的限制。

Split

set::iterator split(int pos)

将原来含有 $pos$ 的区间分为 $[l,pos)$ 和 $[pos,r]$ 两段。

返回一个 std::set 的迭代器,指向 $[pos,r]$段。

可能有些抽象,详细解如下:

split 函数的作用就是查找 set 中第一个左端点不小于 $pos$ 的结点,如果找到的结点的左端点等于 $pos$ 便直接返回指向该结点的迭代器,如果不是,说明 $pos$ 包含在前一个结点所表示的区间之间,此时便直接删除包含 $pos$ 的结点,然后以 $pos$ 为分界点,将此结点分裂成两份,分别插入 set 中,并返回指向后一个分裂结点的迭代器。

首先我们假设 set 中有三个 node 结点,这三个结点所表示的区间长度为 $14$,如下图:

不妨以提取区间 $[10,12]$ 为例详细展开((躁动的读者)诶,说好的查询 $10$ 到 $12$ 呢,怎么下面扯了一堆 $13$?别急,后续将会揭晓 ):

如果我们要查询序列第 $13$ 个位置,首先执行iter it = s.lower_bound(node(13)); 此时 it 将成为一个指向第三个结点的迭代器,为什么是第三个结点,而不是第二个结点呢,因为 lower_bound 这个函数获取的是第一个左端点 $l$ 不小于 $13$ 的结点,所以 it 是指向第三个结点的。

然后执行判断语句,发现第三个结点的左端点不是 $13$,不满足条件,说明13必包含在前一个结点中,继续向下执行,让 $it$ 指向前一个结点:

先将该结点的信息保存下来:int l=10,r=13,val=2,然后直接删除该结点。

以 $pos$ 为分界点,将被删除的结点分裂为 $[l,pos-1]$ ,$[pos,r]$ 这两块,并返回指向 $[pos,r]$ 这个区间的迭代器,事实上return s.insert(node(pos,r,val)).first; ,便做到了插入 $[pos,r]$ 这端区间,并返回指向它的迭代器,有一个 insert 函数返回值为 pair 类型,其中 pair 的第一个元素就是元素插入位置的迭代器。

至此 $13$ 位置已经分裂完成,然后是查询第 $10$ 个位置,查询步骤同上,但是 $10$ 号点满足 if 语句,便直接返回了。

由上述步骤,为了提取区间 $[10,12]$,我们执行了两次 split ,一次为 split(13),一次为 split(10),并获得了两个迭代器,一个指向第二结点,一个指向第三结点。

为什么要先分裂右端点,然后再分裂左端点呢?

因为如果先分裂左端点,返回的迭代器会位于所对应的区间以 $l$ 为左端点,此时如果 $r$ 也在这个节点内,就会导致分裂左端点返回的迭代器被 erase 掉,导致 RE。

结合问题 $1$ 和问题 $2$ ,获取区间迭代器时,务必写成如下格式 iter itr=split(r+1),itl=split(l); 起名无所谓,按自己的习惯就好。

代码

iter split(int pos)
{
    iter it=st.lower_bound(node(pos));
    if (it!=st.end() && it->l==pos)  return it;
    --it;
    node tmp=*it; st.erase(it);
    st.insert(node(tmp.l,pos-1,tmp.val));
    return st.insert(node(pos,tmp.r,tmp.val)).first;
}

Assign

注意:以后在使用 split 分裂区间的时候,请先右后左。

区间赋值操作,也是珂树维持其复杂度的关键函数。

很暴力的思想,既然刚刚我们写了一个 split,那么就要把它用起来。
首先 split 出 $l$ 并记返回值为 $itl$,然后 split 出 $r+1$ 并记返回值为 $itr$,显然我们要操作的区间为 $[itl,itr)$,那么我们将 $[itl,itr)$ 删除(std::set.erase(itl, itr)),再插入一个节点 node,其 $l$ 为 $l$,$r$ 为 $r$,$val$ 为赋值的 $val$。

我们注意到因为这个操作,$[itl,itr)$ 中的所有节点合并为了一个节点,大大降低了集合的元素数量,因此调整了我们的复杂度。

void assign(int l,int r,int val)
{
	iter itr=split(r+1),itl=split(l);
    st.erase(itl,itr);
    st.insert((node){l,r,val});
}
//将一个区间全部改为某个值。

其他操作

通用方法是 split 出 $l$,split 出 $r+1$,然后直接暴力扫描这段区间内的所有节点执行需要的操作。

例如我们的区间和查询:

int querySum(int l,int r)
{
	int res=0;
    iter itr=split(r+1),itl=split(l);
    for(iter it=itl;it!=itr;it++)
        res+=(it->r-it->l+1)*it->val;
return res;
}

例如我们的区间加:

void add(int l,int r,int val)
{
    iter itr=split(r+1),itl=split(l);
    for(iter it= itl;it!=itr;it++)  it->val+=val;
}

例如我们的区间第k小:

前置需要

algorithm 库中的 std::sort(快速排序)。

std::map(方便起见使用其中的 pair),std::vector(方便起见)。

还是 split 出 $l$,split 出 $r+1$,然后将每个节点的值和个数(即 $r-l+1$)组成一个 pair(注意为了排序,将值放在第一关键字),将 pair 加入一个 vector 中。

将 vector 排序。

从 vector 的 begin 开始扫描,不停的使 $k$ 减去 vector 当前项的第二关键字,若 $k \le 0$,返回当前项的第一关键字。

int queryKth(int l,int r,int k)
{
    vector<PII> vec(0);
    iter itr=split(r+1),itl=split(l);
    for(iter it=itl;it!=itr;it++)
        vec.push_back(make_pair(it->val,it->r-it->l+1));
    sort(vec.begin(),vec.end());
    for(vector<PII>::iterator it=vec.begin();it!=vec.end();it++)
        if((k-=it->second)<=0)  return it->first;
return -1; 
//note:if there are negative numbers, return another impossible number.
}

求区间所有数的 $x$ 次方的和模 $y$ 的值。

//快速幂取模
int qpow(int a,int b,int p)
{
    int rec=1ll;
    a%=p;
    for(;b;b>>=1,a=a*a%p)
	    if(b&1)  rec=rec*a%p;
return rec;
}
//提取区间,暴力运算
int queryPow(int l,int r,int x,int y)
{
    iter itr=split(r+1),itl=split(l);
    int res=0;
    for(iter it=itl;it!=itr;it++)
        res=(res+(it->r-it->l+1)*qpow(it->val,x,y))%y;
return res;
}

珂朵莉树代码样例

/*Chtholly, flatten it! */
#include<cstdio>
#include<vector>
#include<algorithm>
#include<set>
#include<map>
#define int long long
#define iter set<node>::iterator
#define PII pair<int,int>
using namespace std;
struct node
{
    int l,r;
    mutable int val;
    node(int a=-1,int b=-1,int c=0){ l=a,r=b,val=c; }
    bool operator<(const node &a)const{ return l<a.l; }
};
set<node> st;
iter split(int pos)
{
    iter it=st.lower_bound(node(pos));
    if (it!=st.end() && it->l==pos)  return it;
    --it;
	node tmp=*it; st.erase(it);
    st.insert(node(tmp.l,pos-1,tmp.val));
    return st.insert(node(pos,tmp.r,tmp.val)).first;
}
void assign(int l,int r,int val)
{
	iter itr=split(r+1),itl=split(l);
    st.erase(itl,itr);
    st.insert((node){l,r,val});
}
void add(int l,int r,int val)
{
    iter itr=split(r+1),itl=split(l);
    for(iter it= itl;it!=itr;it++)  it->val+=val;
}
int querySum(int l,int r)
{
	int res=0;
    iter itr=split(r+1),itl=split(l);
    for(iter it=itl;it!=itr;it++)
        res+=(it->r-it->l+1)*it->val;
return res;
}
int queryKth(int l,int r,int k)
{
    vector<PII> vec(0);
    iter itr=split(r+1),itl=split(l);
    for(iter it=itl;it!=itr;it++)
        vec.push_back(make_pair(it->val,it->r-it->l+1));
    sort(vec.begin(),vec.end());
    for(vector<PII>::iterator it=vec.begin();it!=vec.end();it++)
        if((k-=it->second)<=0)  return it->first;
return -1; 
//note:if there are negative numbers, return another impossible number.
}
signed main()
{
	
return 0;
}

例题详解CF896C

一说起区间维护问题,我们就能想到线段树,主席树,树状数组,Splay,分块,莫队等数据结构,但是读完题目我们发现,第四个操作涉及每个数字的相关操作,上面提到的结构只有莫队可以做到,但是复杂度太高,我们需要一个更加高效的数据结构 珂朵莉来维护这些操作。

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<set>
#include<map>
#define int long long
#define PII pair<int,int>
#define iter set<node>::iterator
using namespace std;
const int N=1e5+5,P=1e9+7;
int seed,a[N];
int rnd()
{
	int ret=seed;
	seed=(seed*7+13)%P;
return ret;
}
int read()
{
    char ch;
	int v,f=1;
    for(ch='*';(ch<'0'||ch>'9') && ch!='-';ch=getchar());
    if(ch=='-')  f=-1,ch=getchar();
    for(v=0ll;ch>='0' && ch<='9';ch=getchar())  v=v*10+ch-'0';
return v*f;
}
int qpow(int a,int b,int p){
	if (!a)  return 0;
	int rec=1ll; a%=p;
	for(;b;b>>=1ll,a=a*a%p)
		if(b&1)  rec=rec*a%p;
return rec;
}
struct node
{
    int l, r;
    mutable int val;
    node(int a=-1,int b=-1,int c=0){ l=a,r=b,val=c; }
    bool operator<(const node &a)const{ return l<a.l; }
};
set<node> st;
iter split(int pos)
{
    iter it=st.lower_bound(node(pos));
    if(it!=st.end() && it->l==pos)  return it;
    --it;
	node tmp=*it; st.erase(it);
    st.insert(node(tmp.l,pos-1,tmp.val));
return st.insert(node(pos,tmp.r,tmp.val)).first;
}
void assign(int l,int r,int val)
{
	iter itr=split(r+1),itl=split(l);
    st.erase(itl,itr);
    st.insert((node){l,r,val});
}
void add(int l, int r, int val)
{
    iter itr=split(r+1),itl=split(l);
    for(iter it=itl;it!=itr;it++)  it->val+=val;
}
int querySum(int l,int r)
{
    iter itr=split(r+1),itl=split(l);
	int res=0;
    for(iter it=itl;it!=itr;it++)
        res+=(it->r-it->l+1)*it->val;
return res;
}
int querySumWithPow(int l,int r,int x,int p)
{
    iter itr=split(r+1),itl=split(l); 
	int res=0;
    for(iter it=itl;it!=itr;it++)
        (res+=(it->r-it->l+1)*qpow(it->val,x,p))%=p;
    return res;
}
int queryKth(int l,int r,int k)
{
    vector<PII> vec(0);
    iter itr=split(r+1),itl=split(l);
    for(iter it=itl;it!=itr;it++)
        vec.push_back(make_pair(it->val,it->r-it->l+1));
    sort(vec.begin(),vec.end());
    for(vector<PII>::iterator it=vec.begin();it!=vec.end();it++)
        if((k-=it->second)<=0)  return it->first;
return -1;
//note:if there are negative numbers, return another impossible number.
}
signed main()
{
	int n=read(),m=read();
	seed=read();
	int vmax=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=(rnd()%vmax)+1;
		st.insert((node){i,i,a[i]});
	}
	int x,y;
	for(int i=1;i<=m;i++)
	{
	    int op=(rnd()%4)+1,l=(rnd()% n)+1,r=(rnd()%n)+1;
	    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",queryKth(l,r,x));
	    else if(op==4)  printf("%lld\n",querySumWithPow(l,r,x,y));
	}
return 0;
}
posted @ 2022-08-05 18:20  青鸢挽歌  阅读(240)  评论(0编辑  收藏  举报
浏览器标题切换
浏览器标题切换end