某科学的珂朵莉树

· · 算法·理论

更好的阅读体验

update
时间 修改
2025.8.15 改正函数返回值错误

珂朵莉树概述

百度百科&bilibili

珂朵莉树又称老司机树(Chtholly Tree / Old Driver Tree)暴力数据结构,特别适用于处理区间赋值、区间修改等操作。该结构在随机数据下表现优异,尤其在处理大量区间操作时具有显著优势。

该数据结构最早在 Codeforces Round #394 (Div. 1) 的 C 题 CF-896C 中被提出,并因其题面中角色“珂朵莉”的名字而得名。在实际应用中,珂朵莉树常用于替代线段树或树状数组,在某些特定场景下实现更简洁、高效的代码逻辑。

前置知识

STL-set

简介

set 是 C++ STL 提供的标准关联容器,其核心特性包括元素唯一性和自动排序。底层基于红黑树(自平衡二叉搜索树)实现,这使得插入、删除和查找操作的时间复杂度均为 \Theta(\log n),能高效处理大规模数据。

迭代器 —— set <T>::iterator

set 的迭代器是 const_iterator,不允许通过迭代器修改元素值(排除使用 mutable)。通过 begin()end() 可获取正向迭代器(指向首元素和尾后位置),rbegin()rend() 获取反向迭代器(指向尾元素和首前位置)。迭代器在插入 / 删除操作后不会失效(红黑树仅调整指针,不移动内存)。 迭代器使用方式与普通指针相同。迭代器支持两种操作为 const_iterator++const_iterator--,分别指向前驱和后继。

查找 —— find()

find(value) 函数用于查找元素 value,返回指向该元素的迭代器;若不存在,返回 end()。时间复杂度为 \Theta(\log n)。 示例:

set<int> s = {1, 3, 5}; 
auto it = s.find(3);  // it 指向元素 3 
if (it != s.end()) cout << *it;  // 输出 3 

插入 —— insert()

insert(value) 函数用于向 set 中插入元素 value,返回一个 pair<iterator, bool> 类型:

set<int> s; 
// 插入新元素,返回 {迭代器, true} 
auto res1 = s.insert(3); 
if (res1.second) cout << "插入成功: " << *res1.first; // 输出 3 

// 插入重复元素,返回 {原元素迭代器, false} 
auto res2 = s.insert(3); 
if (!res2.second) cout << "元素已存在,当前值: " << *res2.first; // 输出 3 

删除 —— erase()

set 提供三种删除方式:

  1. 按值删除:erase(value),删除所有值为 value 的元素(set 中最多一个),返回删除的元素个数(0 或 1)。
  2. 按迭代器删除:erase(it),删除迭代器 it 指向的元素,无返回值。
  3. 区间删除:erase(first, last),删除 [first, last) 区间内的元素,无返回值。 时间复杂度:均为 \Theta(\log n)(红黑树节点删除仅需指针调整)。 示例:
set<int> s = {2, 5, 8}; 
s.erase(5);  // 删除值为 5 的元素,s 变为 {2, 8} 
auto it = s.find(2); 
s.erase(it);  // 删除迭代器指向的元素,s 变为 {8} 

规定大小查找 —— lower_bound()

lower_bound(value) 返回指向第一个大于等于 value 的元素的迭代器;若所有元素均小于 value,返回 end()。时间复杂度为 \Theta(\log n),常用于二分查找或区间划分。 示例:

set<int> s = {1, 4, 6, 9}; 
auto it = s.lower_bound(5);  // 第一个 >=5 的元素是 6,it 指向 6 
cout << *it;  // 输出 6 

正文

原理

珂朵莉树的核心思想是通过维护一个有序的区间集合(存储在 set 中),将整个数组划分为若干个值相同且连续的区间。每个区间用一个节点表示,包含左端点 l、右端点 r 和区间值 val。当需要对某个区间进行操作时,通过分裂(split) 操作将覆盖该区间的节点拆分成精确匹配的子区间,再进行修改或查询,最后通过合并(merge) 操作将相邻的相同值区间合并,以减少节点数量。 为何节省时间?

  1. 区间合并特性:在大量区间赋值操作下,数组会被划分成少量连续区间,使每次操作只需处理少数节点(而非逐个元素)。
  2. 随机数据优化:在随机数据中,区间操作后节点数量增长缓慢,单次操作的时间复杂度接近 \Theta(\log n)
  3. 简洁的实现:相比线段树,珂朵莉树代码量少,逻辑直观,减少了实现错误的可能性。

期望时间复杂度证明

珂朵莉树的效率依赖于区间赋值操作和随机数据分布。假设初始有 n 个元素,每次区间赋值操作会将某区间内的节点合并为 1 个节点。在随机数据下,每次分裂操作产生的新节点数量有限,且合并操作会不断减少节点总数。

节点存储信息 —— ODT_node

珂朵莉树的每一个节点都存储了一段元素相同的区间,并把每一个节点按左端点从小到大扔进有序集即 set 中来方便操作。

struct ODT_node {
    int l, r;  // 区间的左右端点
    mutable int val; // 区间的取值(由于可能修改所以使用 mutable)
    bool operator < (const ODT_node x) const {return l < x.l;}
    // 为使用是 set 需要重载运算符并按左端点排序
};

建树操作 —— void build(int a[], int n)

void build(int a[], int n) 表示建一棵表示 a 数组的树。具体为找出一段连续的值用一个节点表示。

void build(int a[], int n) {
    int x = a[1], pos = 1;
    // x 为值,pos 为一段连续的值的左端点
    for (int i = 2; i <= n; i++) {
        if (a[i] != x) {
            t.insert({pos,  i - 1, x});
            pos = i; x = a[i];
            // 找到不同的位置,插入节点
        }
    }
    t.insert({pos,  n, a[n]});
    // 插入剩下的元素
}

查找节点操作 —— set<ODT_node>::iterator find(int x)

set<ODT_node>::iterator find(int x) 表示查找包含 x 的节点,并返回指向该节点的指针。

set<ODT_node>::iterator find(int x) {
    auto it = t.lower_bound({x,  0, 0});
    // 找出要分裂的节点的下一个节点
    if (it == t.end()  || it->l > x) it--;
    // 找出要分裂的节点
    return it;
}

节点分裂操作 —— set<ODT_node>::iterator split(int x)

set<ODT_node>::iterator split(int x) 表示把一个包含 x 的节点按 x 分裂成两个节点并返回分裂后靠右节点的指针,如下:

flowchart LR
    A(l, r, val)  --> D[(split x)]
    D --> B(l, x - 1, val)
    D --> C(x, r, val)

具体过程为先二分得出需要分裂的节点,然后删除要分裂的节点,插入分裂后的两个节点。

set<ODT_node>::iterator split(int x) {
    auto it = t.find(x);
    // 找出要分裂的节点
    if (it->l == x) return it;
    // 如果要分裂的节点左端点就是 x,则不需要分裂
    int l = it->l, r = it->r, val = it->val;
    // 记录信息,防止之后的操作改变指针
    t.erase(it);    t.insert({l, x - 1, val});
    return t.insert({x, r, val}).first;
    // 删除要分裂的节点,插入分裂后的两个节点
}

区间合并操作 —— set<ODT_node>::iterator merge(set<ODT_node>::iterator it, bool dir)

set<ODT_node>::iterator merge(set<ODT_node>::iterator it, bool dir) 表示把 it 左右两边的与它相同的区间合并并返回合并后的指针,dir = 1 合并左边,反之,如图:

flowchart LR
    A(l1, r1, val) --> D[(merge it)]
    C(l2, r2, val) --> D
    D --> B(l1, r2, val)
set<ODT_node>::iterator merge(set<ODT_node>::iterator it, bool dir) {
    if (it == t.end())  return t.end();
    // 如果 it 指向的是最后一个节点,则不需要合并
    // 合并左边
    if (dir)  {
        if (it == t.begin())    return t.end();
        // 如果 it 指向的是第一个节点,则不需要合并
        auto prev_it = prev(it);
        // 左边的指针
        if (prev_it->val == it->val) {
            int l = prev_it->l, r = it->r, val = it->val;
            // 预先记录信息
            it = t.erase(prev_it);
            if (it != t.end())  t.erase(it);
            // 删除原节点
            return t.insert({l,  r, val}).first;
            // 添加合并后的节点
        }
        return it;
    }
    // 合并右边(同上)
    auto next_it = next(it);
    if (next_it == t.end()) return t.end();
    if (next_it->val == it->val) {
        int l = it->l, r = next_it->r, val = it->val;
        t.erase(it);
        if (next_it != t.end())  t.erase(next_it);
        return t.insert({l,  r, val}).first;
    }
    return it;
}

区间推平操作 —— void assign(int l, int r, int val)

void assign(int L, int R, int Val) 表示把 LR 区间内的数赋值为 Val,如下:

flowchart LR
    a(l, r, val)  --> D[(assign L, R, Val)]
    b(……)  --> D
    c(l, r, val)  --> D
    g(l, r, val)  --> D
    e(……)  --> D
    f(l, r, val)  --> D
    D --> h(l, r, val)
    D --> i(……)
    D --> j(l, L - 1, val)
    D --> k(L, R, Val)
    D --> l(R + 1, r, val)
    D --> m(……)
    D --> n(l, r)

具体过程为在 LR + 1 处分裂,删除中间的一段被赋值的区间的原节点,最后重新插入被赋值的区间。

void assign(int l, int r, int val) {
    auto ir = split(r + 1), il = split(l);
    // 分裂操作端点(必须先分裂 r 再分裂 l 不然迭代器会失效)
    t.erase(il, ir);
    // 删除中间的一段被赋值的区间的原节点
    t.insert({l, r, val});
    // 重新插入被赋值的区间
    it = merge(it, 1);  merge(it, 0);
    // 合并端点
}

其他的区间修改操作 —— void fun(int l, int r, ……)

珂朵莉树修改操作都可以按一下模板操作(类比分块)。

LR + 1 处分裂,修改中间的一段被操作的区间的节点。

void fun(int l, int r, ……) {
    auto ir = split(r + 1), il = split(l);
    // 分裂操作端点
    for (auto it = il; it != ir; it ++ )
        // 对中间的节点操作
    merge(find(l), 1);    merge(find(r), 0);
    // 合并端点
}

区间查询操作 —— int fun(int l, int r)

珂朵莉树查询操作都可以按一下模板操作(y 也类比分块)。

LR + 1 处分裂,计算中间的一段被查询的区间的贡献。

int fun(int l, int r) {
    auto ir = split(r + 1), il = split(l);
    // 分裂操作端点
    for (auto it = il; it != ir; it ++ )
        // 对中间的节点贡献
    merge(find(l), 1);  merge(find(r), 0);
    // 合并端点
}

total-code

struct ODT_node {
    int l, r;
    mutable int val;
    bool operator < (const ODT_node x) const {return l < x.l;}
};

struct ODT {
    set<ODT_node> t;
    void build(int a[], int n) {
        int x = a[1], pos = 1;
        for (int i = 2; i <= n; i++) {
            if (a[i] != x) {
                t.insert({pos,  i - 1, x});
                pos = i; x = a[i];
            }
        }
        t.insert({pos,  n, a[n]});
    }
    set<ODT_node>::iterator find(int x) {
        auto it = t.lower_bound({x,  0, 0});
        if (it == t.end()  || it->l > x) it--;
        return it;
    }
    set<ODT_node>::iterator split(int x) {
        auto it = find(x);
        if (it->l == x) return it;
        int l = it->l, r = it->r, val = it->val;
        t.erase(it);  t.insert({l,  x - 1, val});
        return t.insert({x,  r, val}).first;
    }
    set<ODT_node>::iterator merge(set<ODT_node>::iterator it, bool dir) {
        if (it == t.end())  return t.end();
        if (dir)  {
            if (it == t.begin())    return t.end();
            auto prev_it = prev(it);
            if (prev_it->val == it->val) {
                int l = prev_it->l, r = it->r, val = it->val;
                it = t.erase(prev_it);
                if (it != t.end())  t.erase(it);
                return t.insert({l,  r, val}).first;
            }
            return it;
        }
        auto next_it = next(it);
        if (next_it == t.end()) return t.end();
        if (next_it->val == it->val) {
            int l = it->l, r = next_it->r, val = it->val;
            t.erase(it);
            if (next_it != t.end())  t.erase(next_it);
            return t.insert({l,  r, val}).first;
        }
        return it;
    }
    void assign(int l, int r, int val) {
        auto ir = split(r + 1), il = split(l);
        t.erase(il,  ir);
        auto it = t.insert({l,  r, val}).first;
        it = merge(it, 1);    merge(it, 0);
    }
    void fun(int l, int r, ……) {
        auto ir = split(r + 1), il = split(l);
        for (auto it = il; it != ir; it ++ )
            // 对中间的节点操作
        merge(find(l), 1);  merge(find(r), 0);
    }
    int fun(int l, int r) {
        auto ir = split(r + 1), il = split(l);
        for (auto it = il; it != ir; it ++ )
            // 对中间的节点贡献
        merge(find(l), 1);  merge(find(r), 0);
    }
};

例题

上帝造题的七分钟 2 / 花神游历各国

Willem, Chtholly and Seniorious

序列