平衡树写题记录
练思维的同时还是顺便点一下科技树吧。。。顺便fhq-treap好写好用,再也不用Splay了,反正不学LCT了。
这下面是实现大多数功能的fhq-treap板子
1 mt19937 rnd(time(0)); 2 struct fhq_treap { 3 int size[maxn],ls[maxn],rs[maxn],val[maxn],tot,wei[maxn],root; 4 fhq_treap() {tot = 0;} 5 int New(int v) { 6 size[++tot] = 1; 7 val[tot] = v; 8 wei[tot] = rnd(); 9 return tot; 10 } 11 void pushup(int p) {size[p] = size[ls[p]] + size[rs[p]] + 1;} 12 void Split(int p, int v, int &x, int &y) { 13 if(!p) {x = 0; y = 0; return;} 14 if(val[p] <= v){x = p; Split(rs[p], v, rs[p], y);} 15 else {y = p; Split(ls[p], v, x, ls[p]);} 16 pushup(p); 17 } 18 int Merge(int x, int y) { 19 if((!x) || (!y)) return x + y; 20 if(wei[x] < wei[y]) { 21 ls[y] = Merge(x, ls[y]); 22 pushup(y); 23 return y; 24 } 25 else { 26 rs[x] = Merge(rs[x], y); 27 pushup(x); 28 return x; 29 } 30 } 31 void insert(int v) { 32 int x,y; 33 Split(root, v - 1, x, y); 34 root = Merge(Merge(x, New(v)), y); 35 } 36 void del(int v) { 37 int x,y,z; 38 Split(root, v, x, z); 39 Split(x, v - 1, x, y); 40 root = Merge(Merge(x, Merge(ls[y], rs[y])), z); 41 } 42 int find(int p, int kth) { 43 if(size[ls[p]] + 1 == kth) return p; 44 if(kth < size[ls[p]] + 1) return find(ls[p], kth);// 又写反了。。 45 return find(rs[p], kth - size[ls[p]] - 1); 46 } 47 int rnk(int v) { 48 int x,y,ret; 49 Split(root, v - 1, x, y); 50 ret = size[x] + 1; 51 root = Merge(x, y); 52 return ret; 53 } 54 int pre(int v) { 55 int x,y,ret; 56 Split(root, v - 1, x, y); 57 ret = val[find(x, size[x])]; 58 root = Merge(x, y); 59 return ret; 60 } 61 int nxt(int v) { 62 int x,y,ret; 63 Split(root, v, x, y); 64 ret = val[find(y, 1)]; 65 root = Merge(x, y); 66 return ret; 67 } 68 }t;
嗯好下面是实现区间树的fhq-treap。
1 int build(int l, int r) {// 十分简约 a数组存你要整来建树的一段序列 2 if(l != r) { 3 int mid = (l + r) >> 1; 4 return merge(build(l, mid), build(mid + 1, r)); 5 } 6 return New(a[l]); 7 } 8 // 注意有标记下传的话要在split的开头下传(split常用排名分裂),merge的话哪棵树作为主树就传哪棵就好了 9 // fhq-treap区间操作的原理,就是把原本树裂开成[1,l-1],[l,r],[r+1,n]三个区间,按排名分裂,最后对中间区间进行操作再merge回去就好了 10 split(root, l - 1, x, y); 11 split(y, r - l + 1, y, z);// 注意因为左边的树没了,要写成r-l+1 12 // 这里对y树进行操作 13 root = merge(x, merge(y, z));
HNOI2002 营业额统计
链接 挺傻的一个题,set都能水过去,写个查前驱后继的平衡树就可以了,记得判重复
JLOI2011 不等式组
链接 不等式基本变换一下就知道是个平衡树查排名的题了,分类讨论一下a,然后改成double类型即可,只是这题细节有点多需要注意。
TJOI2007 书架
链接 还是比较简单的,考虑像map那样每个节点开两个值:字符串、排名。然后按排名为关键字排序就可以了。插入的时候按v-1分裂,像线段树那样给大一点的那棵树打上区间加标记,分裂合并时下传标记就可以了,查询跟rnk查值一样的,写个find就可以了。(其实这题可以按排名分裂做的,woshilaji。。。)
NOI2003 文本编辑器
链接 不得不说这道题还是挺水的(平衡树被块状链表吊打了,555)。这道题水是水,但是蕴含了不少平衡树维护序列的思想:插入一段的时候可以考虑$O(logn)$建树整段插入,删除直接暴力删没啥好说的(不像毒瘤NOI2005 维护数列一样还要回收编号防止MLE),GET就是把子树列出来中序遍历就行了,区间式平衡树主要是维护了中序遍历不变的性质。此外这道题读入有一点坑,然后细节就没了,move啥一堆乱七八糟的操作开个变量维护当前光标位置即可。
1 // Insert操作:大力掰开,建一颗树,插入
2 // Delete操作:大力掰开,删掉,合并回去
3 // get操作:大力掰开,中序遍历
4 // 剩下的几个操作拿一个变量维护光标位置就好了
5 #include<cstring>
6 #include<cstdio>
7 #include<random>
8 #include<ctime>
9 using namespace std;
10 const int maxn = 2100005;
11 int m,x,cnt,gb;// gb维护光标
12 char op[8],txt[maxn],ch;
13 mt19937 rnd(time(0));
14 struct fhq_treap {
15 int size[maxn],ls[maxn],rs[maxn],wei[maxn],tot,root;
16 char val[maxn];
17 int New(char c) {
18 val[++tot] = c;
19 size[tot] = 1;
20 wei[tot] = rnd();
21 return tot;
22 }
23 void pushup(int p) {size[p] = size[ls[p]] + size[rs[p]] + 1;}
24 void split(int p, int k, int &x, int &y) {// 按排名分裂
25 if(!p) {x = 0; y = 0; return;}
26 if(size[ls[p]] + 1 <= k) {x = p; split(rs[p], k - size[ls[p]] - 1, rs[p], y);}
27 else {y = p; split(ls[p], k, x, ls[p]);}
28 pushup(p);
29 }
30 int merge(int x, int y) {
31 if(!x || !y) return x + y;
32 if(wei[x] < wei[y]) {
33 rs[x] = merge(rs[x], y);
34 pushup(x);
35 return x;
36 }
37 else {
38 ls[y] = merge(x, ls[y]);
39 pushup(y);
40 return y;
41 }
42 }
43 int build(int l, int r) {
44 if(l != r) {
45 int mid = (l + r) >> 1;
46 return merge(build(l, mid), build(mid + 1, r));
47 }
48 return New(txt[l]);
49 }
50 void bl(int p) {// 遍历
51 if(ls[p]) bl(ls[p]);
52 putchar(val[p]);
53 if(rs[p]) bl(rs[p]);
54 }
55 void insert(int p) {
56 int x,y;
57 split(root, p, x, y);// 观察样例不难得知是从p + 1位置开始插入
58 root = merge(merge(x, build(1, cnt)), y);
59 }
60 void del(int l, int r) {// 删除区间[l,r]
61 int x,y,z;
62 split(root, l - 1, x, y);
63 split(y, r - l + 1, y, z);
64 root = merge(x, z);
65 }
66 void get(int l, int r) {// 获取区间[l,r]字符
67 int x,y,z;
68 split(root, l - 1, x, y);
69 split(y, r - l + 1, y, z);
70 bl(y);
71 root = merge(merge(x, y), z);
72 putchar('\n');
73 }
74 }t;
75 int main() {
76 scanf("%d", &m);
77 for(int i = 1; i <= m; ++i) {
78 scanf("%s", op);
79 if(op[0] == 'N') {
80 ++gb;
81 continue;
82 }
83 if(op[0] == 'P') {
84 --gb;
85 continue;
86 }
87 scanf("%d", &x);
88 if(op[0] == 'M') gb = x;
89 if(op[0] == 'D') t.del(gb + 1, gb + x);
90 if(op[0] == 'I') {
91 cnt = 0;
92 while(1) {
93 ch = getchar();
94 if(ch >= 32 && ch <= 126) txt[++cnt] = ch;
95 if(cnt == x) break;
96 }
97 t.insert(gb);
98 }
99 if(op[0] == 'G') t.get(gb + 1, gb + x);
100 }
101 return 0;
102 }
TJOI2019 甲苯先生的滚榜
链接 还是个水题(2019还有平衡树裸题?)。但是关键是这道题卡fhq_treap的常数!而且生成数据方式必须按题目给的unsigned int不然要没。本题可以拿一个结构体重载,按AC、罚时数重载运算符。记得重载<=的时候罚时是返回>=,一开始写错了搞了半天。每个AC记录就把原来的人删除、然后再插入数据更新后的那个人,查询排名即可。但这题坑点有点多。
1.内存问题:
虽然这题开了1G,但是1e6的数据平衡树也会爆(还要算递归啥的)。考虑采用回收编号的方式,就是开个栈存下已经删除的节点编号,新建节点函数内不能直接调用++tot,如果栈里面有元素要取出栈顶的元素。但是这题有个特性:删除一个数后马上插入一个数。因此我们就只用一个变量维护上次删掉的节点编号就可以了。
2.时间问题
就算这题开了10s但是fhq_treap也逃不脱被卡常的命运啊。但是我们可以通过但不限于以下方式进行优化(register、inline没啥用):
·最基本的输入/输出优化
·memset时候我们采用sizeof(person) * (m + 1)(m为人数,person为存人ac、罚时的结构体)加速
·一开始不要一个一个插入,直接采用$O(logn)$建树
·(关键 能快1.5s左右)每次删除后插入新元素时,我们实际上就可以直接获取当前节点的排名了,插入查询二合一,常数变成2/3倍
·pushup更新子树大小展开卡常,减少栈空间使用
NOI 2005 数列
链接 很裸但细节很多、算神仙又不算神仙的数据结构题。要求实现如下操作:
维护一个数列,然后
1.插入一段数
2.删除一段数
3.数列区间覆盖
4.数列区间翻转(很显然的平衡树了)
5.数列区间求和
6.数列整体最大子段和
每个操作单独拿出来都不难,但是合在一起就变成了毒瘤中的毒瘤。。。接下来就把这道题的易错细节一一列出:
1.受GSS系列的启发,我们维护最大子段和考虑采用维护最大前缀和pre、后缀和suf、子段和mx的形式。需要注意的是这里最大子段和不能不取,但是我们往上更新的时候有的子段又不能取,因此我们可以让pre、suf与0取最大值,反正最后子段更新时还有根节点的值,一定有方法保证子段不为空
1.内存问题:开4e6数组肯定爆了(但是好像有人过了?)所以就要用到上一题提到的回收编号机制,开一个栈存删除了的节点编号,删除一段数的时候递归删除,编号入栈即可。
2.插入一段数采用老套路,$O(logn)$建树可以搞过去
3.对于区间覆盖和翻转操作,我们不要只打个标记就溜了,应该在打标记的同时直接交换左右子树/更新子树和、结点值(因为这题要求最大子段和,维护信息过多,必须当即更新),同时区间覆盖的时候判断覆盖值的正负,如果大于0就让pre、suf、mx全部更新为区间和,否则suf、pre更新为0,mx更新为覆盖值即可。同时,区间覆盖值有可能是0,我们要打标记判断区间覆盖与否,不能简单地判断覆盖值是否为0.初始化为无穷又过于麻烦。然后,区间翻转会使得最大前缀和pre和最大后缀和suf进行交换。
4.新建结点的时候就要把pre与suf和0取最大值
然后就over了,同时还有双倍经验,甚至就多了个输出第k个数
APIO2015 八邻旁之桥
链接 至少不是个裸题了。首先预处理一下,要过桥的先把答案+1,不过桥的答案加上|si - ti|。观察到k<=2,显然是个分类讨论的题目。其实我们并不关心一个人是从哪里出发、到哪里(也就是行进方向),我们只关心每一对出发点、终点的相对位置。k=1的时候,我们就要最小化这个式子:$\sum_{a[i] \in A} |a[i] - X|$。而这就是一个经典结论了:X应该是A序列的中位数。简略证明如下:
我们A序列中假设小于X的有P个元素,大于X的有Q个元素,若P<Q,则X每加一,比X大的元素到X距离都少1,比X小的元素到X距离都多1,总计减少了Q-P,反之,如果P>Q,X每向左移动一个单位,答案减少P-Q。为达到答案最小值处,X应该选A序列中的中位数。
所以k=1的时候找序列个中位数瞎搞搞就可以了,由于上面我们提到的不用关心每个人的行进方向,所以直接把s、t数组揉在一起即可。
k=2的时候,我们要面临的问题就是每个人到底要选哪个桥。我们不妨设si < ti,设一个函数$f(x) = |x - si| + |x - ti|$,然后观察到有绝对值,直接暴力分类讨论,得到:
$$ \begin{equation} f(x)= \begin{cases} a_i + b_i - 2x & \mbox{x < $s_i$} \\ b_i - a_i & \mbox{$s_i$ <= x <= $t_i$} \\ 2x - a_i - b_i & \mbox{x > $t_i$ } \end{cases} \end{equation} $$
因而我们知道,桥修在$s_i$和$t_i$之间对于第i个人的贡献是没有区别的,此外,我们希望第i个人选的桥位于$x_i$使得$f(x_i)$尽可能地小,则$x_i$越靠近这个函数的中间段也就越好,同时意味着越靠近函数对称轴$\frac{s_i + t_i}{2}$越好,因为函数在左边段单调递减,右边段单调递增。所以第i个人选的桥仅与$\frac{s_i + t_i}{2}$有关,我们所有人按这个值排个序(同边的不计入统计),枚举断点i(第i个元素,t数组和s数组糅合而成的),断点i左边的元素选第一个桥,断点i右边的元素选第二个桥,这两个桥分别的位置显然就是两个区域内的中位数(不同于一般定义,假设有n个元素,如果n为偶数,中位数为排名第$\frac{n}{2}$的元素),统计一下两个区域内所有元素与中位数之差的绝对值之和。
至于统计,我们知道小于中位数mid的数对答案贡献了$$\sum_{i=1}^{\frac{n}{2}} mid - a_i$$而大于等于中位数的数贡献了 $$\sum_{i=\frac{n}{2}+1}^{n}a_i - mid$$然后发现其实如果能把数列分成前后两半是不用管中位数到底是谁的,反正已经消掉了,fhq_treap就可以干这个事(虽然对顶堆也可以):动态插入、动态维护序列中位数、序列求和。然后再写个按排名分裂就可以做到不用查中位数把数列直接分成两半。
最后会有一个桥都不用修的情况,特判一下直接输出就好了。
HNOI 2012 永无乡
链接 一眼用并查集维护,还是好看出来的,然后就是启发式合并 + 排名查询就可以了。此题不能直接用fhq_treap的merge,这个想法很容易被hack掉,所以我们要判断子树大小进行启发式合并,把小树中的每个节点抽出来插入大树之中即可。最后,此题数据有锅,最后一个点会合并 0 0,要特判一下。
NOIP2017 列队
链接 这个题用树状数组确实很难想,但是如果我们用fhq_treap就可以大大降低思维量。首先我们已知nm都很大,但是q是与n、m同数量级的。也就是说,有许多点我们是完全用不到的,我们可以考虑用平衡树维护每行(除了该行最后一列)和最后一列,一开始所有点并成一长条,我们维护每条开头的实际编号,由于每条中是连续(最后一列虽然不连续,但是形成等差数列)的,剩下点的编号都可以推导出。我们就裂出两个点,进行重新合并即可,但是细节很多,建议写。
JSOI2008 火星人
链接 带修改的题目一定考虑不带修改怎么做,如果整过字符串哈希实现后缀数组的应该知道,两个后缀LCP(即题目中的LCQ)可以用哈希+二分答案实现(假设不知道,我们观察到题目对询问次数做了限制,就可以猜测询问操作的单次时间复杂度较高,实际上也如此,要二分,会多一个log)。这题除了修改还有插入,线段树略逊于平衡树(也可以一开始就开好,插入就在末尾修改,但是就比平衡树慢了)。为什么能想到这俩数据结构呢,因为字符串哈希这个东西是满足结合律的,带修改、区间查哈希值容易做到。