浅谈珂朵莉树

珂朵莉最可爱了。

好了不废话了,直接开始珂朵莉树。

什么是珂朵莉树

珂朵莉树,又叫老司机树,英文名字 ODT,是一种支持区间平推的乱搞数据结构,在数据随机时表现十分优秀。

一般来说有两种实现方式,分别是链表实现和 set 实现。本文讲解第二种实现方式。

珂朵莉树的思想

珂朵莉树的思想主要是将所有颜色相同(也就是数值相同)的区间缩成一个一个的块,将这些块的左右端点插入 set ,再进行后续操作。

举个小栗子,比如我现在有数列 a = {1, 1, 2, 2, 1, 1, 1, 3},那么就可以维护这样四个小块:1, 1, 2, 2, 1, 1, 1, 3。把这些小块放到 set 里,在进行后续操作。比如我查询区间和,我可以把在区间内的小块都取出来然后求和。由于小块个数在随机数据下远小于数的个数,因此可以保证复杂度。(复杂度具体我也不会证。)

注意: 珂朵莉树仅可在以下情况下使用:

  1. 数据随机
  2. 操作中有平推操作

否则复杂度无法保证。

另外多提一嘴,珂朵莉树保证复杂度的机制是,set 内块的个数期望不超过 logn

操作

定义

根据珂朵莉树的思想,我们可以把每个块压缩成三个信息,也就是一个三元组 {l,r,v},分别表示这个块的左端点,右端点和里面所有数的权值。比如在数列 a = {1, 1, 2, 2, 1, 1, 1, 3} 中,第二个块就是 {3,4,2},表示第二个块左端点在 3,右端点在 4,里面的所有数都是 2

这样,我们就只需要在 set 中放入每个块的左右端点以及权值,并且按照左端点排序即可。

struct node {
    int l, r;
    mutable int v; // 注意这里的 mutable 一定不能改,否则会有奇奇怪怪的错误。
    node(int L, int R = -1, int V = 0):l(L), r(R), v(V){}
    bool operator < (const node& W)const { // 按左端点排序
	return l < W.l;
    }
};
set<node> s;
#define itset set<node> :: iterator // 宏定义迭代器

建树

建树没啥好说的,就是把初始所有的点一个一个插进去。有时候都不需要建树。

for (int i = 1; i <= n; i ++ )
    s.insert(node(i, i, w[i]));

分裂(split)

split(pos) 操作是将 pos 所在的块分成两个,并返回右边块所在的迭代器。

当然,我们只需要在 set 中找到 pos 所在的迭代器,找到这个块的左端点 l 和右端点 r,删除原块,并加入 [l, pos), [pos, r] 两块即可。

itset split(int pos) {
    itset p = s.lower_bound(node(pos)); // 找到左端点大于等于 pos 的第一个块
    // 当然也就是找到 pos 所在块右边的第一个块
    if (p != s.end() && p -> l == pos) return p;
    -- p; // pos 所在块就是 p
    int ls = p -> l, rs = p -> r, vv = p -> v;
    s.erase(p); // 删除原块
    s.insert(node(ls, pos - 1, vv)); // 插入分裂后的左块
    return s.insert(node(pos, rs, vv)).first; // 插入右块
}

平推(assign)

assign(l, r, v) 操作是将 [l,r] 区间内都替换成 v

当然,我们只要将 l,r 分裂出来,将 [l,r+1) 之间的删掉,最后插入块 (l,r,v) 即可。

void assign(int l, int r, int v) {
    itset rs = split(r + 1), ls = split(l); // 注意这里 l, r 也不能反,r 别忘了加一(因为右端是开区间)
    s.erase(ls, rs);
    s.insert(node(l, r, v));
}

其他操作

操作思路和平推基本一样,就是取出 [l,r] 之间的所有块,然后再一个一个处理即可。块数是 log 级别,可以乱搞。

比如求 [l,r] 之间的所有数的和:

int query(int l, int r) {
    itset rs = split(r + 1), ls = split(l);
    int ans = 0;
    for (itset it = ls; it != rs; it ++ )
    	ans += (it -> r - it -> l + 1) * it -> v;
    return ans;
}

最后再放一个完整代码 Luogu P5350 序列

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
#include <set>

using namespace std;

using LL = long long;
using PII = pair<int, int>;
using PLL = pair<LL, LL>;

const int mod = 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){}
	bool operator < (const node& tmp)const {
		return l < tmp.l;
	}
};
set<node> s;
#define itset set<node>::iterator

int n, m;

itset split(int pos) {
	itset p = s.lower_bound(node(pos));
	if (p != s.end() && p -> l == pos) return p;
	-- p;
	int ls = p -> l, rs = p -> r, vv = p -> v;
	s.erase(p);
	s.insert(node(ls, pos - 1, vv));
	return s.insert(node(pos, rs, vv)).first;
}

void assign(int l, int r, int v) {
	itset rs = split(r + 1), ls = split(l);
	s.erase(ls, rs);
	s.insert(node(l, r, v));
}

LL query(int l, int r) {
	itset rs = split(r + 1), ls = split(l);
	LL ans = 0;
	for (itset it = ls; it != rs; ++ it)
		(ans += (it -> r - it -> l + 1) * it -> v) %= mod;
	return ans;
}

void add(int l, int r, int v) {
	itset rs = split(r + 1), ls = split(l);
	for (itset it = ls; it != rs; ++ it)
		(it -> v += v) %= mod;
}

void copy(int l1, int r1, int l2, int r2) {
	itset rs = split(r1 + 1), ls = split(l1);
	vector<node> v;
	for (itset it = ls; it != rs; ++ it)
		v.push_back(*it);
	rs = split(r2 + 1), ls = split(l2);
	s.erase(ls, rs);
	for (auto i : v) {
		s.insert(node(i.l + l2 - l1, i.r + l2 - l1, i.v));
	}
}

void Swap(int l1, int r1, int l2, int r2) {
	if (l1 > l2) swap(l1, l2), swap(r1, r2);
	itset rs = split(r2 + 1), ls = split(l2);
	vector<node> v1, v2;
	for (itset it = ls; it != rs; ++ it)
		v2.push_back(*it);
	rs = split(r1 + 1), ls = split(l1);
	for (itset it = ls; it != rs; ++ it)
		v1.push_back(*it);
	s.erase(ls, rs);
	rs = split(r2 + 1), ls = split(l2);
	s.erase(ls, rs);
	
	for (auto i : v1)
		s.insert(node(i.l + l2 - l1, i.r + l2 - l1, i.v));
	for (auto i : v2)
		s.insert(node(i.l + l1 - l2, i.r + l1 - l2, i.v));
}

void reverse(int l, int r) {
	itset rs = split(r + 1), ls = split(l);
	vector<node> v;
	for (itset it = ls; it != rs; ++ it)
		v.push_back(*it);
	s.erase(ls, rs);
	reverse(v.begin(), v.end());
	for (auto i : v) {
		int len = i.r - i.l;
		s.insert(node(l, l + len, i.v));
		l += len + 1;
	}
}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i ++ ) {
		int w; scanf("%d", &w);
		s.insert(node(i, i, w));
	}
	
	while (m -- ) {
		int op, l1, r1, l2, r2, v;
		scanf("%d%d%d", &op, &l1, &r1);
		switch(op) {
			case 1:
				printf("%d\n", query(l1, r1));
				break;
			case 2:
				scanf("%d", &v);
				assign(l1, r1, v);
				break;
			case 3:
				scanf("%d", &v);
				add(l1, r1, v);
				break;
			case 4:
				scanf("%d%d", &l2, &r2);
				copy(l1, r1, l2, r2);
				break;
			case 5:
				scanf("%d%d", &l2, &r2);
				Swap(l1, r1, l2, r2);
				break;
			default:
				reverse(l1, r1);
				break;
		}
	}
	
	for (itset i = s.begin(); i != s.end(); ++ i) {
		for (int j = i -> l; j <= i -> r; j ++ )
			printf("%lld ", (i -> v) % mod);
	}
	
	return 0;
}

好了,现在你已经完全掌握珂朵莉树了,拿几道小题练练手吧!

例题

Luogu P4979 矿洞:坍塌

Luogu P5350 序列

Luogu P4979

CF915E Physical Education Lessons

CF896C Willem, Chtholly and Seniorious (这个是模板题,但是并不好)

这些题后面可能还会补上题解。先挖个坑

posted @   Link-Cut-Y  阅读(264)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
点击右上角即可分享
微信分享提示