「闭门造车」二叉分块树

写在前面

从刚开始学平衡树的时候我就被平衡树的美感所打动,一直渴望着自己能够创造出一种新的平衡树并接受众人的膜拜

标题这个 idea 大概是在 2020.9 左右学完分块的 O(1)O(n) 求第 k 大后产生的。我认为每次查询时都要暴力枚举整块求和太蠢了,在考虑用各种奇怪的东西维护这样一个带修前缀权值区间和问题时,我想到对块建立一个 BST 的形式,于是就有了标题这玩意。

以下将对这种乱搞做法进行一些介绍。

这里提前说一下,这东西完全没有用。复杂度最优的情况是块大小为 1,即建一棵静态 BST 的时候。阅读下文可能会浪费您宝贵的 2 分钟时间。

问题

Luogu P3369 【模板】普通平衡树

您需要写一种数据结构来维护一些数。
n 次操作,每种操作是下列 6 种之一:

  1. 插入 x 数。
  2. 删除 x 数(若有多个相同的数,因只删除一个)。
  3. 查询 x 数的排名(排名定义为比当前数小的数的个数 +1)。
  4. 查询排名为 x 的数。
  5. x 的前驱(前驱定义为小于 x,且最大的数)。
  6. x 的后继(后继定义为大于 x,且最小的数)。

1n105
1S,128MB。

分块解法

这里介绍修改 O(1) 查询 O(n) 的值域分块,在修改数大于查询数时可以获得更优的时间复杂度。

操作数量较少,先对出现的数离散化,设离散化后值域为 [1,n]。查询数的排名与某排名对应的数,考虑对值域分块,设块大小为 T。维护每个数出现的次数,及值域分块后每块内所有数出现的个数。

操作 1,2,插入删除操作,O(1) 单点修改即可。

操作 3,查询排名操作,即查询该数左侧所有数的出现次数。整块直接查询,散块暴力,复杂度上界 O(nT+T)

操作 4,查询某排名对应的数,大力枚举即可。
从小到大枚举整块,累计维护值域内所有数出现次数之和。当累计值 + 最后一个枚举到的整块 x 时,说明答案就在该块中。再顺序枚举答案所在整块中的数,累计出现次数直至 x 即得。复杂度上界 O(nT+T)

操作 5,查询前驱。先枚举 x 所在散块内 <x 的数,检查是否存在。再枚举 x 之前的整块,直至找到一个内部有数的整块。答案就在该整块中,降序枚举找到最大的存在的数即可。复杂度上界 O(nT+T)
操作 6,查询后继,同操作 5 大力枚举,找到第一个存在的 >x 的数,复杂度相同。

设块大小为 n,操作 3 ~ 6 的复杂度均为 O(n),总复杂度上界 O(nn)。当修改次数 > 查询次数时复杂度较优。

参考代码:

复制复制
//知识点:分块
/*
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;
}

二叉分块树

考虑将分块序列建立成一个二叉搜索树的形态。满足树的中序遍历是原有的分块序列。在每一个节点上储存该节点子树内所有块中数的个数和 size。记树高为 h,显然有 hlog2(nT)T 为块大小。

操作 1,2,插入删除操作,修改对应块,并跳父亲维护 size 即可,时间复杂度 O(h)

操作 3,查询排名操作,考虑树上二分,将值小于对应值的子树 size 求和,权值所在散块暴力求和,时间复杂度 O(h+T)

操作 4,同样树上二分,不断减去左子树的 size 向下递归,权值所在散块暴力,时间复杂度 O(h+T)

操作 5、6,查询前驱后继。考虑直接查询指定值的排名 -1/+1 的数的值即可(感谢 xwmwr!)。
这里是一开始的更复杂的的实现:对于操作 5,由于树的形态固定,这里比较难以实现。首先考虑暴力查询散块,若不在散块中则检查被查询块左儿子内是否有数,如果有则找到左儿子内最靠右的节点即为答案所在块。否则答案在其它子树中,或者是原位置的某一祖先。考虑跳父亲找到对应子树/祖先,若在祖先中则暴力查散块,若在子树中则进入子树并找到子树中最靠右的节点即为答案所在块。操作 6 同理实现即可。
上述过程的时间复杂度仍为 O(h+T)

则总时间复杂度为 O(n(h+T))O(n(log2(nT)+T))

离谱的是,由于 log 的增长率较小,则 T=1 时复杂度最优,为 O(nlogn) 级别。此时相当于建立了一棵静态的 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 又有什么意义呢?

posted @   Luckyblock  阅读(207)  评论(3编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 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】
点击右上角即可分享
微信分享提示