珂朵莉树
1. 算法分析
玄学原理
用一个三元组(l, r, val)记录一个区间,set来存储。每次区间赋值操作可以把一段区间[l, r]快速赋值为一个值,把[l, r]这段区间内的所有子区间合并为一个区间{l, r, x}。由于数据随机,[l, r]的期望长度为$ \frac{ 1 }{ 3 } (r - l + 1)
,
即
每
次
赋
值
操
作
会
使
得
s
e
t
的
区
间
个
数
变
为
原
来
的
,即每次赋值操作会使得set的区间个数变为原来的
,即每次赋值操作会使得set的区间个数变为原来的 \frac{ 2 }{ 3 } $,这样使得set的大小快速下降,最后趋于logn个区间。认为每次操作时间复杂度为O(logn)(也就是说set内只有log个区间,所以每次都暴力处理时间也只是logn)
使用情景
当存在区间赋值的时候
2. 板子
/*
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的和(指数和)
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MOD7 = 1e9 + 7;
const int MOD9 = 1e9 + 9;
const int N = 1e5 + 7;
//快速幂
LL qmi(LL a, LL b, LL mod) {
LL res = 1;
LL ans = a % mod;
while (b) {
if (b & 1) res = res * ans % mod;
ans = ans * ans % mod;
b >>= 1;
}
return res;
}
// 表示[l,r]这一个区间中所有的数都是v
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& o) const { return l < o.l; }
};
set<node> s;
// 拆分操作
// 一个集合中,有一部分需要修改,而另一部分不需要修改.把集合拆开,拆成两部分,要修改的就修改,不修改的就算了
// 找pos所在的三元组[l, r],然后切割为[l, pos - 1], [pos, r]
set<node>::iterator split(int pos) {
auto it = s.lower_bound(node(pos)); //找到首个不小于pos的set
if (it != s.end() && it->l == pos) return it; //找到了,直接返回
--it; //否则一定在前一个区间中
if (pos > it->r) return s.end(); // 找不到,返回末尾迭代器的下一个位置
int L = it->l, R = it->r; //[l, r]就是要分裂的区间
LL V = it->v; //取出值
s.erase(it); //删除原集合
s.insert(node(L, pos - 1, V)); //构建前半段的新结合
return s.insert(node(pos, R, V)).first; //构建后半段的新集合并且返回地址
}
//区间加操作
void add(int l, int r, LL val = 1) {
split(l);
auto itr = split(r + 1), itl = split(l); // 得到区间左右端点迭代器
for (; itl != itr; ++itl) itl->v += val; // 暴力累加
}
//区间赋值操作
//珂朵莉树的复杂度是由assign_val保证的.由于数据随机,有1/4的操作为assign。vset的大小快速下降,最终趋于logn
//,使得这种看似暴力无比的数据结构复杂度接近mlogn 。
void assign_val(int l, int r, LL val = 0) {
split(l);
auto itr = split(r + 1), itl = split(l); //求出要被摊平区间的收尾地址
s.erase(itl, itr); //删除原集合
s.insert(node(l, r, val)); //添加新集合
}
//求区间第K大值,reversed控制顺序与逆序
LL rank_(int l, int r, int k, bool reversed = 0) {
vector<pair<LL, int>> vp;
if (reversed) k = r - l + 2 - k;
split(l);
auto itr = split(r + 1), itl = split(l); // 得到区间左右端点的迭代器
vp.clear();
for (; itl != itr; ++itl) // 放入vector
vp.push_back({itl->v, itl->r - itl->l + 1});
sort(vp.begin(), vp.end()); // 从小到大排序
for (auto i : vp) // 得到第k大
{
k -= i.second;
if (k <= 0) return i.first;
}
return -1LL;
}
//区间值求和操作
LL sum(int l, int r, int ex, int mod) {
split(l);
auto itr = split(r + 1), itl = split(l); // 得到左右的迭代器
LL res = 0;
for (; itl != itr; ++itl) // 暴力求和
res = (res + (LL)(itl->r - itl->l + 1) * qmi(itl->v, LL(ex), LL(mod))) %
mod;
return res;
}
int n, m;
LL seed, vmax;
LL rnd() {
LL ret = seed;
seed = (seed * 7 + 13) % MOD7;
return ret;
}
LL a[N];
int main() {
cin >> n >> m >> seed >> vmax;
for (int i = 1; i <= n; ++i) {
a[i] = (rnd() % vmax) + 1;
s.insert(node(i, i, a[i]));
}
s.insert(node(n + 1, n + 1, 0));
int lines = 0;
for (int i = 1; i <= m; ++i) {
int op = int(rnd() % 4) + 1;
int l = int(rnd() % n) + 1;
int r = int(rnd() % n) + 1;
if (l > r) swap(l, r);
int x, y;
if (op == 3)
x = int(rnd() % (r - l + 1)) + 1;
else
x = int(rnd() % vmax) + 1;
if (op == 4) y = int(rnd() % vmax) + 1;
if (op == 1)
add(l, r, LL(x)); // 区间增加x
else if (op == 2)
assign_val(l, r, LL(x)); // 区间赋值x
else if (op == 3)
cout << rank_(l, r, x) << endl; // 求区间第x大
else
cout << sum(l, r, x, y) << endl; // 区间指数和
}
return 0;
}
3. 例题
CF915E Physical Education Lessons
n个元素,黑白两种状态,起始全为黑色,区间黑白染色,每次操作结束后给出整个序列的黑色总数
/* 每个操作就是区间赋值0或1,顺带把总和修改一下 */
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int n, q;
LL res;
// 表示[l,r]这一个区间中所有的数都是v
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& o) const { return l < o.l; }
};
set<node> s;
// 拆分操作
// 一个集合中,有一部分需要修改,而另一部分不需要修改.把集合拆开,拆成两部分,要修改的就修改,不修改的就算了
// 找pos所在的三元组[l, r],然后切割为[l, pos - 1], [pos, r]
set<node>::iterator split(int pos) {
auto it = s.lower_bound(node(pos)); //找到首个不小于pos的set
if (it != s.end() && it->l == pos) return it; //找到了,直接返回
--it; //否则一定在前一个区间中
if (pos > it->r) return s.end(); // 找不到,返回末尾迭代器的下一个位置
int L = it->l, R = it->r; //[l, r]就是要分裂的区间
LL V = it->v; //取出值
s.erase(it); //删除原集合
s.insert(node(L, pos - 1, V)); //构建前半段的新结合
return s.insert(node(pos, R, V)).first; //构建后半段的新集合并且返回地址
}
//区间赋值操作
//珂朵莉树的复杂度是由assign_val保证的.由于数据随机,有1/4的操作为assign。vset的大小快速下降,最终趋于logn
//,使得这种看似暴力无比的数据结构复杂度接近mlogn 。
void assign_val(int l, int r, bool val) {
auto itr = split(r + 1), itl = split(l), it = itl;
for (; itl != itr; ++itl) { // 每次操作的时候顺便维护下res的值
if (val == 1 && itl->v == 0) res += itl->r - itl->l + 1;
if (val == 0 && itl->v == 1) res -= itl->r - itl->l + 1;
}
s.erase(it, itr);
s.insert(node(l, r, val));
}
int main() {
cin >> n >> q;
res = n;
s.insert(node(1, n, 1));
while (q--) {
int op, l, r;
scanf("%d%d%d", &l, &r, &op);
if (op == 1)
assign_val(l, r, 0); // 区间赋值为0
else
assign_val(l, r, 1); // 区间赋值为1
printf("%lld\n", res); // 打印1的数目
}
return 0;
}