珂朵莉树·颜色段均摊

珂朵莉树,ODT(Old Driver Tree),颜色段均摊都是它。其实我觉得硬说这是一种数据结构(尤其是“树”)是不恰当的,这更多应该是一种技巧。
与线段树等传统数据结构的区别在于:它可以更方便地维护每个被覆盖区间的值。如模板题中的操作 4:求 \(\sum_{i=l}^ra_i^x\bmod y\)

实现

一般用 std::set 实现。

节点

struct node{
    int l,r;
    mutable int val;
    bool operator<(const node &x)const{
        return l<x.l;
    }
};
set<node> odt;

\(l\)\(r\) 表示这一段的区间,\(val\) 表示这一段的权值,使用 mutable 修饰是为了使得结构体或函数在被 const 修饰后仍能修改 \(val\) 的值,这样,我们就可以直接修改在 set 内部的元素的 \(val\)

split

用于将一个区间为 \([l,r]\) 的区间分裂为 \([l,pos)\)\([pos,r]\),并返回指向后者的迭代器的函数。

auto split(int pos){
    if(pos>n) return odt.end();
    auto it=odt.lower_bound({pos});
    if(it!=odt.end()&&it->l==pos) return it;//pos已是左端点,无需分割
    it--;//从上一个分割
    int l=it->l,r=it->r,val=it->val;
    odt.erase(it);//删除原区间
    odt.insert({l,pos-1,val});//左区间
    return odt.insert({pos,r,val}).first;//右区间
}

std::set.insert() 返回一个 std::pair<iterator,bool>,表示插入元素的迭代器及插入是否成功。
现代编译器应当都可以将 auto 识别为 std::set<node>::iterator

assign

用于区间赋值。同时也是时间复杂度的保证,以模板题为例,大约 \(\dfrac{1}{4}\) 的操作调用了 assign,而这个操作可以大幅减小 set 的大小。
特别注意:在截取 \([l,r]\) 时一定要先调用 split(r+1) 再调用 split(l),否则可能导致 RE。 具体原因可以看这里。我大致解释一下,就是若删除了迭代器 it,就会导致 it 及以后的迭代器失效,此时若强行访问失效部分就是 UB。如果我们先操作前面再操作后面,itlitr 之间包含的区间就可能不是我们想要的那一部分。由此我们就需要特别注意,珂朵莉树的任何涉及 erase 的操作都要先操作后面再操作前面。如区间 swap

void assign(int l,int r,int val){
    auto itr=split(r+1),itl=split(l);//截取[l,r]
    odt.erase(itl,itr);//删除[l,r]
    odt.insert({l,r,val});//插入新值
    return;
}

模板题

对于 1 操作,3 操作和 4 操作,直接分离出对应区间后暴力求解。2 操作直接用 assign

#include<iostream>
#include<set>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long ll;
struct node{
    int l,r;
    mutable ll val;
    bool operator<(const node &x)const{return l<x.l;}
};
ll qpow(ll a,int b,int mod){
    ll res=1;
    a%=mod;
    while(b){
        if(b&1) res=res*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return res;
}
set<node> odt;
auto split(int pos){
    auto it=odt.lower_bound({pos});
    if(it!=odt.end()&&it->l==pos) return it;
    it--;
    int l=it->l,r=it->r;
    ll val=it->val;
    odt.erase(it);
    odt.insert({l,pos-1,val});
    return odt.insert({pos,r,val}).first;
}
void assign(int l,int r,int val){
    auto itr=split(r+1),itl=split(l);
    odt.erase(itl,itr);
    odt.insert({l,r,val});
    return;
}
void add(int l,int r,int val){
    auto itr=split(r+1),itl=split(l);
    for(;itl!=itr;itl++) itl->val+=val;
    return;
}
ll kth(int l,int r,int k){
    auto itr=split(r+1),itl=split(l);
    vector<pair<ll,int>> b;
    for(;itl!=itr;itl++)
        b.push_back({itl->val,itl->r-itl->l+1});
    sort(b.begin(),b.end());
    for(auto t:b){
        k-=t.second;
        if(k<=0) return t.first;
    }
    return 1145141919810LL;
}
ll sum(int l,int r,int x,int mod){
    ll res=0;
    auto itr=split(r+1),itl=split(l);
    for(;itl!=itr;itl++)
        res=(res+(qpow(itl->val,x,mod)*(itl->r-itl->l+1))%mod)%mod;
    return res;
}
const int N=1e5+10;
int n,m,seed,vmax,a[N],op,x,y,l,r;
int rnd(){
    int ret=seed;
    seed=(seed*7ll+13)%1000000007;
    return ret;
}
int main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n>>m>>seed>>vmax;
    for(int i=1;i<=n;i++){
        a[i]=(rnd()%vmax)+1;
        odt.insert({i,i,a[i]});
    }
    for(int i=1;i<=m;i++){
        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) cout<<kth(l,r,x)<<'\n';
        else cout<<sum(l,r,x,y)<<'\n';
    }
    return 0;
}

习题

复杂度相关

我们可以从模板题中发现,这玩意除区间赋值外都需要暴力,所以是一种暴力数据结构。对于模板题,均摊时间复杂度 \(O(m\log\log n)\)(关于这个题的复杂度说法很多,具体是不是这个我也不是很清楚,原题严谨的复杂度分析极其复杂,这里不展开分析)。其时间复杂度保证完全依赖于 assign 操作,所以仅适用于:

  1. 数据随机生成的题,如序列
  2. 区间查询后立即在查询区间 assign 的,这样通过势能分析可得复杂度为单 \(\log\),如数列分块入门 8
  3. 其他可以通过复杂度分析证明复杂度正确的。

参考资料

https://oi-wiki.org/misc/odt/

posted @ 2025-05-22 20:23  headless_piston  阅读(78)  评论(1)    收藏  举报