珂朵莉树
名称简介
老司机树,ODT(Old Driver Tree),又名珂朵莉树(Chtholly Tree)。起源自 CF896C。
核心思想
分块,把块按照顺序储存在一起。把值相同的区间合并成一个结点保存在 set 里面。
复杂度
骗分。只要是有区间赋值操作的数据结构题都可以用来骗分。在数据随机的情况下一般效率较高,但在不保证数据随机的场合下,会被精心构造的特殊数据卡到超时。
如果要保证复杂度正确,必须保证数据随机。如果数据不够随机,区间比较紧密,那么将会出现许多碎片化的区块,就像磁盘一样。对于 add,assign 和 sum 操作,用 set 实现的珂朵莉树的复杂度为 O ( n log log n ) O(n \log \log n) O(nloglogn),而用链表实现的复杂度为 O ( n log n ) O(n \log n) O(nlogn)。
数据结构
一个结构体表示区块。
struct Block
{
int l; // 区间左端点(包括)
int r; // 区间右端点(不包括)
mutable int v; // 区间元素
// 自定义比较函数,关键字为区间左端点。
bool operator<(const Block &rhs) const
{
return l < rhs.l;
}
};
其中,区间中的元素具有相同的元素的值。
mutable 的意思是“可变的”,让我们可以在后面的操作中修改 v 的值。在 C++ 中,mutable 是为了突破 const 的限制而设置的。被 mutable 修饰的变量(mutable 只能用于修饰类中的非静态数据成员),将永远处于可变的状态,即使在一个 const 函数中。
这意味着,我们可以直接修改已经插入 set 的元素的 v 值,而不用将该元素取出后重新加入 set。
这之后,我们使用set来维护一个区块序列。
set<Block> tree;
typedef set<Block>::iterator iter;
初始化
void init(int l, int r, int v)
{
tree.insert(Block(l, r, v));
}
分裂
这是珂朵莉树的核心,将一个区块拆成两个区间,其中分裂点为 x x x。
// 分裂区块,返回以x为左端点的区块的迭代器
// 分裂区块,返回以x为左端点的区块的迭代器
iter split(int x)
{
// 寻找左端点第一个大于等于x区块
iter it = tree.lower_bound(Block(x, 0, 0));
// 如果存在区块正好是左端点,那么就不用分裂,直接返回迭代器即可
if (it != tree.end() && it->l == x)
{
return it;
}
// 否则就要退回一个区块
it--;
Block o = *it;
// 删除原来的区块,插入新的区块即可
tree.erase(it);
tree.insert(Block(o.l, x - 1, o.v));
return tree.insert(Block(x, o.r, o.v)).first;
}
赋值
对区间 [ l , r ] [l,r] [l,r]进行元素复制,需要拆分区块一个以 l l l为左端点,一个以 r r r为左端点。
// 将区间[l,r]内的元素全部赋值为v
void assign(int l, int r, int v)
{
// 切分区间
iter itr = split(r + 1);
iter itl = split(l);
// 删除区间内的所有小区块
tree.erase(itl, itr);
// 插入目标区块
tree.insert(Block(l, r, v));
}
注:珂朵莉树在进行求取区间左右端点操作时,必须先 split 右端点,再 split 左端点。若先 split 左端点,返回的迭代器可能在 split 右端点的时候失效,可能会导致 RE。
区间操作模板
其他操作都可也基于以下模板进行操作,根据需要的操作进行魔改。
// 将区间[l,r]操作
void proc(ll l, ll r)
{
// 切分区间
iter itr = split(r + 1);
iter itl = split(l);
for (iter i = itl; i != itr; i++)
{
// TODO 遍历区块
}
// 删除区间内的所有小区块
tree.erase(itl, itr);
// 插入目标区块
tree.insert(Block(l, r, v));
}
区间加法
void add(ll l, ll r, int v)
{
// 切分区间
iter itr = split(r + 1);
iter itl = split(l);
// 挨个加
for (iter i = itl; i != itr; i++)
{
i->v += v;
}
}
求区间第K大值
直接扔进优先队列即可。
int kth(ll l, ll r, int k)
{
// 切分区间
iter itr = split(r + 1);
iter itl = split(l);
// 挨个加
priority_queue<int> pq;
for (iter i = itl; i != itr; i++)
{
pq.push(i->v);
}
for (int i = 0; i < k - 1; i++)
{
pq.pop();
}
return pq.top();
}
例题
理解了题目的操作之后,我们发现,我们每次插入一个序列操作,需要在一个时间线上寻找位置,而区间段本身就是有序的,我们可以使用珂朵莉树解决(骗分)此问题。
#include <bits/stdc++.h>
using namespace std;
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
typedef long long ll;
struct Segment
{
int l;
int r;
bool operator<(const Segment &o) const
{
return l < o.l;
}
};
typedef set<Segment>::iterator iter;
// Chtholly ODT
struct Machine
{
set<Segment> sequence;
Machine()
{
// ! insert an enough BIG SEGMENT
sequence.insert({1, 50000});
}
int insert(int start, int len)
{
iter it = sequence.lower_bound({start, 0});
if (it->l != start && it != sequence.begin())
it--;
for (; max(start, it->l) + len - 1 > it->r; it++)
;
int ed = max(start, it->l) + len - 1;
// delete
Segment old = *it;
sequence.erase(it);
// insert
if (old.l < start)
{
// before
sequence.insert({old.l, start - 1});
}
// current
// {max(start, old.l), ed};
// after
if (ed < old.r)
{
sequence.insert({ed + 1, old.r});
}
return ed;
}
} Ma[20];
int que[405]; // queue
int match[20][20]; // pieces-machines match
int lens[20][20]; // time
int order[20]; // order
int prv[20]; // the last time of the i-th piece
int main()
{
int m, n;
// Read
scanf("%d %d", &m, &n);
for (int i = 1; i <= m * n; i++)
{
scanf("%d", que + i);
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
scanf("%d", &match[i][j]);
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
scanf("%d", &lens[i][j]);
}
}
// DO THAT
int ans = 0;
for (int i = 1; i <= m * n; i++)
{
int gid = que[i];
int ord = ++order[gid];
int mid = match[gid][ord];
prv[gid] = Ma[mid].insert(prv[gid] + 1, lens[gid][ord]);
ans = max(ans, prv[gid]);
}
printf("%d", ans);
return 0;
}
总结
其实就是分块的思想,注意,珂朵莉树非常容易HACK,数据不够随机效率就会大大下降。