珂朵莉树

oi.ds.odt
upd.2025.7.15

珂朵莉树 (\(OldDriverTree\))

末日三问:

  • 什么是珂朵莉树
  • 珂朵莉树可以干什么
  • 怎么写珂朵莉树

为什么叫珂朵莉树?

名字出处来源于Codeforces的一场比赛,因为题面和《末日三问》(《末日时在干什么?有没有空?可以来拯救吗?》)中的女主(其实并非,小说里上来就死了)珂朵莉相关,因此这道题的方法被称作珂朵莉树

珂朵莉树可以干什么?

经常用于处理带有“推平”(将某一段全部修改为一个数)类的询问

怎么写珂朵莉树?

瞎写

先来看一下模板题:

请你写一种奇怪的数据结构,支持:

\(1\) \(l\) \(r\) \(x\) :将\([l,r]\) 区间所有数加上\(x\)
\(2\) \(l\) \(r\) \(x\) :将\([l,r]\) 区间所有数改成\(x\)
\(3\) \(l\) \(r\) \(x\) :输出将\([l,r]\) 区间从小到大排序后的第\(x\)个数是的多少(即区间第\(x\)小,数字大小相同算多次,保证 \(1≤ x ≤ r−l+1\) )
\(4\) \(l\) \(r\) \(x\) \(y\) :输出\([l,r]\) 区间每个数字的\(x\) 次方的和模\(y\) 的值(即(\(\Sigma_{i=l}^ra_i^x\) \(mod\) \(y\)))

看到这道题的操作1和操作2,你第一个想到的肯定是线段树,但是这道题的操作3和操作4都不易用线段树维护,因此我们用到了珂朵莉树




珂朵莉树是以段为单位去考虑问题的,我们先来想这些数据怎么成为“段”,同一“段”内的数据肯定意味着这些数据有某些共同点

由于操作2,不难发现这道题中数据的共同点就是数值相同

我们先来看珂朵莉树是如何建的
/*node为一个数据段*/
struct node {
    int l, r; // 表示线段的左右端点
    mutable int v; // 表示这一段每个元素的数值(1)
    node(int l, int r = 0, int v = 0) :l(l),r(r),v(v){}
    bool operator < (const struct node &A) const
    {
        return l < A.l; // 按照左端点排序
    }
};
set\ s; // s就是一个珂朵莉树
(1):我们看到在定义v的时候使用了关键字

mutable
这个关键字可以确保v可以被修改,即使它在有些时候被识别成了常量,比如当你用set< node >::iterator去访问v的时候它依旧可以重新赋值


接下来我们去维护这个珂朵莉树
介绍维护操作中最重要 的操作:分割段

当我们想要使用操作1或操作2去修改数据时,就会出现需要把一段数据断开进行操作的时候,因此我们需要分割

//将数据段[l,r]分割为两端[l,pos-1],[pos,r];并且返回[pos,r]
set::iterator split(int pos)
{
    // 我们先查找pos这个数据在哪一段
    // 定义时的重载 < 运算就是为了这里
    // lower_bound是返回第一个l大于等于pos的段落,it或it前一段就是包含pos的段
    auto it = s.lower_bound(node(pos)); 
    // 假如说pos是这一段的开头第一个数据,那就不用分割了
    if (it != s.end() && it->l == pos)
        return it;
    // 取it前一个数据
    it --;
    // 假如pos是这一段的结尾也不用分割,返回空指针
    if (pos > it->r) return s.end();
    int l = it->l, r = it->r, v = it->v;
    // 先删再加回来
    s.erase(it);
    s.insert(node(l,pos-1,v));
    // insert的返回值是pair,first代表插入的数据
    return s.insert(node(pos,r,v)).first;
}

接下来介绍操作: 推平

void assign(int l, int r, int v) 
{
    // 注意:一定是要先split(r+1),否则会有概率RE
    // 这行代码也是常用的操作,用于获得l开头的段落和r结尾的段落
    set::iterator itr = split(r + 1), itl = split(l);
    // 把l到r删掉
    s.erase(itl, itr);
    // 再加入推平后的段落
    s.insert(node(l, r, v));
}

其实珂朵莉树就已经维护好了,使用珂朵莉树其实很简单,就是暴力拆段做,接下来拿操作4举例

//快速幂
int qpow(int a, int b, int p)
{
    int res = 1;
    a = a%p;
    while (b)
    {
        if (b&1) res = res * a % p;
        b >>= 1;
        a = a * a % p;
    }
    return res % p;
}
int querypow(int l, int r, int x, int y)
{
    //拆段
    auto itr = split(r + 1), itl = split(l);
    int s = 0;
    for (auto it = itl; it != itr; it ++ )
        // it这一段的贡献为 长度*每个数据的值
        s += qpow(it->v,x,y)*(it->r - it->l + 1) % y, s %= y;
    return s % y;
}

可见珂朵莉树其实并不是一棵树(也许是set帮你写好了),就是一种更为优雅的暴力,因此也被称为老司机树(Old Driver Tree)

中国珂学院链接:https://wiki.sukasuka.cn/首页

\(End.\)

posted @ 2025-07-15 00:11  ZzhAllen  阅读(80)  评论(0)    收藏  举报