• 「闭门造车」二叉分块树


    写在前面

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

    标题这个 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,转载请声明出处。
  • 相关阅读:
    System.AccessViolationException”类型的第一次机会异常在 System.Data.dll 中发生 其他信息: 尝试读取或写入受保护的内存。这通常指示其他内存已损坏。
    解决浏览器显示长数字不能自动换行或长字母不能换行的问题
    索引 -1 没有值
    vrpie在Visio Studio 中无法调试的问题
    vrpie下实现vrp模型和javascript的交互
    IE下Date.parse出现NaN有关问题解决
    SQL Server 一些关键字详解(一)
    SQL Server 一些关键字详解(二)
    WPF WebBrowser 遇到的问题
    日历代码
  • 原文地址:https://www.cnblogs.com/luckyblock/p/14447193.html
Copyright © 2020-2023  润新知