「闭门造车」二叉分块树
写在前面
从刚开始学平衡树的时候我就被平衡树的美感所打动,一直渴望着自己能够创造出一种新的平衡树并接受众人的膜拜。
标题这个 idea 大概是在 2020.9 左右学完分块的 求第 大后产生的。我认为每次查询时都要暴力枚举整块求和太蠢了,在考虑用各种奇怪的东西维护这样一个带修前缀权值区间和问题时,我想到对块建立一个 BST 的形式,于是就有了标题这玩意。
以下将对这种乱搞做法进行一些介绍。
这里提前说一下,这东西完全没有用。复杂度最优的情况是块大小为 1,即建一棵静态 BST 的时候。阅读下文可能会浪费您宝贵的 2 分钟时间。
问题
您需要写一种数据结构来维护一些数。
有 次操作,每种操作是下列 6 种之一:
- 插入 数。
- 删除 数(若有多个相同的数,因只删除一个)。
- 查询 数的排名(排名定义为比当前数小的数的个数 +)。
- 查询排名为 的数。
- 求 的前驱(前驱定义为小于 ,且最大的数)。
- 求 的后继(后继定义为大于 ,且最小的数)。
。
1S,128MB。
分块解法
这里介绍修改 查询 的值域分块,在修改数大于查询数时可以获得更优的时间复杂度。
操作数量较少,先对出现的数离散化,设离散化后值域为 。查询数的排名与某排名对应的数,考虑对值域分块,设块大小为 。维护每个数出现的次数,及值域分块后每块内所有数出现的个数。
操作 1,2,插入删除操作, 单点修改即可。
操作 3,查询排名操作,即查询该数左侧所有数的出现次数。整块直接查询,散块暴力,复杂度上界 。
操作 4,查询某排名对应的数,大力枚举即可。
从小到大枚举整块,累计维护值域内所有数出现次数之和。当累计值 + 最后一个枚举到的整块 时,说明答案就在该块中。再顺序枚举答案所在整块中的数,累计出现次数直至 即得。复杂度上界 。
操作 5,查询前驱。先枚举 所在散块内 的数,检查是否存在。再枚举 之前的整块,直至找到一个内部有数的整块。答案就在该整块中,降序枚举找到最大的存在的数即可。复杂度上界 。
操作 6,查询后继,同操作 5 大力枚举,找到第一个存在的 的数,复杂度相同。
设块大小为 ,操作 3 ~ 6 的复杂度均为 ,总复杂度上界 。当修改次数 > 查询次数时复杂度较优。
参考代码:
复制复制//知识点:分块 /* 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; }
二叉分块树
考虑将分块序列建立成一个二叉搜索树的形态。满足树的中序遍历是原有的分块序列。在每一个节点上储存该节点子树内所有块中数的个数和 。记树高为 ,显然有 , 为块大小。
操作 1,2,插入删除操作,修改对应块,并跳父亲维护 即可,时间复杂度 。
操作 3,查询排名操作,考虑树上二分,将值小于对应值的子树 求和,权值所在散块暴力求和,时间复杂度 。
操作 4,同样树上二分,不断减去左子树的 向下递归,权值所在散块暴力,时间复杂度 。
操作 5、6,查询前驱后继。考虑直接查询指定值的排名 -1/+1 的数的值即可(感谢 xwmwr!)。
这里是一开始的更复杂的的实现:对于操作 5,由于树的形态固定,这里比较难以实现。首先考虑暴力查询散块,若不在散块中则检查被查询块左儿子内是否有数,如果有则找到左儿子内最靠右的节点即为答案所在块。否则答案在其它子树中,或者是原位置的某一祖先。考虑跳父亲找到对应子树/祖先,若在祖先中则暴力查散块,若在子树中则进入子树并找到子树中最靠右的节点即为答案所在块。操作 6 同理实现即可。
上述过程的时间复杂度仍为 。
则总时间复杂度为 。
离谱的是,由于 的增长率较小,则 时复杂度最优,为 级别。此时相当于建立了一棵静态的 BST,并且相当于没有分块。当然这是在修改查询操作次数平衡时的估计,在本题中块大小取 10 时表现最好。提交记录:Link。
那这东西意义何在?
这一切,值得吗?
意义——这真的重要吗?
参考代码
优美实现
//知识点:分块 /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cmath> #include <cstdio> #include <cstring> #define ll long long #define ls lson[now_] #define rs rson[now_] #define mid ((L_+R_)>>1) const int kMaxn = 1e5 + 10; const int kBlockNum = 1e5 + 10; const int kInf = 1e9 + 2077; //============================================================= struct Operation { int opt, x; } q[kMaxn]; int n, block_size, block_num, L[kBlockNum], R[kBlockNum], bel[kMaxn]; int cnt[kMaxn], cntblock[kBlockNum]; int node_num, root, fa[kBlockNum], lson[kBlockNum], rson[kBlockNum], sumcnt[kBlockNum]; int data_num, max_data, data[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 koishi() { int saroti; } 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; q[i].x = std::lower_bound(data + 1, data + max_data + 1, q[i].x) - data; } } void PrepareBlock() { block_size = (int) std::min(max_data, 10); 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 Pushup(int now_) { sumcnt[now_] = sumcnt[ls] + sumcnt[rs]; } void Build(int &now_, int L_, int R_) { if (L_ > R_) return ; int ls_ = 0, rs_ = 0; Build(ls_, L_, mid - 1); now_ = ++ node_num; Build(rs_, mid + 1, R_); fa[ls = ls_] = now_, fa[rs = rs_] = now_; } void Insert(int val_) { ++ cnt[val_]; ++ cntblock[bel[val_]]; for (int pos_ = bel[val_]; pos_; pos_ = fa[pos_]) ++ sumcnt[pos_]; } void Delete(int val_) { -- cnt[val_]; -- cntblock[bel[val_]]; for (int pos_ = bel[val_]; pos_; pos_ = fa[pos_]) -- sumcnt[pos_]; } int QueryRank(int now_, int val_) { //查询给定数值的排名 if (L[now_] <= val_ && val_ <= R[now_]) { int ret = 0; for (int i = L[now_]; i < val_; ++ i) ret += cnt[i]; return sumcnt[ls] + ret + 1; } int ret = 0; if (val_ < L[now_]) ret = QueryRank(ls, val_); else ret = sumcnt[ls] + cntblock[now_] + QueryRank(rs, val_); return ret; } int QueryVal(int now_, int rank_) { //查询给定排名对应的数 if (rank_ <= sumcnt[ls]) return QueryVal(ls, rank_); rank_ -= sumcnt[ls]; for (int i = L[now_]; i <= R[now_]; ++ i) { rank_ -= cnt[i]; if (rank_ <= 0) return i; } return QueryVal(rs, rank_); } //============================================================= int main() { // freopen("in.txt", "r", stdin); // freopen("out.txt", "w", stdout); Prepare(); PrepareBlock(); Build(root, 1, block_num); 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(root, x)); if(opt == 4) printf("%d\n", data[QueryVal(root, x)]); if(opt == 5) printf("%d\n", data[QueryVal(root, QueryRank(root, x) - 1)]); if(opt == 6) printf("%d\n", data[QueryVal(root, QueryRank(root, x + 1))]); } return 0; }
菜鸡实现
仅有查询前驱后继不同。
//知识点:分块 /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cmath> #include <cstdio> #include <cstring> #define ll long long #define ls lson[now_] #define rs rson[now_] #define mid ((L_+R_)>>1) const int kMaxn = 1e5 + 10; const int kBlockNum = 1e5 + 10; const int kInf = 1e9 + 2077; //============================================================= struct Operation { int opt, x; } q[kMaxn]; int n, block_size, block_num, L[kBlockNum], R[kBlockNum], bel[kMaxn]; int cnt[kMaxn], cntblock[kBlockNum]; int node_num, root, fa[kBlockNum], lson[kBlockNum], rson[kBlockNum], sumcnt[kBlockNum]; int data_num, max_data, data[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 koishi() { int saroti; } 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; q[i].x = std::lower_bound(data + 1, data + max_data + 1, q[i].x) - data; } } void PrepareBlock() { block_size = (int) std::min(max_data, 10); 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 Pushup(int now_) { sumcnt[now_] = sumcnt[ls] + sumcnt[rs]; } void Build(int &now_, int L_, int R_) { if (L_ > R_) return ; int ls_ = 0, rs_ = 0; Build(ls_, L_, mid - 1); now_ = ++ node_num; Build(rs_, mid + 1, R_); fa[ls = ls_] = now_, fa[rs = rs_] = now_; } void Insert(int val_) { ++ cnt[val_]; ++ cntblock[bel[val_]]; for (int pos_ = bel[val_]; pos_; pos_ = fa[pos_]) ++ sumcnt[pos_]; } void Delete(int val_) { -- cnt[val_]; -- cntblock[bel[val_]]; for (int pos_ = bel[val_]; pos_; pos_ = fa[pos_]) -- sumcnt[pos_]; } int QueryRank(int now_, int val_) { //查询给定数值的排名 if (L[now_] <= val_ && val_ <= R[now_]) { int ret = 0; for (int i = L[now_]; i < val_; ++ i) ret += cnt[i]; return sumcnt[ls] + ret + 1; } int ret = 0; if (val_ < L[now_]) ret = QueryRank(ls, val_); else ret = sumcnt[ls] + cntblock[now_] + QueryRank(rs, val_); return ret; } int QueryVal(int now_, int rank_) { //查询给定排名对应的数 if (rank_ <= sumcnt[ls]) return QueryVal(ls, rank_); rank_ -= sumcnt[ls]; for (int i = L[now_]; i <= R[now_]; ++ i) { rank_ -= cnt[i]; if (rank_ <= 0) return i; } return QueryVal(rs, rank_); } int QueryAhead(int val_) { //查询前驱 int now_ = bel[val_]; for (int i = val_ - 1; i >= L[now_]; -- i) { if (cnt[i]) return i; } if (sumcnt[ls]) { now_ = ls; } else { for (; now_; now_ = fa[now_]) { if (now_ == lson[fa[now_]]) continue; if (cntblock[fa[now_]]) { now_ = fa[now_]; for (int i = R[now_]; i >= L[now_]; -- i) { if (cnt[i]) return i; } } else if (sumcnt[lson[fa[now_]]]) { now_ = lson[fa[now_]]; break; } } } while (sumcnt[rs]) now_ = rs; for (int i = R[now_]; i >= L[now_]; -- i) { if (cnt[i]) return i; } } int QueryBack(int val_) { //查询后继 int now_ = bel[val_]; for (int i = val_ + 1; i <= R[now_]; ++ i) { if (cnt[i]) return i; } if (sumcnt[rs]) { now_ = rs; } else { for (; now_; now_ = fa[now_]) { if (now_ == rson[fa[now_]]) continue; if (cntblock[fa[now_]]) { now_ = fa[now_]; for (int i = L[now_]; i <= R[now_]; ++ i) { if (cnt[i]) return i; } } else if (sumcnt[rson[fa[now_]]]) { now_ = rson[fa[now_]]; break; } } } while (sumcnt[ls]) now_ = ls; for (int i = L[now_]; i <= R[now_]; ++ i) { if (cnt[i]) return i; } } //============================================================= int main() { // freopen("in.txt", "r", stdin); // freopen("out.txt", "w", stdout); Prepare(); PrepareBlock(); Build(root, 1, block_num); 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(root, x)); if(opt == 4) printf("%d\n", data[QueryVal(root, x)]); if(opt == 5) printf("%d\n", data[QueryAhead(x)]); if(opt == 6) printf("%d\n", data[QueryBack(x)]); } return 0; }
写在最后
这是一个根本没有任何卵用的东西。单从结果上来看我没有获得任何收获。
但是我玩的很开心。
如果单从结果上来看,在弱校学 OI 又有什么意义呢?
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】