【ybt金牌导航4-4-2】【luogu P2042】维护数列(fhq Treap 做法)
维护数列
题目链接:ybt金牌导航4-4-2 / luogu P2042
题目大意
给你一个序列,要你支持一些操作。
往一个地方插入一段数,删去一段连续的数,将一段连续的数改成同一个值,将连续的一段数翻转,求出连续的一段数的和,求当前数列的最长子数列。
思路
这种题看着都知道是用平衡树来做。
看着也知道要维护很多的懒标记,非常的恶心。
这里讲的是无旋 Treap,即 fhq Treap 的做法。
无旋 Treap 的做法这里就不多说的,主要是来解决几个实现的问题。
- 它会插入一些数组,那一个一个点插时间复杂度太大了,会直接跑到 \(nlogn\) 级别。
我们会考虑把这个数组先建一个树,然后再用合并操作,分离出插入的位置,夹在中间合并。
但问题是你要如何块苏的建一个树呢?
那我们先看单点的插入,不难想到插入方法:
我们从根节点开始,不断的与当前点比较优先级,如果不优就往下走,找到第一个优的,然后它就变成这个位置的父亲的右儿子,这个位置就变成它的左儿子。
那可以看出它只会在右儿子上跑,那我们可以开一个栈维护。
每次即不停的从小往上看,更优就一直往上跳,跳到不优的前一个。然后就连,然后把不优的都弹出,放入你现在的。
那每个点最多入栈出栈一次,时间复杂度啊就是 \(O(n)\) 的了。
- 翻转
有人会想到文艺平衡树,没错,大概也是同样的方法,用懒标记。
- 求和最大的子序列
做线段树的时候就有这种题,大概做法是维护前缀最大和后缀最大,通过这两个来维护这个求和最大。
而且你发现这个东西翻转的时候求和最大不变,但是前缀最大和后缀最大是也要交换的。
然后再加值啊,换值的时候都要重新维护。
总的来说就是多 \(up(now)\),多 \(down(now)\)
- 区间赋值
不难想到是用懒标记,而且用的时候还可以把翻转的懒标记去掉。
(不过赋值完都是一样的,翻转不翻转好像都无所谓了)
- 关于空间
由于你要维护很多东西,而且操作次数达到了 \(4\times10^6\) 你开那么多数组会 MLE。
但你你会发现它任何时刻内,数组内最多有 \(5\times 10^5\) 个数,那也就是说,有很多的数会被删去。
那你这些数删去它们就不存在了,那它们原本占用的位置你可以重新利用。
那你可以搞一个队列记录被删去,位置还没有被重新用的数的位置,然后你要加点的时候,如果队列空了就新开空间放,否则就用队列里面的位置,然后把这个位置从队列中弹出。
然后就是痛苦的具体实现了,具体可以看看代码。
代码
#include<queue>
#include<cstdio>
#include<cstdlib>
#include<iostream>
#define N 500051
#define INF 1e9
using namespace std;
int n, m, a[N], root, sta[N], size[N], yj[N], TOT, turn[N], change[N];
int ls[N], rs[N], val[N], lmax[N], rmax[N], sum[N], lrmax[N];
int pos, tot, c;
queue <int> rubbish;
char op, opp, oppp;
//为了节省空间,我们要将删去的点的空间用来存新的点
void clean_rubbish(int now) {//初始化数值
size[now] = yj[now] = turn[now] = ls[now] = rs[now] = 0;
val[now] = lmax[now] = rmax[now] = sum[now] = lrmax[now] = 0;
change[now] = INF;
}
int find_rubbish() {
if (!rubbish.empty()) {//用队列存因为删去而可以放的位置
int re = rubbish.front();
rubbish.pop();
clean_rubbish(re);
return re;
}
return ++TOT;//否则新开位置
}
int new_point(int num) {//开一个新的点
int re = find_rubbish();
ls[re] = rs[re] = 0;
val[re] = sum[re] = lrmax[re] = num;
lmax[re] = rmax[re] = max(0, num);
yj[re] = rand();
change[re] = INF;
size[re] = 1;
return re;
}
void up(int now) {//信息上传
size[now] = size[ls[now]] + size[rs[now]] + 1;
sum[now] = sum[ls[now]] + sum[rs[now]] + val[now];
lmax[now] = max(lmax[ls[now]], max(0, sum[ls[now]] + val[now] + lmax[rs[now]]));
rmax[now] = max(rmax[rs[now]], max(0, sum[rs[now]] + val[now] + rmax[ls[now]]));
lrmax[now] = rmax[ls[now]] + val[now] + lmax[rs[now]];//这里子串不能是空的,所以不能和 0 取 max
if (ls[now]) lrmax[now] = max(lrmax[now], lrmax[ls[now]]);
if (rs[now]) lrmax[now] = max(lrmax[now], lrmax[rs[now]]);
}
//下放懒标记
void down_turn(int now) {//序列翻转的懒标记
swap(ls[now], rs[now]);
swap(lmax[now], rmax[now]);//记得翻转的话前缀最大和后缀最大就要交换
turn[now] ^= 1;
}
void down_change(int now, int num) {//序列改值的懒标记
val[now] = num;
change[now] = num;
sum[now] = size[now] * num;
lmax[now] = rmax[now] = max(0, sum[now]);
lrmax[now] = max(val[now], sum[now]);//还是老规矩,不是是空的
}
void down(int now) {
if (turn[now]) {
if (ls[now]) down_turn(ls[now]);
if (rs[now]) down_turn(rs[now]);
turn[now] ^= 1;
}
if (change[now] != INF) {
if (ls[now]) down_change(ls[now], change[now]);
if (rs[now]) down_change(rs[now], change[now]);
change[now] = INF;
}
}
int build(int num) {
sta[0] = 0;
int now = new_point(a[1]);
sta[++sta[0]] = now;
for (int i = 2; i <= num; i++) {
int lst = 0;
now = new_point(a[i]);
while (sta[0] && yj[now] < yj[sta[sta[0]]]) {//用一个栈记录最右的链,维护这个链满足堆性质
up(sta[sta[0]]);//记得要上传标记
lst = sta[sta[0]];//这些比它大的到时都要放到它的左边
sta[sta[0]--] = 0;
}
ls[now] = lst;//放到左边
up(now);
if (sta[0]) {//它跟上面的连
rs[sta[sta[0]]] = now;
up(sta[sta[0]]);
}
sta[++sta[0]] = now;//它就放进了这个最右的链中
}
int lst = sta[sta[0]];
while (sta[0]) {
up(sta[sta[0]]);
lst = sta[sta[0]];
sta[sta[0]--] = 0;
}
return lst;
}
pair <int, int> split_rnk(int now, int rnk) {//将这个树中前 rnk 小的分离出来
if (!now) return make_pair(0, 0);
if (!rnk) return make_pair(0, now);
down(now);
pair <int, int> re;
if (size[ls[now]] >= rnk) {//它的右子树都会放在右边
re = split_rnk(ls[now], rnk);//继续分割左边的树
ls[now] = re.second;//左边的树右边的部分就接到了右边树的左儿子处
up(now);
re.second = now;
}
else {//它的左子树都会放在在左边
re = split_rnk(rs[now], rnk - size[ls[now]] - 1);//继续分割右边的数(记得排名要减去左子树大小和本身点大小)
rs[now] = re.first;//右边的树左边的部分就街道了左边树的右儿子处
up(now);
re.first = now;
}
return re;
}
int merge(int x, int y) {//合并两个 Treap(不用想 Splay 那样要一个大于另一个)
if (x) down(x);
if (y) down(y);
if (!x) return y;
if (!y) return x;
if (yj[x] < yj[y]) {//左边的优先级大,它应放到上面
rs[x] = merge(rs[x], y);//左边的右儿子跟右边合并
up(x);
return x;
}
else {//右边的优先级大,它应放到上面
ls[y] = merge(x, ls[y]);//左边跟右边的左儿子合并
up(y);
return y;
}
}
void insert(int pos, int tot) {//插入数组,就这些数组构成一个树,然后把原来的树按着要求的位置分离成两个,然后三个按顺序合并(新的放在中间)
int nroot = build(tot);
pair <int, int> x = split_rnk(root, pos);
root = merge(merge(x.first, nroot), x.second);
}
void become_rubbish(int now) {//把点删去的时候记录下那些位置是被删去可以重新用的
if (!now) return ;
if (ls[now]) become_rubbish(ls[now]);
rubbish.push(now);
if (rs[now]) become_rubbish(rs[now]);
}
void delete_(int pos, int tot) {//类似插入的操作,把树分离乘三个部分(就干题目给的),中间是要删的,就记录下被删去的位置,然后把剩下左右两个合并
pair <int, int> x = split_rnk(root, pos - 1);
pair <int, int> y = split_rnk(x.second, tot);
become_rubbish(y.first);
root = merge(x.first, y.second);
}
void change_(int pos, int tot, int c) {//类似前面一样把这个区间分离出来,打上要改值的标记,然后改一下这个点的值,再重新合并回去
pair <int, int> x = split_rnk(root, pos - 1);
pair <int, int> y = split_rnk(x.second, tot);
val[y.first] = c;
change[y.first] = c;
sum[y.first] = size[y.first] * c;
lmax[y.first] = rmax[y.first] = max(0, sum[y.first]);
lrmax[y.first] = max(val[y.first], sum[y.first]);
turn[y.first] = 0;
root = merge(x.first, merge(y.first, y.second));
}
void turn_(int pos, int tot) {//跟上面类似,都是分离,打标记,翻转,合并
pair <int, int> x = split_rnk(root, pos - 1);
pair <int, int> y = split_rnk(x.second, tot);
turn[y.first] ^= 1;
swap(lmax[y.first], rmax[y.first]);
swap(ls[y.first], rs[y.first]);
root = merge(x.first, merge(y.first, y.second));
}
int get_sum(int pos, int tot) {//分离,直接用我们维护的 sum 数组,合并
pair <int, int> x = split_rnk(root, pos - 1);
pair <int, int> y = split_rnk(x.second, tot);
int re = sum[y.first];
root = merge(x.first, merge(y.first, y.second));
return re;
}
int main() {
srand(19491001);
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
root = build(n);
while (m--) {
op = getchar();
while (op != 'I' && op != 'D' && op != 'M' && op != 'R' && op != 'G') op = getchar();
opp = getchar();
oppp = getchar();
if (op == 'I') {
for (int i = 1; i <= 3; i++) getchar();
scanf("%d %d", &pos, &tot);
for (int i = 1; i <= tot; i++) scanf("%d", &a[i]);
insert(pos, tot);
continue;
}
if (op == 'D') {
for (int i = 1; i <= 3; i++) getchar();
scanf("%d %d", &pos, &tot);
delete_(pos, tot);
continue;
}
if (op == 'M' && oppp == 'K') {
for (int i = 1; i <= 6; i++) getchar();
scanf("%d %d %d", &pos, &tot, &c);
change_(pos, tot, c);
continue;
}
if (op == 'R') {
for (int i = 1; i <= 4; i++) getchar();
scanf("%d %d", &pos, &tot);
turn_(pos, tot);
continue;
}
if (op == 'G') {
for (int i = 1; i <= 4; i++) getchar();
scanf("%d %d", &pos, &tot);
printf("%d\n", get_sum(pos, tot));
continue;
}
if (op == 'M' && oppp == 'X') {
for (int i = 1; i <= 4; i++) getchar();
printf("%d\n", lrmax[root]);
continue;
}
}
return 0;
}