[算法]珂朵莉树

· · 个人记录

何为珂朵莉树

珂朵莉树,又名老司机树(\text{ODT}),是由毒瘤 lxl 发明的一种暴力数据结构。

\text{ODT} 的前置知识

需掌握 set 的用法。

\text{ODT} 题目

\text{ODT} 的祖宗 CF896C 为例,来进行以下讲解。

基本操作

注意:以下代码在 c++14 下可以编译成功。

1. 定义

```cpp struct Node{ int l, r; mutable int val; Node(int L, int R = -1, int v = 0){ l = L; r = R; val = v; } bool operator<(const Node &x) const{ return l < x.l; } }; ``` ~~很迷吧!~~ 首先解释 `mutable` 是什么意思,因为 `set` 的特质,要修改一个元素必须取出,修改,删除,插入,十分复杂,而 `mutable` 可以使得任何元素(包括 `const`)都能任意修改。 然后就是初始化了,其中 `R = -1, v = 0` 表示如果没有参数就自动设为 $-1$ 和 $0$,这里也没有什么好讲的。 接下来就是重载运算符,因为 `set` 自动排序,不能删。 ### 2. 分裂区间($\text{split}$) 首先这个函数只有一个参数,根据这个参数找到所在区间 $[l, r]$,然后分裂成 $[l, pos - 1]$ 和 $[pos, r]$,你要说为什么不像线段树那样分裂成 $[l, pos]$ 和 $[pos + 1, r]$,那我也不知道辣! 为什么需要这个操作呢,因为在别的操作的时候,可能会有的节点的左端点跳过操作的左端点,但右端点还在里面,反正需要就对了。 就比如说这样: ![](https://i.imgtg.com/2022/08/27/Zzv6c.png) 蓝色代表存在 `set` 里的节点,这些节点代表一个区间,里面的值都相同。而蓝色区间表示查询区间,从这张图我们就可以看出,需要将第二个节点和第四个节点分裂,才能操作完成。 好了,废话不多说,直接上代码: ```cpp auto split(int pos){ //返回的是分裂完之后的迭代器 auto it = odt.lower_bound(Node(pos)); if(it != odt.end() && it -> l == pos){ //如果的区间左端点就不需要分裂 return it; } it--; //这里因为使用 lower_bound,所以要-1才能是包含 pos 的区间 int l = it -> l, r = it -> r, val = it -> val; odt.erase(it); //删除这段区间,分成两段区间 odt.insert(Node(l, pos - 1, val)); return odt.insert(Node(pos, r, val)).first; //这里因为区间起始点是 pos,所以需要返回 } ``` 至于一些问题,要等到下面才能解释清。 ### 3. 区间推平($\text{assign}$) 首先,我们应该解决上面那个问题。 有的人显然就会说: ```cpp auto itl = split(l), itr = split(r + 1) //这里为什么是 r + 1, 因为返回的迭代器正好是包含 r 的迭代器多 1,后面好处理 ``` 恭喜你,成功的 $\color{purple}\text{RE}$ 了。 首先,我们考虑为何会 $\color{purple}\text{RE}$。 因为你把 `l` $\text{split}$ 后,没有什么问题,但是如果 $\text{split}$ `r + 1` 的时候,就会出大问题,因为我们已经把区间给删掉了,这样就会导致有些情况会出大问题,~~听不懂没关系,反正我也听不懂~~。 那么代码就好写了: ```cpp void assign(int l, int r, int val){ auto itr = split(r + 1), itl = split(l); odt.erase(itl, itr); //这里删除是删除 [itl, itr - 1] odt.insert(Node(l, r, val)); //重新插入就行了 } ``` 没错,就是这么简单。 ### 4. 修改操作($\text{update}$) 跟推平操作一样,还是: ```cpp auto itr = split(r + 1), itl = split(l); ``` 然后针对 $[itl, itr - 1]$ 这个区间内的修改的就行了。 模板: ```cpp void update(int l, int r, int val){ auto itr = split(r + 1), itl = split(l); for(auto i = itl; i != itr; i++){ //这里 != itr 就是 <,但由于迭代器特性,所以不能用 < //more } } ``` 由于是区间加,显然就是这样: ```cpp void update(int l, int r, int val){ auto itr = split(r + 1), itl = split(l); for(auto i = itl; i != itr; i++){ i -> val += val; //这里的 -> 就相当于结构体的.,只不过是指针用法 } } ``` ### 5. 其他神奇操作 区间排名,直接全部扔进维克托($\text{vector}$)里,排个序再神奇乱搞就可以了: ```cpp int rnk(int l, int r, int x){ vector< pair<int, int> > num; init for(auto i = itl; i != itr; i++){ num.push_back(make_pair(i -> val, (i -> r) - (i -> l) + 1)); } sort(num.begin(), num.end()); for(auto i = num.begin(); i != num.end(); i++){ //这就是用 auto 的好处 x -= i -> second; if(x <= 0){ return i -> first; } } return -1; //如果没有返回 -1 } ``` 区间幂次,直接暴力就行了: ```cpp int query(int l, int r, int x, int p){ auto itr = split(r + 1), itl = split(l); int ans = 0; for(auto i = itl; i != itr; i++){ ans = (ans + fastpow(i -> val, x, p) * ((i -> r) - (i -> l) + 1)) % p; } return ans; } ``` 海油各种神奇操作,比如说最大子段和,最长上升/下降/不上升/不下降子序列,还有更多。 ## 代码 CF896C 代码: ```cpp #include<bits/stdc++.h> #define init auto itr = split(r + 1), itl = split(l); #define int long long //话说今年一金牌选手 Qiu**(为保护当事人隐私,已打码),没开 long long,无缘全国前五...... using namespace std; const int mod = 1e9 + 7; const int N = 1e5 + 5; int n, m, seed, vmax; int a[N]; int rnd(){ int ret = seed; seed = (seed * 7 + 13) % mod; return ret; } int fastpow(int a, int b, int p){ int ans = 1; while(b){ if(b & 1){ ans = a % p * ans % p % p; } b >>= 1; a = a % p * a % p % p; } return ans; } struct Node{ int l, r; mutable int val; Node(int L, int R = -1, int v = 0){ l = L; r = R; val = v; } bool operator<(const Node &x) const{ return l < x.l; } }; set<Node> odt; auto split(int pos){ auto it = odt.lower_bound(Node(pos)); if(it != odt.end() && it -> l == pos){ return it; } it--; int l = it -> l, r = it -> r, val = it -> val; odt.erase(it); odt.insert(Node(l, pos - 1, val)); return odt.insert(Node(pos, r, val)).first; } void assign(int l, int r, int val){ init odt.erase(itl, itr); odt.insert(Node(l, r, val)); } void update(int l, int r, int val){ init for(auto i = itl; i != itr; i++){ i -> val += val; } } int rnk(int l, int r, int x){ vector< pair<int, int> > num; init for(auto i = itl; i != itr; i++){ num.push_back(make_pair(i -> val, (i -> r) - (i -> l) + 1)); } sort(num.begin(), num.end()); for(auto i = num.begin(); i != num.end(); i++){ x -= i -> second; if(x <= 0){ return i -> first; } } return -1; } int query(int l, int r, int x, int p){ init int ans = 0; for(auto i = itl; i != itr; i++){ ans = (ans + fastpow(i -> val, x, p) * ((i -> r) - (i -> l) + 1)) % p; } return ans; } signed main(){ cin >> n >> m >> seed >> vmax; for(int i = 1; i <= n; i++){ a[i] = rnd() % vmax + 1; odt.insert(Node(i, i, a[i])); } while(m--){ int op = rnd() % 4 + 1, l = rnd() % n + 1, r = rnd() % n + 1, x, y; 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){ update(l, r, x); } else if(op == 2){ assign(l, r, x); } else if(op == 3){ cout << rnk(l, r, x) << '\n'; } else{ cout << query(l, r, x, y) << '\n'; } } return 0; } ``` ## 时间复杂度分析 由于期望只有 $\log n$ 个节点,又因为排序需要时间,所以**期望**时间复杂度为 $O(n \log n \log n)$,出题人卡 $\text{ODT}$ 很简单,不给区间赋值就卡了。 ## $\text{ODT}$ 题目 题单:[](https://www.luogu.com.cn/training/210300)这里有东西哦。 题目:[这里](https://www.luogu.com.cn/problem/U239135)