[暴力数据结构] 珂朵莉树
珂朵莉树
0x00 绪言
Update:2022/11/23 原来文章大部分都是拼凑内容(现在也是.....),质量过低,所以进行了一次小换血,主要是对代码风格以及阅读体验进行了优化。
温馨提示:如果你幻想找到非指针珂朵莉树代码,那你可以停下了,就看这一篇文章即可,虽然这篇文章代码实现也是指针。但你不可能找到数组实现......
0x01 珂朵莉树的起源
珂朵莉树原名老司机树 (Old Driver Tree,ODT),是一种基于 std::set
的暴力数据结构,由 2017 年一场 CF 比赛中提出的数据结构,因为题目背景主角是《末日时在做什么?有没有空?可以来拯救吗?》的主角珂朵莉,因此该数据结构被称为珂朵莉树。
0x02 应用
解决各种线段树无法完成的操作。
注意珂朵莉树保持复杂度主要依靠 assign
操作,所以题目中必须有区间赋值。
还有很重要的一点:数据需纯随机。
0x03 什么时候用珂朵莉树
关键操作:推平一段区间,使一整段区间内的东西变得一样。保证数据随机。
n
个数,m
次操作。
操作:
-
区间加
-
区间赋值
-
区间第 小
-
求区间幂次和
-
数据随机
0x04 构造
用一个带结构体的集合 set
维护序列
集合中的每个元素有左端点,右端点,值
下面展示该结构体的构造:
struct node
{
int l, r;
mutable int val;
friend bool operator<(node a, node b)
{
return a.l < b.l;
}
node(int l, int r = 0, int val = 0) : l(l), r(r), val(val) {}
};
//mutale,意为可变的,即不论在哪里都是可修改的,用于突破C++带const函数的限制。
0x05 Split 操作
操作过程
set::iterator split(int pos)
将原来含有 pos
的区间分为 [l,pos)
和 [pos,r]
两段。
返回一个 std::set
的迭代器,指向 [pos,r]
段
可能有些抽象,详细解如下:
split
函数的作用就是查找 set
中第一个左端点不小于 pos
的结点,如果找到的结点的左端点等于 pos
便直接返回指向该结点的迭代器,如果不是,说明 pos
包含在前一个结点所表示的区间之间,此时便直接删除包含 pos
的结点,然后以 pos
为分界点,将此结点分裂成两份,分别插入 set
中,并返回指向后一个分裂结点的迭代器。
首先我们假设 set
中有三个 node
结点,这三个结点所表示的区间长度为 14
,如下图:
不妨以提取区间 [10,12]
为例详细展开(说好的查询 10
到 12呢,怎么下面扯了一堆
13` ?别急,后续将会揭晓 ):
如果我们要查询序列第 13
个位置,首先执行 auto it = s.lower_bound(node(13))
; 此时 it
将成为一个指向第三个结点的迭代器,为什么是第三个结点,而不是第二个结点呢,因为 lower_bound
这个函数获取的是第一个左端点 l
不小于 13
的结点,所以 it
是指向第三个结点的。
然后执行判断语句,发现第三个结点的左端点不是 13
,不满足条件,说明 13
必包含在前一个结点中,继续向下执行,让 it
指向前一个结点:
先将该结点的信息保存下来:int l = 10, r = 13, val = 2
然后直接删除该结点
以 pos
为分界点,将被删除的结点分裂为 [l,pos-1] ,[pos,r]
这两块,并返回指向 [pos,r]
这个区间的迭代器,事实上 return s.insert(node(pos,r,val)).first
; ,便做到了插入 [pos,r]
这端区间,并返回指向它的迭代器,有一个 insert
函数返回值为 pair
类型,其中 pair
的第一个元素就是元素插入位置的迭代器。
至此 13
位置已经分裂完成,然后是查询第 10
个位置,查询步骤同上,但是 10
号点满足 if
语句,便直接返回了
由上述步骤,为了提取区间 [10,12]
,我们执行了两次 split
,一次为 split(13)
,一次为 split(10)
,并获得了两个迭代器,一个指向第二结点,一个指向第三结点。
为什么要先分裂右端点,然后再分裂左端点呢?
因为如果先分裂左端点,返回的迭代器会位于所对应的区间以 l
为左端点,此时如果 r
也在这个节点内,就会导致分裂左端点返回的迭代器被 erase
掉,导致 RE
结合问题 1
和问题 2
,获取区间迭代器 auto itr = split(r+1), itl = split(l)
;
代码
auto split(int p)
{
auto it = s.lower_bound(node(p));
if (it != s.end() && it->l == p)
{
return it;
}
it--;
if (it->r < p)
{
return s.end();
}
int l = it->l;
int r = it->r;
int val = it->val;
s.erase(it);
s.insert(node(l, p - 1, val));
return s.insert(node(p, r, val)).first;
}
0x06 Assign 操作
操作过程
注意:以后在使用 split
分裂区间的时候,请先右后左,一般情况不会出事,但你没有机会失误。
区间赋值操作,也是珂树维持其复杂度的关键函数
很暴力的思想,既然刚刚我们写了一个 split
,那么就要把它用起来。
首先 split
出 l
并记返回值为 itl
,然后 split
出 r+1
并记返回值为 itr
,显然我们要操作的区间为 [itl,itr)
,那么我们将 [itl,itr)
删除 (std::set.erase(itl, itr))
,再插入一个节点 Node
,其 l
为 l
,r
为 r
,val
为赋值的 val
。
我们注意到因为这个操作, [itl,itr)
中的所有节点合并为了一个节点,大大降低了集合的元素数量,因此调整了我们的复杂度
代码
void assign(int l, int r, int x)
{
auto itr = split(r + 1);
auto itl = split(l);
s.erase(itl, itr);
s.insert(node(l, r, x));
}
//将一个区间全部改为某个值。
0x07 其他操作
通用方法是 split
出 l
,split
出 r+1
,然后直接暴力扫描这段区间内的所有节点执行需要的操作
查询区间和
long long querySum(int l, int r)
{
auto itr = split(r + 1);
auto itl = split(l);
long long res = 0;
for (auto i = itl; i != itr; i++)
{
res += (i->r - i->l + 1) * i->val;
}
return res;
}
区间加:
void add(int l, int r, int x)
{
auto itr = split(r + 1);
auto itl = split(l);
for (auto i = itl; i != itr; i++)
{
i->val += x;
}
}
区间第 k
小:
algorithm
库中的 std::sort
(快速排序)
std::map
(方便起见使用其中的pair
),std::vector
(方便起见)
还是split
出l
,split
出r+1
,然后将每个节点的值和个数(即r-l+1
)组成一个pair
(注意为了排序,将值放在第一关键字),将pair
加入一个vector
中
将vector
排序
从vector
的begin
开始扫描,不停的使k
减去vector
当前项的第二关键字,若 ,返回当前项的第一关键字。
int kth_number(int l, int r, int k)
{
auto itr = split(r + 1);
auto itl = split(l);
vector<rank> v;
for (auto i = itl; i != itr; i++)
{
v.push_back(rank(i->val, i->r - i->l + 1));
}
sort(v.begin(), v.end());
int i;
for (i = 0; i < v.size(); i++)
{
if (v[i].cnt < k)
{
k -= v[i].cnt;
}
else
{
break;
}
}
return v[i].num;
}
区间平方和
求区间所有数 x
次方的和模 y
的值
int qpow(int x, int b, int p)
{
int res = 1;
int a = x % p;
for (; b; b >>= 1)
{
if (b & 1)
{
res = res * a % p;
}
a = a * a % p;
}
return res;
}
int calc(int l, int r, int x, int y)
{
auto itr = split(r + 1);
auto itl = split(l);
int ans = 0;
for (auto i = itl; i != itr; i++)
{
ans = (ans + qpow(i->val, x, y) * (i->r - i->l + 1) % y) % y;
}
return ans;
}
0x08 模板题代码实现CF896C
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <set>
#define rint register int
#define endl '\n'
#define int long long
using std::set;
using std::vector;
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;
}
struct Chtholly_Tree
{
struct node
{
int l, r;
mutable int val;
friend bool operator<(node a, node b)
{
return a.l < b.l;
}
node(int l, int r = 0, int val = 0) : l(l), r(r), val(val) {}
};
set<node> s;
auto split(int p)
{
auto it = s.lower_bound(node(p));
if (it != s.end() && it->l == p)
{
return it;
}
it--;
if (it->r < p)
{
return s.end();
}
int l = it->l;
int r = it->r;
int val = it->val;
s.erase(it);
s.insert(node(l, p - 1, val));
return s.insert(node(p, r, val)).first;
}
void add(int l, int r, int x)
{
auto itr = split(r + 1);
auto itl = split(l);
for (auto i = itl; i != itr; i++)
{
i->val += x;
}
}
void assign(int l, int r, int x)
{
auto itr = split(r + 1);
auto itl = split(l);
s.erase(itl, itr);
s.insert(node(l, r, x));
}
struct rank
{
int num, cnt;
friend bool operator<(rank a, rank b)
{
return a.num < b.num;
}
rank(int num, int cnt) : num(num), cnt(cnt) {}
};
int kth_number(int l, int r, int k)
{
auto itr = split(r + 1);
auto itl = split(l);
vector<rank> v;
for (auto i = itl; i != itr; i++)
{
v.push_back(rank(i->val, i->r - i->l + 1));
}
sort(v.begin(), v.end());
int i;
for (i = 0; i < v.size(); i++)
{
if (v[i].cnt < k)
{
k -= v[i].cnt;
}
else
{
break;
}
}
return v[i].num;
}
int qpow(int x, int b, int p)
{
int res = 1;
int a = x % p;
for (; b; b >>= 1)
{
if (b & 1)
{
res = res * a % p;
}
a = a * a % p;
}
return res;
}
int calc(int l, int r, int x, int y)
{
auto itr = split(r + 1);
auto itl = split(l);
int ans = 0;
for (auto i = itl; i != itr; i++)
{
ans = (ans + qpow(i->val, x, y) * (i->r - i->l + 1) % y) % y;
}
return ans;
}
void build()
{
for (rint i = 1; i <= n; i++)
{
a[i] = (rnd() % vmax) + 1;
s.insert(node(i, i, a[i]));
}
}
} tree;
signed main()
{
scanf("%lld%lld%lld%lld", &n, &m, &seed, &vmax);
tree.build();
while (m--)
{
int op, l, r, x, y;
op = (rnd() % 4) + 1;
l = (rnd() % n) + 1;
r = (rnd() % n) + 1;
if (l > r)
{
std::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)
{
tree.add(l, r, x);
}
if (op == 2)
{
tree.assign(l, r, x);
}
if (op == 3)
{
printf("%lld\n", tree.kth_number(l, r, x));
}
if (op == 4)
{
printf("%lld\n", tree.calc(l, r, x, y));
}
}
return 0;
}
0x09 例题
CF915E
每个操作就是区间赋值 0 或 1,顺带把总和修改一下
//把 cin 改成快读就可以了,这个题卡常
#include <bits/stdc++.h>
#define rint register int
#define endl '\n'
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, m;
struct node
{
int l, r;
mutable int val;
friend bool operator<(node a, node b)
{
return a.l < b.l;
}
node(int l, int r = 0, int val = 0) : l(l), r(r), val(val) {}
};
struct Chtholly_Tree
{
set<node> s;
int sum = 0;
auto split(int p)
{
auto it = s.lower_bound(node(p));
if (it != s.end() && it->l == p)
{
return it;
}
it--;
if (it->r < p)
{
return s.end();
}
int l = it->l;
int r = it->r;
int val = it->val;
s.erase(it);
s.insert(node(l, p - 1, val));
return s.insert(node(p, r, val)).first;
}
void assign(int l, int r, int x)
{
auto itr = split(r + 1);
auto itl = split(l);
auto it = itl;
for( ;itl != itr; itl++) sum -= itl->val * (itl->r - itl->l + 1);
s.erase(it, itr);
s.insert(node(l, r, x));
sum += x * (r - l + 1);//这个跟模板不一样,要顺带计算一下
}
} tree;
signed main()
{
cin >> n >> m;
tree.s.insert(node(1, n, 1));
tree.sum = n;
while(m--)
{
int l, r, op;
cin >> l >> r >> op;
if(op == 1)
{
tree.assign(l, r, 0);
}
else
{
tree.assign(l, r, 1);
}
cout << tree.sum << endl;
}
return 0;
}
0x10 后话
半年多了,终于重新更了,个人感觉质量涨了许多(最近没时间,0x09 我会尽快更的555.....)
本文作者:PassName
本文链接:https://www.cnblogs.com/spaceswalker/p/15884858.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步