C. Willem, Chtholly and Seniorious(珂朵莉树教程)
C. Willem, Chtholly and Seniorious
— Willem...
— What's the matter?
— It seems that there's something wrong with Seniorious...
— I'll have a look...

Seniorious is made by linking special talismans in particular order.
After over 500 years, the carillon is now in bad condition, so Willem decides to examine it thoroughly.
Seniorious has $n$ pieces of talisman. Willem puts them in a line, the $i$-th of which is an integer $a_i$.
In order to maintain it, Willem needs to perform $m$ operations.
There are four types of operations:
- $1$ $l$ $r$ $x$: For each $i$ such that $l \leq i \leq r$, assign $a_i + x$ to $a_i$.
- $2$ $l$ $r$ $x$: For each $i$ such that $l \leq i \leq r$, assign $x$ to $a_i$.
- $3$ $l$ $r$ $x$: Print the $x$-th smallest number in the index range $[l, r]$, i.e. the element at the $x$-th position if all the elements $a_i$ such that $l \leq i \leq r$ are taken and sorted into an array of non-decreasing integers. It's guaranteed that $1 \leq x \leq r - l + 1$.
- $4$ $l$ $r$ $x$ $y$: Print the sum of the $x$-th power of $a_i$ such that $l \leq i \leq r$, modulo $y$, i.e. $\left(\sum_{i=l}^{r}{a_i^x}\right) \bmod y$.
Input
The only line contains four integers $n$, $m$, $seed$, $v_{max}$ $(1 \leq n, m \leq 10^5, 0 \leq seed < 10^9 + 7, 1 \leq v_{max} \leq 10^9)$.
The initial values and operations are generated using following pseudo code:
def rnd():
ret = seed
seed = (seed * 7 + 13) mod 1000000007
return ret
for i = 1 to n:
a[i] = (rnd() mod vmax) + 1
for i = 1 to m:
op = (rnd() mod 4) + 1
l = (rnd() mod n) + 1
r = (rnd() mod n) + 1
if (l > r):
swap(l, r)
if (op == 3):
x = (rnd() mod (r - l + 1)) + 1
else:
x = (rnd() mod vmax) + 1
if (op == 4):
y = (rnd() mod vmax) + 1
Here $op$ is the type of the operation mentioned in the legend.
Output
For each operation of types $3$ or $4$, output a line containing the answer.
Examples
Input
10 10 7 9
Output
2
1
0
3
Input
10 10 9 9
Output
1
1
3
3
Note
In the first example, the initial array is $\{8, 9, 7, 2, 3, 1, 5, 6, 4, 8\}$.
The operations are:
- $2 \ 6 \ 7 \ 9$
- $1 \ 3 \ 10 \ 8$
- $4 \ 4 \ 6 \ 2 \ 4$
- $1 \ 4 \ 5 \ 8$
- $2 \ 1 \ 7 \ 1$
- $4 \ 7 \ 9 \ 4 \ 4$
- $1 \ 2 \ 7 \ 9$
- $4 \ 5 \ 8 \ 1 \ 1$
- $2 \ 5 \ 7 \ 5$
- $4 \ 3 \ 10 \ 8 \ 5$
解题思路
万恶之源。其实刚接触算竞时我就听说过珂朵莉树了,但平时做题 9 道里面有 10 道都遇不到(特有的 9 比 10 大),所以就一直偷懒没学。结果上个礼拜日打 2026 SZUCPC 第一次碰见珂朵莉树的题,卡了半天嗯是想不出怎么做。赛后一看题解发现居然要用珂朵莉树,瞬间释怀地笑了。已严肃学习珂朵莉树.jpg。
珂朵莉树又称 ODT (Old Driver Tree),单纯是因为提出该方法的人的 Codeforces ID 就叫 ODT。实际上珂朵莉树根本不是图论里的树,也不是数据结构,更与珂朵莉无关,其本质上就是一种类似分块的暴力思想。正因如此,要学会珂朵莉树只需了解 std::set 或 std::map 的用法即可,并且实现也很简单(但思路很难想到)。
珂朵莉树的核心思想是将值相同的连续区间合并为单个节点进行维护。例如序列 $[1,1,1,3,3,1,2,2]$,在珂朵莉树中会被压缩为 $4$ 个 $(l, r, v)$ 的三元组节点:$(1,3,1)$、$(4,5,3)$、$(6,6,1)$、$(7,8,2)$。相较于线段树等传统数据结构,珂朵莉树在处理区间操作(尤其是区间赋值)时更加简洁。当对某段区间进行整体赋值时,该区间内的所有节点会被合并为一个新节点,节点数量大幅减少,因此其非常适合处理大量区间赋值的题目。不过珂朵莉树本质上仍是暴力优化,如果题目还涉及除区间赋值外的其他区间修改或查询操作,除非数据是随机的,否则很容易被卡到超时,所以能用珂朵莉树的场景其实十分受限。
大部分教程通常使用 std::set 来实现珂朵莉树,这里将介绍一种更为简洁的 std::map 写法。由于珂朵莉树维护的区间互不相交且依次相邻,因此我们可以只记录每个区间的左端点,并对区间按左端点排序,并通过下一个区间的左端点减 $1$ 来得到当前区间的右端点。对此我们可以开一个 std::map<int, int> odt,其中键为区间左端点,值为该区间的值。为了防止在查询下一个区间时出现越界,通常会预先插入一个哨兵键 $n+1$(实际数据范围右端点的后一个位置),其值可以任意设定,即 odt[n + 1] = 0。下面讲解珂朵莉树的两个核心操作。
split 操作,其作用是接受一个位置 $x$,把 $x$ 原本所在的区间 $[l,r] \left(1 \leq l \leq x \leq r \leq n\right)$ 拆分成 $[l,x)$ 和 $[x,r]$ 两个区间,核心目的是保证存在一个区间的左端点恰好是 $x$,方便后续对以 $x$ 为边界的区间进行操作。为了找到包含 $x$ 的区间,我们可以先找到严格大于 $x$ 的左端点对应的迭代器,那么其前一个区间就是包含 $x$ 的区间,随后只需将该区间的值赋给键 $x$ 即可完成分裂。split 操作对应的代码如下:
void split(int x) {
odt[x] = prev(odt.upper_bound(x))->second;
}
assign 操作,其作用是将给定区间 $[l,r]$ 赋值为 $c$。为了完整截取原本被 $[l,r]$ 包含的区间,我们可以依次调用 split(l) 与 split(r+1)。此时,从键 $l$ 到键 $r+1$ 的左闭右开迭代器范围内恰好对应 $[l,r]$ 所覆盖的所有区间。只需将这些区间全部删去,再插入合并后的新区间 odt[l] = c,即可完成赋值与合并。assign 操作对应的代码如下:
void assign(int l, int r, int c) {
split(l), split(r + 1);
odt.erase(odt.find(l), odt.find(r + 1));
odt[l] = c;
}
除了上述两个核心操作外,其他区间操作(如区间加、求区间最大值、求区间和、求区间第 $k$ 小等)只能先用 split 把目标区间拆分出来,然后依次遍历对应范围内的区间进行操作。例如本题还需要额外实现区间加、查询区间第 $k$ 小以及求区间每个值的 $k$ 次幂的和,这些都需要在拆分后遍历对应范围内的所有区间逐一处理。实现代码如下:
void add(int l, int r, int c) {
split(l), split(r + 1);
for (auto it = odt.find(l); it->first <= r; it++) {
it->second += c;
}
}
LL get_k(int l, int r, int k) {
split(l), split(r + 1);
vector<array<LL, 2>> p;
for (auto it = odt.find(l); it->first <= r; it++) {
p.push_back({it->second, next(it)->first - it->first});
}
sort(p.begin(), p.end());
for (auto &[x, c] : p) {
if (k <= c) return x;
k -= c;
}
}
int get_sum(int l, int r, int k, int mod) {
split(l), split(r + 1);
int ret = 0;
for (auto it = odt.find(l); it->first <= r; it++) {
ret = (ret + LL(next(it)->first - it->first) * qmi(it->second % mod, k, mod)) % mod;
}
return ret;
}
最后是复杂度分析。考虑只有区间赋值的情况,假设初始有 $n$ 个区间,共进行 $m$ 次区间赋值。一次 assign 操作会调用两次 split,最多增加两个区间;随后合并区间时会删除被覆盖的区间并插入一个新区间,由于 $[l,r]$ 非空,至少会删除一个区间,因此一次赋值操作净增的区间数至多为 $2$。由此可知整个过程中存在的区间总数不超过 $n + 2m$。由于每个区间只会被删除一次,所有 split 与 assign 操作涉及的插入和删除的次数是 $O(n + m)$ 级别的,而每次 std::map 操作的代价为 $O(\log n)$,故总复杂度为 $O((n+m)\log n)$。
如果题目还包含其他区间修改或查询操作,正如上文所述,我们需要枚举操作范围内相应的区间进行遍历。此时如果数据不是随机的,很容易卡到 $O(nm)$ 甚至更高,只需要确保珂朵莉树中的区间足够多,并进行多次遍历。反之,如果数据是完全随机的,可以证明总复杂度为 $O(m \log{\log{n}})$,具体可参考 hqztrue 的珂朵莉树的复杂度分析。
AC 代码如下,时间复杂度为 $O(m \log{\log{n}})$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int n, m, seed, vmax;
map<int, LL> odt;
int rnd() {
int ret = seed;
seed = (seed * 7ll + 13) % 1000000007;
return ret;
}
void split(int x) {
odt[x] = prev(odt.upper_bound(x))->second;
}
void assign(int l, int r, int c) {
split(l), split(r + 1);
odt.erase(odt.find(l), odt.find(r + 1));
odt[l] = c;
}
void add(int l, int r, int c) {
split(l), split(r + 1);
for (auto it = odt.find(l); it->first <= r; it++) {
it->second += c;
}
}
LL get_k(int l, int r, int k) {
split(l), split(r + 1);
vector<array<LL, 2>> p;
for (auto it = odt.find(l); it->first <= r; it++) {
p.push_back({it->second, next(it)->first - it->first});
}
sort(p.begin(), p.end());
for (auto &[x, c] : p) {
if (k <= c) return x;
k -= c;
}
}
int qmi(int a, int k, int mod) {
int ret = 1 % mod;
while (k) {
if (k & 1) ret = 1ll * ret * a % mod;
a = 1ll * a * a % mod;
k >>= 1;
}
return ret;
}
int get_sum(int l, int r, int k, int mod) {
split(l), split(r + 1);
int ret = 0;
for (auto it = odt.find(l); it->first <= r; it++) {
ret = (ret + LL(next(it)->first - it->first) * qmi(it->second % mod, k, mod)) % mod;
}
return ret;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> seed >> vmax;
odt = {{n + 1, 0}};
for (int i = 1; i <= n; i++) {
odt[i] = (rnd() % vmax) + 1;
}
while (m--) {
int op = (rnd() % 4) + 1;
int l = (rnd() % n) + 1;
int r = (rnd() % n) + 1;
if (l > r) swap(l, r);
int x = rnd() % (op == 3 ? r - l + 1 : vmax) + 1;
if (op == 1) add(l, r, x);
else if (op == 2) assign(l, r, x);
else if (op == 3) cout << get_k(l, r, x) << '\n';
else cout << get_sum(l, r, x, rnd() % vmax + 1) << '\n';
}
return 0;
}
参考资料
Codeforces Round #449 Editorial - Codeforces:https://codeforces.com/blog/entry/56135
珂朵莉树/颜色段均摊 - OI Wiki:https://oi-wiki.org/misc/odt/
珂朵莉树的复杂度分析:https://zhuanlan.zhihu.com/p/102786071
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/19879911

浙公网安备 33010602011771号