珂朵莉树学习笔记

声明:对于这部分知识我也是从网上学习的,若有雷同绝非有意抄袭。

0x01 简介

珂朵莉树,又名 ODT,出自 CF896C 的正解。主要处理区间修改、查询问题。

是一种基于std::set的 ”指代一种特定的基于数据随机的算法“,它不是数据结构,这点 lxl 先生本人也承认了

它的适用范围狭小,当且仅当以下情况全部满足时才可以使用:

  1. 有区间赋值操作。
  2. 数据保证完全随机。在数据随机的前提下有着优秀的复杂度。

0x02 结构体初始化

struct ct{
    int l,r;
    mutable ll v;
    ct(int L,int R=-1,ll V=0){
        l=L,r=R,v=V;
    }
    bool operator <(const ct &a)const{
        return l<a.l;
    }
};
set<ct> s;

其中值得注意的是,\(v\)​​ 被 mutable 修饰,因为这样可以直接在 set 中修改该变量的值。

但是我们查询的时候不能保证查询的区间端点一定与这些节点的端点重合,如果采用分块思想肯定行不通,因为会退化成暴力。所以我们有了下一步。

0x03 分割区间操作

这是珂朵莉树的核心。

#define str set<ct>::iterator//太长了啊qwq
inline str split(int pos){
    str it=s.lower_bound(ct(pos));
    if(it!=s.end()&&it->l==pos)
        return it;
    it--;
    int l=it->l,r=it->r,v=it->v;
    s.erase(it);
    s.insert(ct(l,pos-1,v));
    return s.insert(ct(pos,r,val)).first;
}

注意,一些人可能会用到 auto 。在以前的 NOI 系列赛事中不可以用 auto,但时代变了,所以可以用了。

这一部分对于 \(pos\) 节点分为 \([l,pos)\) 和 \((pos,r]\) 两部分,并且对所选取的节点 \(it\) 进行判断。当为 \((pos,r]\) 时,直接返回;当为 \([l,pos)\) 时,再次分裂并删去原来的区间。但当 \(pos\) 恰为两边界之一时,会直接返回这个位置。

0x04 合并区间操作

有分裂就要有合并。

inline void assign(int l,int r,int v) {
	str it2=split(r+1),it1=split(l);//注意,这里一定要先split(r+1)再split(l)
	s.erase(it1,it2);
	s.insert(ct(l,r,v));
}

assign 的本质就是分离出 \(l\) 到 \(r\) 的所有节点,然后删除它们,再新建一个 \(l\) 到 \(r\),权值为 \(v\)​ 的节点。看似暴力,却能大大优化复杂度。

assign 的作用很大:保证珂朵莉树的时间复杂度不退化,同时也是保证其在随机数据下时间复杂度较低(趋近于 \(O(m\log n)\))的根源。

0x05 区间加操作

inline void add(int l,int r,ll val){
    str it2=split(r+1),it1=split(l);
    for(;it1!=it2;it1++)
        itl->v+=val;
}

先分离出 \(l\) 到 \(r\) 的所有节点,然后把每个节点的权值都加上 \(v\)​。

0x06 时间复杂度

关于 ODT 的时间复杂度证明较为复杂,本人不太会计算,还请到其他神犇的博客中看看吧 /kk

0x07 CF896C

这道题中还有两个特殊操作。

区间第 \(k\) 小

inline ll rank(int l,int r,int k){
    vector<pair<ll,int>> p;
    str it2=split(r+1),it1=split(l);
    p.clear();
    for (;it1!=it2;it1++)
        p.push_back(pair<ll,int>(it1->v,it1->r-it1->l+1));
    sort(p.begin(),p.end());
    for(vector<pair<ll,int>>:iterator  it=p.begin();it!=p.end();it++){
        k-=it->second;
        if(k<=0) 
            return it->first;
    }
}

分离后把节点按权值从小到大排序,每遍历到一个节点,就将 \(k\) 减去它的长度。如果 \(k\leqslant 0\)​,则当前节点的权值即为第 \(k\)​小值。

区间幂次和

inline ll sum(int l, int r, int x, int y){
    str it2=split(r+1),it1=split(l);
    ll res=0;
    for(;it1!=it2;it1++)
        res=(res+(ll)(it1->r-it1->l+1)*qpow(it1->v,ll(x),ll(y)))%y;
    return res;
}

暴力遍历找到元素,左端点在 \([l,r)\)​ 的区间中最左边的区间到最右边的区间依次使用快速幂加入 \(res\) 。

伪代码都已经给你了,种子生成器很好写,其余的主函数内容也很简单。

View code:

#include<algorithm>
#include<bitset>
#include<cctype>
#include<cerrno>
#include<clocale>
#include<cmath>
#include<complex>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<ctime>
#include<deque>
#include<exception>
#include<fstream>
#include<functional>
#include<limits>
#include<list>
#include<map>
#include<iomanip>
#include<ios>
#include<iosfwd>
#include<iostream>
#include<istream>
#include<ostream>
#include<queue>
#include<set>
#include<sstream>
#include<stack>
#include<stdexcept>
#include<streambuf>
#include<string>
#include<utility>
#include<vector>
#include<cwchar>
#include<cwctype>
#include<chrono>
#include<random>
#include<unordered_map>
using namespace std;

//#define int long long
#define ll long long
#define ull unsigned long long
#define rll register long long
#define ri register int
#define il inline
#define str set<ct>::iterator

const int INF=0x7fffffff,MOD=1e9+7;
int n,m;
ll seed,vmax;
struct ct{
    int l,r;
    mutable ll v;
    ct(int L,int R=-1,ll V=0){
        l=L,r=R,v=V;
    }
    bool operator <(const ct &a)const{
        return l<a.l;
    }
};
set<ct> s;

il ll read(){
    ll x=0,y=1;
    char c=getchar();
    while(c<'0'||c>'9'){
        if(c=='-')
            y=-1;
        c=getchar();
    }
    while(c>='0'&&c<='9'){
        x=x*10+c-'0';
        c=getchar();
    }
    return x*y;
}

il str split(int pos){
    str it=s.lower_bound(ct(pos));
    if(it!=s.end()&&it->l==pos) 
        return it;
    it--;
    int l=it->l,r=it->r;
    ll v=it->v;
    s.erase(it);
    s.insert(ct(l,pos-1,v));
    return s.insert(ct(pos,r,v)).first;

}

il void assign(int l,int r,ll v) {
	str it2=split(r+1),it1=split(l);
    s.erase(it1,it2);
    s.insert(ct(l,r,v));
}

il void add(int l,int r,ll val){
    str it2=split(r+1),it1=split(l);
    for(;it1!=it2;it1++)
        it1->v+=val;
}

il ll rk(int l,int r,int k){
    vector<pair<ll,int>> p;
    str it2=split(r+1),it1=split(l);
    p.clear();
    for(;it1!=it2;it1++)
        p.push_back(pair<ll,int>(it1->v,it1->r-it1->l+1));
    sort(p.begin(),p.end());
    for(vector<pair<ll,int>>::iterator it=p.begin();it!=p.end();it++){
        k-=it->second;
        if(k<=0) 
            return it->first;
    }
    return -1;
}

il ll qpow(ll x,ll y,ll p){
    ll res=1,ans=x%p;
    while(y){
        if(y&1) 
            res=res*ans%p;
        ans=ans*ans%p;
        y>>=1;
    }
    return res;
}

il ll sum(int l,int r,int x,int y){
    str it2=split(r+1),it1=split(l);
    ll res=0;
    for(;it1!=it2;it1++)
        res=(res+1ll*(it1->r-it1->l+1)*qpow(it1->v,1ll*x,1ll*y))%y;
    return res;
}

il ll rnd(){
    ll ret=seed;
    seed=(seed*7+13)%MOD;
    return ret;
}

signed main(){
    n=read(),m=read(),seed=read(),vmax=read();
    for(ri i=1;i<=n;i++)
        s.insert(ct(i,i,rnd()%vmax+1));
    s.insert(ct(n+1,n+1,0));
    for(ri i=1;i<=m;i++){
        int op=int(rnd()%4)+1,l=int(rnd()%n)+1,r=int(rnd()%n)+1,x,y;
        if(l>r)
            swap(l,r);
        if(op==3){
            x=int(rnd()%(r-l+1))+1;
            printf("%lld\n",rk(l,r,x));
        }
        else
            x=int(rnd()%vmax) +1;
        if(op==1)
            add(l,r,1ll*x);
        if(op==2)
            assign(l,r,1ll*x);
        if(op==4){
            y=int(rnd()%vmax)+1;
            printf("%lld\n",sum(l,r,x,y));
        }
    }
    return 0;
}

调了我 26 个小时后终于调出来了啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊

0x07 后记

请切记:珂朵莉树还是要少用,很容易被卡。

上一篇:数据结构 02-线性结构2 一元多项式的乘法与加法运算 (20 分)


下一篇:2020_02_04 LeetCode刷题