珂朵莉树(Chtholly Tree)渗析

版权声明:欢迎转载ヽ(•ω•ゞ)···如有表意不清或您没有看懂评论即可! https://blog.csdn.net/yandaoqiusheng/article/details/84992816

先给一道题
题目链接:传送门

#【题面】

请你写一种奇怪的数据结构,支持:
1 1 l r x l r x :将 [ l , r ] [l,r] 区间所有数加上 x x
2 2 l r x l r x :将 [ l , r ] [l,r] 区间所有数改成 x x
3 3 l r x l r x :输出将 [ l , r ] [l,r] 区间从小到大排序后的第 x x 个数是的多少(即区间第 x x 小,数字大小相同算多次,保证 1 x r l + 1 1\leq x \leq r-l+1 )
4 4 l r x y l r x y :输出 [ l , r ] [l,r] 区间每个数字的 x x 次方的和模 y y 的值(即 i = l r a i x m o d    y \sum^r_{i=l}a_i^x \mod y
【输入格式】 这道题目的输入格式比较特殊,需要选手通过seed自己生成输入数据。 输入一行四个整数 n , m , s e e d , v m a x 1 n , m 1 0 5 , 0 s e e d 1 0 9 + 7 , 1 v m a x 1 0 9 n,m,seed,v_{max}(1\leq n,m\leq 10^{5} ,0\leq seed \leq 10^{9}+7,1\leq v_{max} \leq 10^{9} ),其中 n n 表示数列长度, m m 表示操作次数,后面两个用于生成输入数据。 数据生成的伪代码如下

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

现在
疯狂脑补搞基数据结构
快快快
……
好了这篇文章是讲珂朵莉树的

I initially named it as “Old Driver Tree” ( Which is my codeforces ID ).
(But now I call it Chtholly Tree~).

这是Codeforce上作者的原话
也就是说原来是叫ODT的,也就是作者的ID
后来改名叫珂朵莉树
至于为什么就不管了
反正既然有这么一个东西了
我们就学一学
它的思想是很值得我们学习的
下面开始吧
(话说对指针和迭代器方面不了解的真的很难懂)

首先,它是用来干什么的呢?

This is an interesting algorithm which can easily deal with many data structure problems------if the data is random…

珂朵莉树就是运用一颗树对一个区间进行操作的数据结构
但是一个区间内的数必须是一样的
也就是使一段区间内的数变得一样
数据随机最好,不然会大大影响效率

可见它的局限性还是比较大的
但是代码简单好调
这就很让人喜欢了
以CF 896C 也就是上面那个题为例
区间加,区间赋值,区间 k k 小,区间幂次和

很符合我们的标准
主要是这道题的数据是随机的

初始化与定义

struct node {
	int l, r;
	mutable ll v; //mutable是“可变的”,加上它可以让我们在别的函数中修改v的值,官方写法是这样的,照着写上就可以了
	node (int L, int R = -1, ll V = 0) : l(L), r(R), v(V) {} //方便添加节点
	friend bool operator < (const node a, const node b) { //重载了小于号
		return a.l < b.l;
	}
};
set<node> s;

这样每定义的一个节点表示的是在 l r l-r 这个区间内的数都是 v v
珂朵莉树的许多操作用 s e t set 会显得十分简便
什么操作下面会讲
当然你不用 s e t set 手打一个 t r e a p treap s p l a y splay 也是可以的

下面先说说迭代器

要知道
迭代器是一种数据类型
是用来遍历容器内元素的数据类型
S T L STL 中为每一种容器都定义了一种迭代器
基本只有通过迭代器才能访问其中元素
只有 v e c t o r vector 可以通过下标来访问元素
也就是像数组一样访问

基本操作

split
iter split(int pos) {
	iter it = s.lower_bound(node(pos));
	if (it != s.end() and it->l == pos) return it;
	--it;
	int lt = it->l, lr = it->r;
	ll lv = it->v;
	s.erase(it);
	s.insert(node(lt, pos - 1, lv));
	return s.insert(node(pos, lr, lv)).first;
}

等等,那个 i t e r iter 是啥
我们在前面会写一句这个

#define iter set<node>::iterator

这就是一个set的迭代器。
s p l i t split 写过平衡树的都知道是啥
在这里插入图片描述
那这个操作放在珂朵莉树上干啥的呢?
它就是把当前 p o s pos 节点所在的区间分成左右两部分
[ l , p o s 1 ] [l,pos-1] [ p o s , r ] [pos,r]
(注意它返回的是 [ p o s , r ] [pos,r] 所在的迭代器)
珂朵莉树能做的就是对一个区间进行操作
所以要把修改的区间和不修改区间分开
这样才能进行操作
注意最后
s e t set i n s e r t insert 会返回一个 &lt; i t e r a t o r , b o o l &gt; &lt;iterator,bool&gt; p a i r pair 类型
我们只取 f i r s t first (当然取别的也没用)
也就是后半段区间的迭代器
再就是里面的指针
例如 i t &gt; l it-&gt;l
可以写成 ( i t ) . l (*it).l
都是获取 i t it 中名为 l l 的成员
都属于解引用(*)
再放一遍代码

iter split(int pos) {
	iter it = s.lower_bound(node(pos)); //找到第一个大于等于当前位置的set
	if (it != s.end() and it->l == pos) return it;
	//当前set的左端点就在pos上,就不用分了
	--it; //否则就在上一个位置,即左端点小于pos且最大的区间所在的迭代器
	int lt = it->l, lr = it->r; //标出要被分割的区间
	ll lv = it->v; //取出要赋的值
	s.erase(it); //删除原节点(区间)
	s.insert(node(lt, pos - 1, lv)); //插入前半段的新的区间
	return s.insert(node(pos, lr, lv)).first; //插入后半段的新的区间并返回地址
}
assign

珂朵莉树最精髓的地方在这里
它是靠这个来维持它的复杂度的
不知道什么意思?
在这里插入图片描述
这里意思差不多
我们要平摊区间
把这个区间设成一个值
用一个大区间代替原来的小区间

void assign(int l, int r, ll add = 0) { //默认为0,因为经常都摊平
	iter llc = split(l), rrc = split(r + 1); //取出要平摊区间的首位地址
	s.erase(llc, rrc); //然后删掉
	s.insert(node(l, r, add)); //插入新节点(区间)
}

这就保证了 s e t set 的数量
最终是在 l o g ( n ) log(n) 左右
为啥是到 r + 1 r+1 ??
因为我们的区间是左闭右开的

文章快写完的时候页面自动切成兼容模式了
然后就在这里保存了一下
下面的都没了
是重新写了一遍的
尽量不漏
虽然有点浮躁
(▼ヘ▼#)

区间赋值

下面的操作都是暴力了
从最左边的区间加到最右边的区间

void add(int l, int r, ll add = 1) {
	iter llc = split(l), rrc = split(r + 1);
	for (; llc != rrc; llc++) llc->v += add;
}

区间加的话找到每个节点加上值就好了

区间第 k k
ll kth(int l, int r, int k) {
	vector<pair<ll, int> > v;
	iter llc = split(l), rrc = split(r + 1);
	for (; llc != rrc; llc++) v.push_back(pair<ll, int>(llc->v, llc->r - llc->l + 1));
	sort(v.begin(), v.end());
	for (vector<pair<ll, int> >::iterator it = v.begin(); it != v.end(); it++) {
		k -= it->second;
		if (k <= 0) return it->first;
	}
	return -1LL;
}

k k 小也是一样
把每个元素放到 v e c t o r vector 里排序
然后暴力找第 k k 小就可以了
由于我们的节点存的是区间的信息
所以最后还要乘以区间的长度
下面的区间幂次和也是要这样

区间幂次和
ll ask(int l, int r, int add, int mod) {
	iter llc = split(l), rrc = split(r + 1);
	ll ans = 0;
	for (; llc != rrc; llc++) ans = (ans + ll(llc->r - llc->l + 1) * fpow(llc->v, (ll)add, (ll)mod)) % mod;
	return ans;
}

找到每个区间加起和来就行
就是这么暴力
快速幂的时候要先把底数取个模
关于这个东西在线段树上怎么维护我还没找到有写的
如果有人知道请告诉我(ゝω・)

复杂度

s p l i t split 的均摊时间复杂度为 O ( l o g 2 l o g 2 n ) O(log_2log_2n)
a s s i g n assign 的均摊时间复杂度为 O ( l o g 2 l o g 2 n ) O(log_2log_2n)
区间加的均摊时间复杂度为 O ( l o g 2 n ) O(log_2n)
区间赋值(也就是 a s s i g n assign )的均摊时间复杂度为 O ( l o g 2 l o g 2 n ) O(log_2log_2n)
区间第k大的均摊时间复杂度为 O ( ( l o g 2 n ) ( l o g 2 l o g 2 n ) ) O((log_2n)(log_2log_2n))
区间幂次和的均摊时间复杂度为 O ( l o g 2 n ) O(log_2n)
以上均为均摊以及理论复杂度
下面引用作者的原话:

We suppose that we have a randomly selected range [l, r] now, and we randomly choose which operation it is, suppose that there are x intervals in this range.
1/4 possibility we use O(x) time to erase O(x) nodes.
2/4 possibility we use O(x) time to erase nothing.
1/4 possibility we use O(x) time to erase nothing and add 2 new nodes into the tree.
So we are expected to use O(x) time to erase O(x) nodes.
By using interval tree to maintain, the time complexity of this problem is O(mlogn)

大致翻译(个人修改):
假设我们现在有一个随机出的范围 [ l r ] [l,r] ,并且我们随机选择它是哪个操作,假设在这个范围内存在 x x 个区间。
1 / 4 1/4 的可能性我们使用 O ( x ) O(x) 时间擦除 O ( x ) O(x) 节点。
2 / 4 2/4 的可能性我们使用 O ( x ) O(x) 时间不删除任何内容。
另外 1 / 4 1/4 的可能性是,我们使用 O ( x ) O(x) 时间不删除任何内容,并在树中添加 2 2 个新节点。
因此,我们期望使用 O ( x ) O(x) 时间来擦除 O ( x ) O(x) 节点。
利用 s e t set (也就是原文中的区间树)进行维护,该问题的时间复杂度为 O ( m l o g n ) O(mlogn)

可见
它的效率在理想条件下是非常可观的,为 O ( m l o g n ) O(mlogn) ,可是在特意构造的数据下会到 O ( n m ) O(nm) 的数量级

不能只有这一道例题吧?
当然还有
比如这道题这道题就都可以用珂朵莉树做

珂朵莉树仅仅是一种工具而已
一种适用性小的工具
具体用处就看你的理解和灵活程度了
至此。
下面给份板子吧

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <complex>
#include <algorithm>
#include <climits>
#include <queue>
#include <map>
#include <set>
#include <vector>
#include <iomanip>
#define A 100010
#define B 2010
#define iter set<node>::iterator

using namespace std;
typedef long long ll;
const int modd = 1e9 + 7;
struct node {
	int l, r;
	mutable ll v;
	node (int L, int R = -1, ll V = 0) : l(L), r(R), v(V) {}
	friend bool operator < (const node a, const node b) {
		return a.l < b.l;
	}
};
ll a[A], seed, vm;
int n, m, x, y;
set<node> s;
iter split(int pos) {
	iter it = s.lower_bound(node(pos));
	if (it != s.end() and it->l == pos) return it;
	--it;
	int lt = it->l, lr = it->r;
	ll lv = it->v;
	s.erase(it);
	s.insert(node(lt, pos - 1, lv));
	return s.insert(node(pos, lr, lv)).first;
}
void assign(int l, int r, ll add = 0) {
	iter llc = split(l), rrc = split(r + 1);
	s.erase(llc, rrc);
	s.insert(node(l, r, add));
}
void add(int l, int r, ll add = 1) {
	iter llc = split(l), rrc = split(r + 1);
	for (; llc != rrc; llc++) llc->v += add;
}
ll kth(int l, int r, int k) {
	vector<pair<ll, int> > v;
	iter llc = split(l), rrc = split(r + 1);
	for (; llc != rrc; llc++) v.push_back(pair<ll, int>(llc->v, llc->r - llc->l + 1));
	sort(v.begin(), v.end());
	for (vector<pair<ll, int> >::iterator it = v.begin(); it != v.end(); it++) {
		k -= it->second;
		if (k <= 0) return it->first;
	}
	return -1LL;
}
ll fpow(ll a, ll b, ll mod) {
	ll ans = 1;
	a = a % mod;
	while (b) {
		if (b & 1) ans = ans * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return ans % mod;
}
ll ask(int l, int r, int add, int mod) {
	iter llc = split(l), rrc = split(r + 1);
	ll ans = 0;
	for (; llc != rrc; llc++) ans = (ans + ll(llc->r - llc->l + 1) * fpow(llc->v, (ll)add, (ll)mod)) % mod;
	return ans;
}
ll randd() {
	ll ans = seed;
	seed = (seed * 7 + 13) % modd;
	return ans;
}

int main() {
	cin >> n >> m >> seed >> vm;
	for (int i = 1; i <= n; i++) { //不要看不懂下面了这都是题目给出的!!!
		a[i] = (randd() % vm) + 1;
		s.insert(node(i, i, a[i]));
	}
	s.insert(node(n + 1, n + 1, 0));
	while (m--) {
		int opt = int(randd() % 4) + 1;
		int l = int(randd() % n) + 1;
		int r = int(randd() % n) + 1;
		if (l > r) swap(l, r);
		if (opt == 3) x = int(randd() % (r - l + 1)) + 1;
		else x = int(randd() % vm) + 1;
		if (opt == 4) y = int(randd() % vm) + 1;
		if (opt == 1) add(l, r, ll(x));
		else if (opt == 2) assign(l, r, ll(x));
		else if (opt == 3) printf("%lld\n", kth(l, r, x));
		else printf("%lld\n", ask(l, r, x, y));
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/yandaoqiusheng/article/details/84992816