「闭门造车」二叉分块树

写在前面

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

标题这个 idea 大概是在 2020.9 左右学完分块的 (O(1)-O(sqrt 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),且最小的数)。

(1le nle 10^5)
1S,128MB。

分块解法

这里介绍修改 (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(nsqrt{n}))。当修改次数 > 查询次数时复杂度较优。

参考代码:

//知识点:分块
/*
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
", QueryRank(x));
    if(opt == 4) printf("%d
", map[QueryVal(x)]);
    if(opt == 5) printf("%d
", map[QueryAhead(x)]);
    if(opt == 6) printf("%d
", map[QueryBack(x)]);
  }
  return 0; 
}

二叉分块树

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

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

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

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

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

则总时间复杂度为 (Oleft(n(h + T) ight) approx O(n(log_2{left(frac{n}{T} ight) + T)}))

离谱的是,由于 (log) 的增长率较小,则 (T = 1) 时复杂度最优,为 (O(nlog n)) 级别。此时相当于建立了一棵静态的 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
", QueryRank(root, x));
    if(opt == 4) printf("%d
", data[QueryVal(root, x)]);
    if(opt == 5) printf("%d
", data[QueryVal(root, QueryRank(root, x) - 1)]);
    if(opt == 6) printf("%d
", 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
", QueryRank(root, x));
    if(opt == 4) printf("%d
", data[QueryVal(root, x)]);
    if(opt == 5) printf("%d
", data[QueryAhead(x)]);
    if(opt == 6) printf("%d
", data[QueryBack(x)]);
  }
  return 0; 
}

写在最后

这是一个根本没有任何卵用的东西。单从结果上来看我没有获得任何收获。

但是我玩的很开心。

如果单从结果上来看,在弱校学 OI 又有什么意义呢?

作者@Luckyblock,转载请声明出处。
原文地址:https://www.cnblogs.com/luckyblock/p/14447193.html