平衡结合

灵乌路空

2020-09-08 19:10:50

Solution

# 平衡结合 ## 引入 [Luogu P3369 【模板】普通平衡树](https://www.luogu.com.cn/problem/P3369) >您需要写一种数据结构来维护一些数。 >有 $n$ 次操作,每种操作是下列 6 种之一: >1. 插入 $x$ 数。 >2. 删除 $x$ 数(若有多个相同的数,因只删除一个)。 >3. 查询 $x$ 数的排名(排名定义为比当前数小的数的个数 +$1$)。 >4. 查询排名为 $x$ 的数。 >5. 求 $x$ 的前驱(前驱定义为小于 $x$,且最大的数)。 >6. 求 $x$ 的后继(后继定义为大于 $x$,且最小的数)。 > >$1\le n\le 10^5$。 本题显然可使用平衡树,权值线段树等方法维护,它们查询修改的复杂度是平衡的,均为 $O(\log n)$ 级别。 可以在修改,查询次数同级时获得较优的时间复杂度。 但在某些情况下,修改与查询次数可能是不平衡的,这时可通过其他手段来处理该问题: 考虑分块解法,修改 $O(1)$,查询 $O(\sqrt{n})$ ,在修改数大于查询数时,可以获得更优的时间复杂度。 这就是一种平衡结合。 根据修改与查询次数的关系,通过调整维护的手段,使总的复杂度达到更低级别。 ## 分块解法 操作数量较少,**先对出现的数离散化**,设离散化后值域为 $[1,n]$。 查询数的排名与某排名对应的数,考虑对值域分块,设块大小为 $T$。 维护每个数出现的次数,及值域分块后每块内所有数出现的个数。 操作 1,2,插入删除操作,$O(1)$ 单点修改即可。 操作 3,查询排名操作,即查询该数左侧所有数的出现次数。 整块直接查询,散块暴力,复杂度上界 $O(\frac{n}{T} + T)$。 操作 4,查询某排名对应的数,大力枚举即可。 从小到大枚举整块,累计维护值域内所有数出现次数之和。 当累计值 + 最后一个枚举到的整块 $\ge x$ 时,说明答案就在该块中。 再顺序枚举答案所在整块中的数,累计出现次数直至 $\ge x$ 即得。 复杂度上界 $O(\frac{n}{T} + T)$。 操作 5,查询前驱。 先枚举 $x$ 所在散块内 $< x$ 的数,检查是否存在。 再枚举 $x$ 之前的整块,直至找到一个内部有数的整块。 答案就在该整块中,降序枚举找到最大的存在的数即可。 复杂度上界 $O(\frac{n}{T} + T)$。 操作 6,查询后继,同操作 5 大力枚举,找到第一个存在的 $>x$ 的数,复杂度相同。 设块大小为 $\sqrt{n}$,操作 3 ~ 6 的复杂度均为 $O(\sqrt n)$,总复杂度上界 $O(n\sqrt{n})$。 当修改次数 > 查询次数时复杂度较优。 代码 ```cpp //知识点:分块 /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cmath> #include <cstdio> #include <cstring> #define ll long long const int kMaxn = 1e5 + 10; const int kMaxSqrtn = 320; const int kInf = 1e9 + 2077; //============================================================= struct Operation { int opt, x; } q[kMaxn]; int n, block_size, block_num, L[kMaxSqrtn], R[kMaxSqrtn], bel[kMaxn]; int cnt[kMaxn], cntblock[kMaxSqrtn]; int data_num, max_data, data[kMaxn], map[kMaxn]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Prepare() { //离线操作,并离散化。 n = read(); for (int i = 1; i <= n; ++ i) { q[i] = (Operation) {read(), read()}; if (q[i].opt != 4) data[++ data_num] = q[i].x; //注意操作 4 的参数不需要离散化。 } data[0] = - kInf; std :: sort(data + 1, data + data_num + 1); for (int i = 1; i <= data_num; ++ i) { if (data[i] != data[i - 1]) max_data ++; data[max_data] = data[i]; } for (int i = 1; i <= n; ++ i) { if (q[i].opt == 4) continue; int origin = q[i].x; q[i].x = std :: lower_bound(data + 1, data + max_data + 1, q[i].x) - data; map[q[i].x] = origin; } } void PrepareBlock() { block_size = (int) sqrt(max_data); block_num = max_data / block_size; for (int i = 1; i <= block_num; ++ i) { L[i] = (i - 1) * block_size + 1; R[i] = i * block_size; } if (R[block_num] < max_data) { block_num ++; L[block_num] = R[block_num - 1] + 1; R[block_num] = max_data; } for (int i = 1; i <= block_num; ++ i) { for (int j = L[i]; j <= R[i]; ++ j) { bel[j] = i; } } } void Insert(int val_) { //O(1) 插入 cnt[val_] ++; cntblock[bel[val_]] ++; } void Delete(int val_) { //O(1) 删除 cnt[val_] --; cntblock[bel[val_]] --; } int QueryRank(int val_) { //查询给定数值的排名 int belval = bel[val_], ret = 0; for (int i = L[belval]; i < val_; ++ i) ret += cnt[i]; //注意 <val_ for (int i = 1; i < belval; ++ i) ret += cntblock[i]; return ret + 1; } int QueryVal(int rank_) { //查询给定排名对应的数 int size = 0, ret, belval; for (belval = 1; belval <= block_num; ++ belval) { if (size + cntblock[belval] >= rank_) break; size += cntblock[belval]; } for (ret = L[belval]; ret <= R[belval]; ++ ret) { if (size + cnt[ret] >= rank_) break; size += cnt[ret]; } return ret; } int QueryAhead(int val_) { //查询前驱 int belval = bel[val_]; for (int i = val_ - 1; i >= L[belval]; -- i) { if (cnt[i]) return i; } for (int i = belval - 1; i; -- i) { if (! cntblock[i]) continue; for (int j = R[i]; j >= L[i]; -- j) { if (cnt[j]) return j; } } } int QueryBack(int val_) { //查询后继 int belval = bel[val_]; for (int i = val_ + 1; i <= R[belval]; ++ i) { if (cnt[i]) return i; } for (int i = belval + 1; i <= block_num; ++ i) { if (! cntblock[i]) continue; for (int j = L[i]; j <= R[i]; ++ j) { if (cnt[j]) return j; } } } void koishi() { int satori; } //============================================================= int main() { Prepare(); PrepareBlock(); for (int i = 1; i <= n; ++ i) { int opt = q[i].opt, x = q[i].x; if(opt == 1) Insert(x); if(opt == 2) Delete(x); if(opt == 3) printf("%d\n", QueryRank(x)); if(opt == 4) printf("%d\n", map[QueryVal(x)]); if(opt == 5) printf("%d\n", map[QueryAhead(x)]); if(opt == 6) printf("%d\n", map[QueryBack(x)]); } return 0; } ``` ## 一个例子 [可能是集训题的无出处题](http://114514.cn) >给定一长度为 $n$ 的数列 $a$,参数 $w$,$q$ 次询问。 >每次询问给定参数 $l,r$,求忽略出现次数 $>w$ 的数后,区间 $[l,r]$ 内第 $k$ 小值。 >$1\le n,q,a_i,w\le10^5$。 忽略出现次数 $>w$ 的数,没法上主席树。 但显然可以莫队套平衡树,当枚举的区间内某个数首次出现时插入,出现次数 $=0$ 或 $> w$ 时删除。 $n,a_i$ 同级,修改和查询的复杂度相同,均为 $O(\log n)$。 块大小为 $T$ 时,总复杂度为 $O((\frac{n^2}{T}+qT)\log n + q\log n)$。 块大小为 $\frac{n}{\sqrt{q}}$ 时最优,总复杂度为 $O(n\sqrt{q}\log n+ q\log n)$,过不了。 发现块大小为 $T$ 时,莫队中一共有 $\frac{n^2}{T}+qT$ 次修改操作,但只有 $q$ 次查询操作。 考虑平衡结合,按照上述方法,使用分块维护区间第 $k$ 小值。 单次修改复杂度变为 $O(1)$,查询变为 $O(\sqrt{n})$,总复杂度 $O(\frac{n^2}{T}+qT + q\sqrt{n})$。 块大小取 $\frac{n}{\sqrt{q}}$ 时最优,总复杂度为 $O(n\sqrt{q} + q\sqrt{n})$。 ## 两个例子 [P4867 Gty的二逼妹子序列](https://www.luogu.com.cn/problem/P4867) >给定一长度为 $n$ 的数列 $a$,$m$ 次询问。 >每次询问给定参数 $l,r,a,b$,求区间 $[l,r]$ 内权值 $\in [a,b]$ 的数的种类数。 >$1\le a_i\le n\le 10^5$,$1\le m\le 10^6$。 --- 发现莫队比较便于维护种类数,套一个莫队消去区间的限制。 考虑值域的限制,权值线段树进行维护。 单次 修改/查询 复杂度均为 $O(\log n)$。 设块大小为 $\dfrac{n}{\sqrt{m}}$,总复杂度为 $O(n\sqrt{m}\log n + m\log n)$。 只有单点修改,考虑对值域分块,维护块内不同的数的个数,可在莫队左右端点移动顺便维护。 查询时,在查询值域内的完整块直接统计答案,不完整块暴力查询。 设块大小为 $\dfrac{n}{\sqrt{m}}$,单次修改复杂度 $O(1)$,查询复杂度 $\sqrt{n}$,总复杂度为 $O(n\sqrt{m} +m\sqrt{n})$。 代码详见:[我的 Blog](https://www.cnblogs.com/luckyblock/p/13611018.html)。