P2042 [NOI2005] 维护数列 题解
一道奆数据结构题,需要有较高的码力和基础的数据结构。
一看过去就会发现这是道数据结构题,然后这道题实际上就是平衡树的板子题只是有各种奇怪的操作而已。我用的是 FHQ Treap。
其实吧这道题本来我不打算写题解的,毕竟还是比较显然的数据结构题,但是这道题的众多坑点让我还是决定写篇题解,本篇题解采用拆解代码的方式贴代码,最后会有我对这道题用 FHQ Treap 写这道题的坑点总结。
分析一下这道题,会发现实际上就是插入,删除,反转,推平,区间查询和,全局查询最大子段和。
前置知识:线段树求最大子段和,例题是 GSS1。
好的现在我认为你应该会了这个 trick。
这道题我们可以仿照线段树求最大子段和的方式,在每个节点维护以下 11 个值:l,r
左右儿子,Size
子树大小,val
当前这个节点的权值,Key
就是随机的值,pre,aft
表示前后缀最大和,sum
表示区间和,Maxn
表示最大子段和。然后还要维护 flag
表示推平懒标记,rev
表示翻转懒标记。
于是你会发现如果直接开 \(4 \times 10^6\) 是会炸空间的,因此这里我们需要垃圾回收,就是删除节点的时候将不用的节点记录下来以便重复使用,这块的总复杂度是 \(O(点数)\) 的。
首先贴一下结构体:
const int MAXN = 5e5 + 5;
int n, q, Root, a[MAXN], cnt_Node;
struct node
{
int l, r, Size, val, Key;
int pre, aft, sum, Maxn;
bool flag, rev;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define Size(p) tree[p].Size
#define val(p) tree[p].val
#define Key(p) tree[p].Key
#define pre(p) tree[p].pre
#define aft(p) tree[p].aft
#define sum(p) tree[p].sum
#define Maxn(p) tree[p].Maxn
#define flag(p) tree[p].flag
#define rev(p) tree[p].rev
}tree[MAXN];
stack <int> Rub;
Root
是根,cnt_Node
是当前树的节点个数(不含删除,就是拿来开点用的),Rub
是垃圾回收用的。
然后这道题有一个很大的坑点就是 最大子段和 不能为空,也就是说你必须选一个,因此我们需要考虑对 pre,aft,Maxn
做一点手脚:
在 Update(Pushup) 和 新建节点(Make_Node) 的时候,由于所有区间至少要选一个,所以一开始规定 Maxn = val
但是 pre
和 aft
是可以为 0 的(因为你已经选了一个了),然后按照正常的做法更新 Maxn
,注意这里的 Maxn
是绝对不能和 0 取大的!
还有一点需要注意的是普通线段树写法 Update(Pushup) 的时候节点本身是没有权值的,但是 FHQ Treap 里面是有的,因此不能忘记把这个节点合并进去。
贴一下 Update
函数:
void Update(int p)
{
if (!p) return ;
Size(p) = Size(l(p)) + Size(r(p)) + 1; sum(p) = sum(l(p)) + sum(r(p)) + val(p);
pre(p) = Max(pre(l(p)), Max(sum(l(p)) + val(p) + pre(r(p)), 0));
aft(p) = Max(aft(r(p)), Max(sum(r(p)) + val(p) + aft(l(p)), 0));
Maxn(p) = Max(val(p), aft(l(p)) + val(p) + pre(r(p)));
if (l(p)) Maxn(p) = Max(Maxn(p), Maxn(l(p)));
if (r(p)) Maxn(p) = Max(Maxn(p), Maxn(r(p)));
} // 就是 Pushup
然后是新建节点 Make_Node
函数:
int Make_Node(int val)
{
int tmp = 0; if (Rub.empty()) tmp = ++cnt_Node; else { tmp = Rub.top(); Rub.pop(); } // 重复利用
l(tmp) = r(tmp) = 0; val(tmp) = val; Size(tmp) = 1;
sum(tmp) = Maxn(tmp) = val; pre(tmp) = aft(tmp) = Max(val, 0);
flag(tmp) = rev(tmp) = 0; Key(tmp) = rand();
return tmp;
} // 新建节点
还有一个下传懒标记的 Spread(Pushdown) 函数,这块会顺便带上打翻转懒标记和推平懒标记的两个函数 Reverse
和 Cover
。
Reverse
就是正常的翻转,考虑到 FHQ Treap 的中序遍历就是原序列,直接交换左右子树就好了,往下打懒标记。
这里需要注意两点:
- 翻转的时候子树不能直接打懒标记,是需要看情况的,因为翻转两次就是没有翻转。
- 注意翻转的同时前后缀也被翻转了,因此也是需要交换的。
然后是 Cover
,这个函数往下推平的时候需要注意推平的值就是这个点的 val
,以及儿子的 Maxn
必须要选一个,pre,aft
可选可不选。
写了这两个函数就能写 Spread
了,这三个函数代码如下:
void Cover(int p, int val)
{
if (!p) return ;
val(p) = val; sum(p) = Size(p) * val;
pre(p) = aft(p) = ((val > 0) ? sum(p) : 0);
Maxn(p) = (val > 0) ? sum(p) : val; flag(p) = 1;
} // 区间推平
void Reverse(int p)
{
if (!p) return ;
std::swap(l(p), r(p));
std::swap(pre(p), aft(p));
rev(p) ^= 1; // 注意不能直接赋值为 1
} // 区间反转
void Spread(int p)
{
if (!p) return ;
if (flag(p))
{
if (l(p)) Cover(l(p), val(p));
if (r(p)) Cover(r(p), val(p));
flag(p) = 0;
}
if (rev(p))
{
if (l(p)) Reverse(l(p));
if (r(p)) Reverse(r(p));
rev(p) = 0;
}
} // 就是 Pushdown
然后这道题有一个全局的坑点,好像有两组数据是有的,就是操作的时候有时操作的区间长度是为 0 的,这个点也会坑到很多人,因此以上所有函数都需要在最开始加一个判断节点是否为空。
好的现在有了以上的基础函数,可以开始写各类我们需要的函数了。
首先看 Split
函数,这里的函数按照大小分裂即可。
然后是 Merge
函数,这块的话就是正常 Merge
,但是当你往下合并的时候哪棵树要往下合并哪棵树就需要 Spread
。
void Split(int now, int val, int &x, int &y)
{
if (now == 0) { x = y = 0; return ; }
Spread(now);
if (Size(l(now)) + 1 <= val) { x = now; Split(r(now), val - Size(l(now)) - 1, r(now), y); }
else { y = now; Split(l(now), val, x, l(now)); }
Update(now);
}
int Merge(int x, int y)
{
if (!x || !y) return x + y;
if (Key(x) < Key(y))
{
Spread(x); r(x) = Merge(r(x), y);
Update(x); return x;
}
else
{
Spread(y); l(y) = Merge(x, l(y));
Update(y); return y;
}
}
然后是插入序列 Insert
函数,但是首先我们需要一个 Build
函数来建树。
这个建树就是仿照线段树二分递归建树,Insert
函数应该是基操了,就是将前面 pos
个拿出来然后三棵树合并。
但显然这里也有坑点,先看代码:
int Build(int l, int r)
{
if (l == r) return Make_Node(a[l]);
int mid = (l + r) >> 1;
int x = Build(l, mid);
int y = Build(mid + 1, r);
return Merge(x, y); // 注意这三句话
} // 递归建树
void Insert()
{
int pos = Read(), len = Read();
for (int i = 1; i <= len; ++i) a[i] = Read();
int x, y; Split(Root, pos, x, y);
Root = Merge(Merge(x, Build(1, len)), y);
}
看见上面打注释的三句话了吗?如果你直接写成 return Merge(Build(l, mid), Build(mid + 1, r));
,可能程序会先执行后面的 Build(mid + 1, r)
,这样子你可能就会挂掉了。
加可能的原因是有些人这样写是不会挂掉的(比如我这份代码),但是有些人会。
然后是 Delete
函数,就是正常的把需要的区间拉出来直接毙了,这里会加上垃圾回收函数:
void Recycle(int p)
{
Rub.push(p);
if (l(p)) Recycle(l(p));
if (r(p)) Recycle(r(p));
} // 垃圾回收
void Delete()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Root = Merge(x, z); Recycle(y);
}
接下来看区间翻转和区间推平操作,同样也是将区间拉出来操作:
void Change_Cover()
{
int pos = Read(), len = Read(), val = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Cover(y, val);
Root = Merge(Merge(x, y), z);
}
void Change_Reverse()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Reverse(y);
Root = Merge(Merge(x, y), z);
}
最后就是两个查询操作了,不用我多说了吧:
void Ask_sum()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); printf("%d\n", sum(y));
Root = Merge(Merge(x, y), z);
}
void Ask_Maxn()
{
printf("%d\n", Maxn(Root));
}
接下来总结一下这道题的坑点所在:
- 由于最大子段和不能为空,因此
pre,aft,Maxn
需要细节操作。 - 由于 FHQ Treap 本身的节点也是有权值的,因此也要合并进去。
Reverse
操作的时候不能忘记交换前后缀。- 每次操作之前一定要看一眼节点是不是空的。
- 插入的时候需要先建树再合并。
- 因为空间限制较小,需要垃圾回收。
以上就是我遇到的所有坑点,如果还有的话就需要自己总结了。
Github:CodeBase-of-Plozia。
Code:
/*
========= Plozia =========
Author:Plozia
Problem:P2042 [NOI2005] 维护数列
Date:2021/12/28
========= Plozia =========
*/
#include <bits/stdc++.h>
using std::stack;
using std::string;
typedef long long LL;
const int MAXN = 5e5 + 5;
int n, q, Root, a[MAXN], cnt_Node;
struct node
{
int l, r, Size, val, Key;
int pre, aft, sum, Maxn;
bool flag, rev;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define Size(p) tree[p].Size
#define val(p) tree[p].val
#define Key(p) tree[p].Key
#define pre(p) tree[p].pre
#define aft(p) tree[p].aft
#define sum(p) tree[p].sum
#define Maxn(p) tree[p].Maxn
#define flag(p) tree[p].flag
#define rev(p) tree[p].rev
}tree[MAXN];
stack <int> Rub;
int Read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = sum * 10 + (ch ^ 48);
return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }
int Make_Node(int val)
{
int tmp = 0; if (Rub.empty()) tmp = ++cnt_Node; else { tmp = Rub.top(); Rub.pop(); }
l(tmp) = r(tmp) = 0; val(tmp) = val; Size(tmp) = 1;
sum(tmp) = Maxn(tmp) = val; pre(tmp) = aft(tmp) = Max(val, 0);
flag(tmp) = rev(tmp) = 0; Key(tmp) = rand();
return tmp;
} // 新建节点
void Cover(int p, int val)
{
if (!p) return ;
val(p) = val; sum(p) = Size(p) * val;
pre(p) = aft(p) = ((val > 0) ? sum(p) : 0);
Maxn(p) = (val > 0) ? sum(p) : val; flag(p) = 1;
} // 区间推平
void Reverse(int p)
{
if (!p) return ;
std::swap(l(p), r(p));
std::swap(pre(p), aft(p));
rev(p) ^= 1;
} // 区间反转
void Update(int p)
{
if (!p) return ;
Size(p) = Size(l(p)) + Size(r(p)) + 1; sum(p) = sum(l(p)) + sum(r(p)) + val(p);
pre(p) = Max(pre(l(p)), Max(sum(l(p)) + val(p) + pre(r(p)), 0));
aft(p) = Max(aft(r(p)), Max(sum(r(p)) + val(p) + aft(l(p)), 0));
Maxn(p) = Max(val(p), aft(l(p)) + val(p) + pre(r(p)));
if (l(p)) Maxn(p) = Max(Maxn(p), Maxn(l(p)));
if (r(p)) Maxn(p) = Max(Maxn(p), Maxn(r(p)));
} // 就是 Pushup
void Spread(int p)
{
if (!p) return ;
if (flag(p))
{
if (l(p)) Cover(l(p), val(p));
if (r(p)) Cover(r(p), val(p));
flag(p) = 0;
}
if (rev(p))
{
if (l(p)) Reverse(l(p));
if (r(p)) Reverse(r(p));
rev(p) = 0;
}
} // 就是 Pushdown
void Split(int now, int val, int &x, int &y)
{
if (now == 0) { x = y = 0; return ; }
Spread(now);
if (Size(l(now)) + 1 <= val) { x = now; Split(r(now), val - Size(l(now)) - 1, r(now), y); }
else { y = now; Split(l(now), val, x, l(now)); }
Update(now);
}
int Merge(int x, int y)
{
if (!x || !y) return x + y;
if (Key(x) < Key(y))
{
Spread(x); r(x) = Merge(r(x), y);
Update(x); return x;
}
else
{
Spread(y); l(y) = Merge(x, l(y));
Update(y); return y;
}
}
int Build(int l, int r)
{
if (l == r) return Make_Node(a[l]);
int mid = (l + r) >> 1;
int x = Build(l, mid);
int y = Build(mid + 1, r);
return Merge(x, y);
} // 递归建树
void Recycle(int p)
{
Rub.push(p);
if (l(p)) Recycle(l(p));
if (r(p)) Recycle(r(p));
} // 垃圾回收
void Insert()
{
int pos = Read(), len = Read();
for (int i = 1; i <= len; ++i) a[i] = Read();
int x, y; Split(Root, pos, x, y);
Root = Merge(Merge(x, Build(1, len)), y);
}
void Delete()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Root = Merge(x, z); Recycle(y);
}
void Change_Cover()
{
int pos = Read(), len = Read(), val = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Cover(y, val);
Root = Merge(Merge(x, y), z);
}
void Change_Reverse()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Reverse(y);
Root = Merge(Merge(x, y), z);
}
void Ask_sum()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); printf("%d\n", sum(y));
Root = Merge(Merge(x, y), z);
}
void Ask_Maxn()
{
printf("%d\n", Maxn(Root));
}
int main()
{
n = Read(), q = Read(); srand(time(0));
for (int i = 1; i <= n; ++i) a[i] = Read();
Root = Build(1, n);
for (int i = 1; i <= q; ++i)
{
string str; std::cin >> str;
if (str == "INSERT") Insert();
if (str == "DELETE") Delete();
if (str == "MAKE-SAME") Change_Cover();
if (str == "REVERSE") Change_Reverse();
if (str == "GET-SUM") Ask_sum();
if (str == "MAX-SUM") Ask_Maxn();
}
return 0;
}