【ybt金牌导航4-4-2】【luogu P2042】维护数列(fhq Treap 做法)

维护数列

题目链接:ybt金牌导航4-4-2 / luogu P2042

题目大意

给你一个序列,要你支持一些操作。
往一个地方插入一段数,删去一段连续的数,将一段连续的数改成同一个值,将连续的一段数翻转,求出连续的一段数的和,求当前数列的最长子数列。

思路

这种题看着都知道是用平衡树来做。

看着也知道要维护很多的懒标记,非常的恶心。

这里讲的是无旋 Treap,即 fhq Treap 的做法。

无旋 Treap 的做法这里就不多说的,主要是来解决几个实现的问题。

  1. 它会插入一些数组,那一个一个点插时间复杂度太大了,会直接跑到 nlogn 级别。

我们会考虑把这个数组先建一个树,然后再用合并操作,分离出插入的位置,夹在中间合并。
但问题是你要如何块苏的建一个树呢?
那我们先看单点的插入,不难想到插入方法:
我们从根节点开始,不断的与当前点比较优先级,如果不优就往下走,找到第一个优的,然后它就变成这个位置的父亲的右儿子,这个位置就变成它的左儿子。

那可以看出它只会在右儿子上跑,那我们可以开一个栈维护。
每次即不停的从小往上看,更优就一直往上跳,跳到不优的前一个。然后就连,然后把不优的都弹出,放入你现在的。
那每个点最多入栈出栈一次,时间复杂度啊就是 O(n) 的了。

  1. 翻转

有人会想到文艺平衡树,没错,大概也是同样的方法,用懒标记。

  1. 求和最大的子序列

做线段树的时候就有这种题,大概做法是维护前缀最大和后缀最大,通过这两个来维护这个求和最大。
而且你发现这个东西翻转的时候求和最大不变,但是前缀最大和后缀最大是也要交换的。
然后再加值啊,换值的时候都要重新维护。
总的来说就是多 up(now),多 down(now)

  1. 区间赋值

不难想到是用懒标记,而且用的时候还可以把翻转的懒标记去掉。
(不过赋值完都是一样的,翻转不翻转好像都无所谓了)

  1. 关于空间

由于你要维护很多东西,而且操作次数达到了 4×106 你开那么多数组会 MLE。
但你你会发现它任何时刻内,数组内最多有 5×105 个数,那也就是说,有很多的数会被删去。
那你这些数删去它们就不存在了,那它们原本占用的位置你可以重新利用。
那你可以搞一个队列记录被删去,位置还没有被重新用的数的位置,然后你要加点的时候,如果队列空了就新开空间放,否则就用队列里面的位置,然后把这个位置从队列中弹出。

然后就是痛苦的具体实现了,具体可以看看代码。

代码

#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; }

__EOF__

本文作者あおいSakura
本文链接https://www.cnblogs.com/Sakura-TJH/p/YBT_JPDH_4-4-2.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   あおいSakura  阅读(61)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示