Fork me on GitHub

珂朵莉学习笔记

珂朵莉是什么?

以下内容来自 \(oi-wiki\)

珂朵莉树(Chtholly Tree),又名老司机树

ODT(Old Driver Tree)。起源自 CF896C

这个名称指代的是一种「使用平衡树(std::setstd::map 等)或链表(std::list、手写链表等)维护颜色段均摊」的技巧,而不是一种特定的数据结构。其核心思想是将值相同的一段区间合并成一个结点处理。相较于传统的线段树等数据结构,对于含有区间覆盖的操作的问题,珂朵莉树可以更加方便地维护每个被覆盖区间的值。

珂朵莉虽然是一种优化,但其本质是一种均摊时间复杂度的思想,是比较看脸看人品的看数据随机不随机的,所以,如果不是实在不行了,想冲一把,请勿打 \(odt\) !

有多少种珂朵莉

有用 \(set\),\(map\) 维护的珂朵莉,也有用手打链表来维护的珂朵莉,这里三个都会提及,但是着重写用 \(set\) 维护的。

珂朵莉:set

珂朵莉的操作有哪些?

一.构建

struct node 
{
    int l, r;
    mutable int v;
    node(const int &il, const int &ir, const int &iv) : l(il), r(ir), v(iv) {}
    bool operator<(const node &o) const 
    {
        return l < o.l; 
    }
};

这是珂朵莉树一个叶子的结构,整颗珂朵莉用 \(set<node>\) 来维护,当你要初始化时,在 \(set\) 中插入 \([1, n +1]\) 即可(或者是任意一个极长区间也可以)。

二.split

\(split\) 可是珂朵莉的核心,但是这个核心真的没什么不好理解的地方....

这个函数就是可以将一个区间 \([l, r]\) 划分成 \([l,x)\)\([x, r]\)

代码如下:

auto split(int x) 
{
    auto it = odt.lower_bound(Node_t(x, 0, 0));
    if (it != odt.end() && it -> l == x) return it;
    it -- ;
    auto l = it -> l;
    auto r = it -> r;
    auto v = it -> v;
    odt.erase(it);
    odt.insert(Node_t(l, x - 1, v));
    return odt.insert(Node_t(x, r, v)).first;
}

三.assign

另外一个重要的操作:assign。用于对一段区间进行赋值。设将要对区间 \([l,r]\) 赋值为 \(v\)

首先,将区间 \([l, r]\) 截取出来。依次调用 split(r + 1), split(l),将此两者返回的迭代器记作 \(itr, itl\),那么 \([itl, itr)\) 这个迭代器范围就指向了珂朵莉树中 \([l,r]\) 包含的所有区间。

然后,将原有的信息删除。std::set 有成员方法 erase,签名如同 iterator erase( const_iterator first, const_iterator last );,可以移除范围 [first; last) 中的元素。于是我们调用 odt.erase(itl, itr); 以删除原有的信息。

最后,插入区间 \([l,r]\) 的新值。调用 odt.insert(Node_t(l, r, v)) 即可。

下面是代码:

void assign(int l, int r, int v) 
{
    auto itr = split(r + 1);
    auto itl = split(l);
    odt.erase(itl, itr);
    odt.insert(node(l, r, v));
}

四.perform

就是将珂朵莉树上的一段区间提取出来并进行操作,其实只要把删除区间改为遍历区间即可。

代码和上面一样啦。

void assign(int l, int r, int v) 
{
    auto itr = split(r + 1);
    auto itl = split(l);
    odt.erase(itl, itr);
    odt.insert(node(l, r, v));
}

珂朵莉:map

差异

相较于 \(std::set\) 的实现,\(std::map\) 的实现的 \(split\) 操作写法更简单。除此之外,其余操作与 \(std::set\) 并无二异。

由于珂朵莉树存储的区间是连续的,我们不一定要记下右端点是什么。不妨使用一个 map<int, int> mp; 存储所有区间,其键维护左端点,其值维护其对应的左端点到下一个左端点之前的值。

初始化时,如题目要求维护位置 \(1\)\(n\) 的信息,则调用 \(mp[1] = -1, mp[n + 1] = -1\) 表示将 \([1, n + 1)\)\([1, n]\) 都设为特殊值 \(-1\) (这里都是整数哈),\([n+1, +\infty)\) 这个区间当作哨兵使用,也可以对它进行初始化。

剩下的就不细说了哈,详见代码:

//F1:
void split(int x) 
{
    auto it = prev(mp.upper_bound(x)); 
    mp[x] = it->second; 
}
//F2:
auto split(int pos)
{
    auto it = prev(mp.upper_bound(pos)); 
    return mp.insert(it, make_pair(pos, it -> second));
}

void assign(int l, int r, int v) 
{
    split(l);
    split(r);
    auto it = mp.find(l);
    while(it -> first != r) it = mp.erase(it);
    mp[l] = v;
}

void perform(int l, int r)
{ 
    split(l);
    split(r);
    auto it = mp.find(l);
    while(it -> first != r) it = next(it);
}

珂朵莉:链表

太难写了,不写了,详见 oi-wiki, 反正我打死也不会写这个

题单

\(QwQ\)

posted @ 2025-06-04 15:16  tony0530  阅读(46)  评论(0)    收藏  举报