首页 > 其他分享 >珂朵莉树学习笔记

珂朵莉树学习笔记

时间:2022-10-22 10:00:23浏览次数:74  
标签:学习 end auto ll 笔记 朵莉树 start 区间 spilt

0x00 前言

0x01 关于其命名

  最开始出现在 Codeforces Round #449 (Div. 1) C题 上,这位珂学家在题解中用了一种玄学的数据结构解题,开始命名为 ODT树(Old Driver Tree,老司机树,以出题者的ID命名),后来普遍称为珂朵莉树。

0x02 能解决的问题

  珂朵莉树用于解决含有区间平推操作(即将区间上的数全部变为一个数)的问题时卓有成效,在数据随机的情况下,用 set 实现复杂度为 \(O(N \ log \ log \ N)\),用链表实现复杂度为 \(O(N \ log \ N)\),比同类问题其他算法更优。时间复杂度证明请移步这篇文章

0x03 前置知识

0x10 正文

  本文使用 Codeforces Round #449 (Div. 1) C题 作为例题讲解珂朵莉树。

0x11 题意

-- 威廉...
-- 怎么了?
-- 瑟尼欧里斯好像出了什么问题...
-- 我会看看的...

瑟尼欧里斯是一把由特殊护符按特定顺序排列组成的剑。
已经 \(500\) 年过去了,现在剑的状态很差,所以威廉决定检查一下。
瑟尼欧里斯由 \(n\) 片护符组成,威廉把它们排成一列,每个护符上有一个数字 \(a_i\)。
为了保养它,威廉需要进行 \(m\) 次操作。
这里有四种操作:

  • \(1 \ l \ r \ x\) : 将区间 \([l, r]\) 上的数加上 \(x\)。
  • \(2 \ l \ r \ x\) : 将区间 \([l, r]\) 上的数全部变为 \(x\)。
  • \(3 \ l \ r \ x\) : 查询区间 \([l, r]\) 的第 \(x\) 大数。
  • \(4 \ l \ r \ x \ y\) : 查询区间 \([l, r]\) 上的数的 \(x\) 次方之和对 \(y\) 取模的值。

本题输入较为特殊,输入格式如下:
一行四个整数,分别为 \(n\),\(m\),\(seed\),\(vmax\),前两个变量意义如题目所述,后两个变量用于生成随机数据,数据生成伪代码如下

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

0x12 珂朵莉树基本思路

  由于数据随机,所以在区间平推操作中区间长度普遍不会太短,所以区间总个数不会太多,于是我们就考虑维护每一个这样连续的区间,区间中的数都相同。

0x13 结构体定义

  用一个结构体来维护每一个区间的信息。

struct node {
	ll l, r; //区间左右端点
	mutable ll v; //区间单个元素值
	node(ll l, ll r, ll v) : l(l), r(r), v(v) {}
	bool operator< (const node &a) const { return l < a.l; }
};

  在上述定义中有下面一点需要注意:

  • 因为元素值并不是固定的,所以一定要用 mutabel 让元素值可变起来

0x14 初始化

#include<set>
set<node> tree;

  这样你就得到了一颗啥也没有的珂朵莉树。

0x15 spilt操作

  因为一个区间上的数不一定自始至终都是一样的,所以我们需要一个分割函数将区间分隔开,这就是 spilt 函数。
  这个操作是珂朵莉树的核心操作之一,此函数有一个参数,表示要分裂的位置,我们先看代码,再解释它的运作过程。

auto spilt(ll pos) {
	auto it = tree.lower_bound(node(pos, 0, 0));
	if(it != tree.end( ) && it -> l == pos) return it;
	it--;
	ll l = it -> l, r = it -> r, v = it -> v;
	tree.erase(it);
	tree.insert(node(l, pos - 1, v));
	return tree.insert(node(pos, r, v)).first;
}

  首先,我们要找到一个左端点大于等于 \(pos\) 的区间,用一个迭代器指向它(注意,如果你使用的是c++11,auto 必须要换成 set<node>::iterator),如果当前区间的左端点等于 \(pos\) (并且这个区间要存在)那就说明当前区间不用分割,直接返回当前迭代器,否则就向前跳转到前一个区间,并将其分割为 \([l, pos - 1]\) 和 \([pos, r]\) 两个区间。

0x16 assgin操作

  珂朵莉树的核心操作之二,也就是区间平推操作。
  有了 spilt 函数,我们的实现也简单了很多,依旧是对着代码解释。

void assgin(ll l, ll r, ll v) {
	auto end = spilt(r + 1), start = spilt(l);
	tree.erase(start, end);
	tree.insert(node(l, r, v));
}

  实现思路没什么好讲的,无非就是断开需要赋值的区间,全部删除再加入一个新的区间,重点在 spilt 的顺序上。
  看上去貌似和顺序没什么关系,如果单从逻辑上看确实如此,但是如果从实现上去看就会发现问题。
  假设我们要从区间 \([1, 10]\) 里截取出 \([3, 7]\),我们先执行 spilt(1),现在 start 迭代器指向的是区间 \([3, 10]\),然后我们再执行 spilt(8),end 则指向了区间 \([8,10]\),此时我们发现 start 指向的迭代器被第二次 spilt 操作 erase 掉了,所以调用时可能会 RE。(之所以是可能,是因为这东西比较玄学,有可能一会 RE,一会 AC,为了避免这种麻烦,还是规范写法较为稳妥)
  如果还是不理解,就结合下图再多看几遍上一段。

0x17 其他代码实现

  核心代码就上面两个,剩下的乱搞就行。

void add(ll l, ll r, ll x) { //区间加操作
	auto end = spilt(r + 1), start = spilt(l);
	for(auto it = start; it != end; it++)
		it -> v += x; //mulable的作用在此
} 
struct Rank {
	ll num, cnt; // 值与数量
	Rank(ll num, ll cnt) : num(num), cnt(cnt) {}
	bool operator< (const Rank &a) const { return num < a.num; }
};

ll get_rank(ll l, ll r, ll x) { //求区间第 x 大数
	auto end = spilt(r + 1), start = spilt(l);
	vector<Rank> vec;
	for(auto it = start; it != end; it++) vec.push_back(Rank(it -> v, it -> r - it -> l + 1));
	sort(vec.begin( ), vec.end( )); //将区间上的所有数排序,以便后续暴力查找
	int i;
	for(i = 0; i < vec.size( ); i++) {
		if(vec[i].cnt < x) x -= vec[i].cnt;
		else break;
	}
	return vec[i].num;
}
ll get_power(ll l, ll r, ll x, ll y) { //求区间 x 次方和 mod y 的值
	auto end = spilt(r + 1), start = spilt(l);
	ll ans = 0;
	for(auto it = start; it != end; it++) ans = (ans + power(it -> v, x, y) * (it -> r - it -> l + 1) % y) % y; //power 为快速幂函数
	return ans;
}

0x17 完整代码

  请在确保自己理解上述所有内容的情况下阅读

#include<algorithm>
#include<iostream>
#include<vector>
#include<cstdio>
#include<set>
using namespace std;

typedef long long ll;
const int MOD = 1e9 + 7;
ll n, m, seed, vmax;

struct node {
	ll l, r;
	mutable ll v;
	node(ll l, ll r, ll v) : l(l), r(r), v(v) {}
	bool operator< (const node &a) const { return l < a.l; }
};

struct Rank {
	ll num, cnt;
	Rank(ll num, ll cnt) : num(num), cnt(cnt) {}
	bool operator< (const Rank &a) const { return num < a.num; }
};

set<node> tree; 

ll rnd( );
auto split(ll pos);
void add(ll l, ll r, ll x);
ll power(ll a, ll b, ll p);
void assgin(ll l, ll r, ll v);
ll get_rank(ll l, ll r, ll x);
ll get_power(ll l, ll r, ll x, ll y);

int main( ) {
	cin >> n >> m >> seed >> vmax;
	for(int i = 1; i <= n; i++) tree.insert(node(i, i, rnd( ) % vmax + 1));
	for(int i = 1; i <= m; i++) {
		ll op, l, r, x, y;
		op = rnd( ) % 4 + 1;
		l = rnd( ) % n + 1;
		r = rnd( ) % n + 1;
		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) add(l, r, x);
		if(op == 2) assgin(l, r, x);
		if(op == 3) cout << get_rank(l, r, x) << endl;
		if(op == 4) cout << get_power(l, r, x, y) << endl;
	}
	return 0;
}

auto spilt(ll pos) {
	auto it = tree.lower_bound(node(pos, 0, 0));
	if(it != tree.end( ) && it -> l == pos) return it;
	it--;
	ll l = it -> l, r = it -> r, v = it -> v;
	tree.erase(it);
	tree.insert(node(l, pos - 1, v));
	return tree.insert(node(pos, r, v)).first;
}

void assgin(ll l, ll r, ll v) {
	auto end = spilt(r + 1), start = spilt(l);
	tree.erase(start, end);
	tree.insert(node(l, r, v));
}

void add(ll l, ll r, ll x) {
	auto end = spilt(r + 1), start = spilt(l);
	for(auto it = start; it != end; it++)
		it -> v += x;
} 

ll get_rank(ll l, ll r, ll x) {
	auto end = spilt(r + 1), start = spilt(l);
	vector<Rank> vec;
	for(auto it = start; it != end; it++) vec.push_back(Rank(it -> v, it -> r - it -> l + 1));
	sort(vec.begin( ), vec.end( ));
	int i;
	for(i = 0; i < vec.size( ); i++) {
		if(vec[i].cnt < x) x -= vec[i].cnt;
		else break;
	}
	return vec[i].num;
}

ll get_power(ll l, ll r, ll x, ll y) {
	auto end = spilt(r + 1), start = spilt(l);
	ll ans = 0;
	for(auto it = start; it != end; it++) ans = (ans + power(it -> v, x, y) * (it -> r - it -> l + 1) % y) % y;
	return ans;
}

ll power(ll a, ll b, ll p) {
	ll res = 1, base = a % p;
	while(b) {
		if(b & 1) res = (res * base) % p;
		base = (base * base) % p;
		b >>= 1;
	}
	return res;
}

ll rnd( ) {
	ll res = seed;
	seed = (seed * 7 + 13) % MOD;
	return res;
}

0x18 小结

  珂朵莉树的核心其实就二十行左右的代码,并不是什么很难得算法,但是由于其对于数据的要求,很少有题将其作为正解,但是考场骗分还是很有用的。

0x19 习题

0x20 后记

  本文是本蒟蒻近期学习了珂朵莉树,为了巩固所以写下了这篇学习笔记,如果有纰漏请指出。
  另外感谢本文用到的所有资料的提供者。

标签:学习,end,auto,ll,笔记,朵莉树,start,区间,spilt
From: https://www.cnblogs.com/PlankBlack/p/ChthollyTree.html

相关文章

  • Newtonsoft.Json笔记 -JToken、JObject、JArray详解
    在原来解析json数据是,一般都是用反序列化来实现json数据的解读,这需要首先知道json数据的结构并且建立相应的类才能反序列化,一旦遇到动态的json数据,这种方法就不使用。为了......
  • Vue笔记3 计算属性 ES6语法、作用域闭包var/let/const、v-if/v-else/v-else-if 、v-sh
              闭包                    事件冒泡 阻止默认事件        key="username"......
  • Python学习三天计划-2
    一、函数函数:是组织好的,可重复使用的,用来实现特定功能的代码段。优点:可供重复利用的代码段提高程序的复用性减少重复性代码提高开发效率1.定义deffunc1():......
  • JodeTime的笔记
    Jode-TimeJode-Time介绍任何企业应用程序都需要处理时间问题。应用程序需要知道当前的时间点和下一个时间点,有时它们还必须计算这两个时间点之间的路径。使用JDK完......
  • 2022-2023-1 20221312 《计算机基础与程序设计》第八周学习总结
    作业信息班级链接:https://edu.cnblogs.com/campus/besti/2022-2023-1-CFAP作业要求:https://www.cnblogs.com/rocedu/p/9577842.html#WEEK08作业目标:面向对象,面向过程,顶点,......
  • Pytorch学习笔记
    两大强大的工具函数:1.dir(),打开一个包,输出包内含有的其他子类2.help(),帮助文档1help(torch.cuda.is_available)2Helponfunctionis_availableinmoduletorch.cuda......
  • 正则表达式笔记
    几次想学但是都没学会……现在作业要用就还是硬着头皮学一下吧部分材料源于这个我一开始还不太能理解这怎么配这么多stars,后面才发现stars给的是这个基本语法开始和结......
  • flink入门学习
    一:为什么使用flink1.jdk实现流式处理packagenet.xdclass.app;importnet.xdclass.model.VideoOrder;importjava.util.Arrays;importjava.util.List;importjav......
  • 2022-2023-1 20221408《计算机基础与程序设计》第八周学习总结
    第八周学习总结作业信息这个作业属于哪个课程:https://edu.cnblogs.com/campus/besti/2022-2023-1-CFAP这个作业的要求在哪里:https://www.cnblogs.com/rocedu/p/9577842......
  • 6.824笔记2
    线程为每一个prc请求使用一个线程,当请求回收的时候,线程继续运作,多线程能能够开启多个网络请求,形成io并发并行化,线程用来实现并行化异步编程,事件驱动编程,又一个线程,一个循......