【学习笔记】Dynamic Rankings

NCC79601

2019-03-16 03:17:21

Personal

题目[P2617](https://www.luogu.org/problemnew/solution/P2617) 顾名思义,$Dynamic$ $Rankings$是指一种可以**动态查询区间第$k$小**的数据结构。这里可以用树状数组$+$线段树$+$主席树来维护,具体为用树状数组来保存线段树历史版本的前缀和,然后用主席树思想查询区间第$k$小值。这差不多都能算作**树套树套树**了,如果可以很快打出来,代码能力会提高不少。 # 动态建树 这里由于需要将输入数据**离散化**,所以是先读入完了再建树。建线段树的过程当中也可以顺便把树状数组给更新了,比如这里就表示当前以$x$为根节点新建一个子树,作为线段树的一个历史版本,并用树状数组$C[i]$维护区间节点个数信息。由于树状数组维护前缀和非常方便,查询起来也非常快,所以这里选择用树状数组储存前缀和信息。 ```cpp inline void build_tree(int &x, int l, int r, int k, int d) { if(!x) x = ++cnt; C[x] += d; if(l == r) return; int mid = (l + r) >> 1; if(k <= mid) build_tree(tree[x].ls, l, mid, k, d); else build_tree(tree[x].rs, mid+1, r, k, d); return; } ``` # 修改+更新主席树 由于主席树特性,相邻历史版本之间只有一条链的差距,所以在进行更新版本的时候可以使用刚才的$build\_tree()$进行操作,操作的时候也要顾及树状数组特性,由$C[x]$向$C[n]$进行覆盖。 不过这里还有个玄学操作,那就是$lower\_bound()$居然还可以这样用!**通过$lower\_bound()$减去序列名,就可以直接得到下标**,这招学到了。这里$\_x$表示第一个需要修改的线段树/树状数组标号。 ```cpp inline void edit_tree(int x, int t) { int _x = lower_bound(b+1, b+_n+1, a[x]) - b; for(register int i = x; i <= n; i += lowbit(i)) build_tree(root[i], 1, _n, _x, t); return; } ``` # 查询第$k$小 这里利用的就是主席树的思想了,**用线段树在$r$节点的信息减去线段树在$l-1$节点的信息就可以得到$[l,r]$区间信息**。静态主席树可以直接$O(1)$跑出来,但是如果涉及到了修改,一般就只能用$O(n)$去跑。但是由于之前我们维护了一个树状数组$C[i]$,所以可以直接加速查询速度至$O(logn)$,相对地就可以跑得快一些了。 如果已经预处理出了所有需要查询到的树状数组下标,那么直接跑一次差分就完事了。这里$t1[i]$和$t2[i]$表示的就是需要查询到的树状数组的下标,所以$sum$先加上$C[tree[t2[i]].ls]$再减去$C[tree[t1[i]].ls]$,得到的就是两版本左子树不同节点的个数。如果$sum\ge k$,就说明左边节点数比查询的$k$要多,所以向左儿子跳去,即:把所有的$t1[i]$、$t2[i]$更新为对应线段树的左儿子,然后递归;否则向右儿子跳去,找右子树内第$k-sum$小即可。 ```cpp inline int QueryKth(int l, int r, int k) { if(l == r) return l; int mid = (l + r) >> 1; int sum = 0; for(register int i = 1; i <= n2; i++) sum += C[tree[t2[i]].ls]; for(register int i = 1; i <= n1; i++) sum -= C[tree[t1[i]].ls]; if(sum >= k) { for(register int i = 1; i <= n1; i++) t1[i] = tree[t1[i]].ls; for(register int i = 1; i <= n2; i++) t2[i] = tree[t2[i]].ls; return QueryKth(l, mid, k); } else { for(register int i = 1; i <= n1; i++) t1[i] = tree[t1[i]].rs; for(register int i = 1; i <= n2; i++) t2[i] = tree[t2[i]].rs; return QueryKth(mid+1, r, k-sum); } } ``` # 查询的处理过程 查询$[l,r]$的第$k$小元素的时候,先预处理出所有待处理的树状数组的标号,然后直接从$[1,\_n]$开始查找即可($\_n$是离散化之后的区间长度)。 ```cpp inline int QueryKth_Pre(int l, int r, int k) { n1 = n2 = 0; for(register int i = l - 1; i >= 1; i -= lowbit(i)) t1[++n1] = root[i]; for(register int i = r; i >= 1; i -= lowbit(i)) t2[++n2] = root[i]; return QueryKth(1, _n, k); } ``` # 骚气的$\textbf{unique()}$离散化 离散化本身挺蛋疼,之前我还在用这种基础写法: ```cpp inline void Discrete() { sort(node+1, node+n+1); a[node[1].id] = 1; rev[1] = node[1].num; for(register int i = 2; i <= n; i++) { if(node[i].num != node[i-1].num) id++; a[node[i].id] = id; rev[id] = node[i].num; } return; } ``` 虽然很蠢,却是最中用的。 不过,现在看来这个样子搞完全是没有必要了。又是一个强大的$STL$—— ### $\textbf{STL}$大法好!上网搜索$\textbf{unique()}$有真相! 一种叫做$unique()$的$STL$可以**直接帮我们完成去重的操作**,并**返回离散化之后序列的$end()$迭代器**。上面也有提到,**迭代器减去序列名就可以直接得到下标**,所以这样两句话就可以完成排序$+$离散化操作: ```cpp sort(b+1, b+1+_n); _n = unique(b+1, b+1+_n) - b - 1; ``` 其中$\_n$即为离散化之后的序列长度,上文有提到。这个$unique()$真是太好了哇! $main()$函数也写得差不多就可以了。需要注意修改点权的时候,$edit\_tree(c[i],-1)$后要**先把$\textbf{a[c[i]]}$给更新为$\textbf{d[i]}$**,然后再$\textbf{edit\_tree(c[i],1)}$,这个样子就成功完成了修改。 ```cpp int main() { scanf("%d%d", &n, &m); for(register int i = 1; i <= n; i++) { scanf("%d", &a[i]); b[++_n] = a[i]; } for(register int i = 1; i <= m; i++) { char ch = getchar(); while(ch != 'Q' &&ch != 'C') ch = getchar(); if(ch == 'Q') scanf("%d%d%d", &c[i], &d[i], &e[i]); else { scanf("%d%d", &c[i], &d[i]); b[++_n] = d[i]; } } sort(b+1, b+1+_n); _n = unique(b+1, b+1+_n) - b - 1; for(register int i = 1; i <= n; i++) edit_tree(i, 1); for(register int i = 1; i <= m; i++) { if(e[i]) printf("%d\n", b[QueryKth_Pre(c[i], d[i], e[i])]); else { edit_tree(c[i], -1); a[c[i]] = d[i]; edit_tree(c[i], 1); } } return 0; } ``` --- 完整代码 ```cpp //code #include<bits/stdc++.h> using namespace std; const int MAXN = 500010; int a[MAXN], b[MAXN], c[MAXN], d[MAXN], e[MAXN]; struct SEGTREE { int ls, rs, v; } tree[MAXN * 20]; int C[MAXN * 20]; int n1, n2, t1[MAXN], t2[MAXN]; int cnt = 0, root[MAXN]; int n, _n, m; inline int lowbit(int x) { return x & (-x); } inline void build_tree(int &x, int l, int r, int k, int d) { if(!x) x = ++cnt; C[x] += d; if(l == r) return; int mid = (l + r) >> 1; if(k <= mid) build_tree(tree[x].ls, l, mid, k, d); else build_tree(tree[x].rs, mid+1, r, k, d); return; } inline void edit_tree(int x, int t) { int _x = lower_bound(b+1, b+_n+1, a[x]) - b; for(register int i = x; i <= n; i += lowbit(i)) build_tree(root[i], 1, _n, _x, t); return; } inline int QueryKth(int l, int r, int k) { if(l == r) return l; int mid = (l + r) >> 1; int sum = 0; for(register int i = 1; i <= n2; i++) sum += C[tree[t2[i]].ls]; for(register int i = 1; i <= n1; i++) sum -= C[tree[t1[i]].ls]; if(sum >= k) { for(register int i = 1; i <= n1; i++) t1[i] = tree[t1[i]].ls; for(register int i = 1; i <= n2; i++) t2[i] = tree[t2[i]].ls; return QueryKth(l, mid, k); } else { for(register int i = 1; i <= n1; i++) t1[i] = tree[t1[i]].rs; for(register int i = 1; i <= n2; i++) t2[i] = tree[t2[i]].rs; return QueryKth(mid+1, r, k-sum); } } inline int QueryKth_Pre(int l, int r, int k) { n1 = n2 = 0; for(register int i = l - 1; i >= 1; i -= lowbit(i)) t1[++n1] = root[i]; for(register int i = r; i >= 1; i -= lowbit(i)) t2[++n2] = root[i]; return QueryKth(1, _n, k); } int main() { scanf("%d%d", &n, &m); for(register int i = 1; i <= n; i++) { scanf("%d", &a[i]); b[++_n] = a[i]; } for(register int i = 1; i <= m; i++) { char ch = getchar(); while(ch != 'Q' &&ch != 'C') ch = getchar(); if(ch == 'Q') scanf("%d%d%d", &c[i], &d[i], &e[i]); else { scanf("%d%d", &c[i], &d[i]); b[++_n] = d[i]; } } sort(b+1, b+1+_n); _n = unique(b+1, b+1+_n) - b - 1; for(register int i = 1; i <= n; i++) edit_tree(i, 1); for(register int i = 1; i <= m; i++) { if(e[i]) printf("%d\n", b[QueryKth_Pre(c[i], d[i], e[i])]); else { edit_tree(c[i], -1); a[c[i]] = d[i]; edit_tree(c[i], 1); } } return 0; } ```