首页 > 其他分享 >全局平衡二叉树 (GBST) 小记

全局平衡二叉树 (GBST) 小记

时间:2024-11-12 21:29:58浏览次数:1  
标签:int text GBST 二叉树 siz 重链 小记

全局平衡二叉树 (GBST) 小记

以下全局平衡二叉树简称 \(\text{GBST(Globel Balanced Search Tree)}\)。

我认识的大多数人,对 \(\text{GBST}\) 的理解基本上都是 静态 \(\text{LCT}\),或者静态 \(\text{Top Tree}\),不过我对 \(\text{LCT}\) 的理解可能还差一点,所以我不打算从阉割 \(\text{LCT}\) 的角度来讲解。

于是下文基本上是从树剖优化的角度讲解。


考虑一个经典的树上问题,\(n\) 个结点,点有点权,\(q\) 次操作,操作有两种:

  • 给定 \(x, y, k\),将 \(x\) 到 \(y\) 路径上的点权值 \(+k\)。
  • 给定 \(x, y\),求 \(x\) 到 \(y\) 路径上所有点的权值之和。

首先经典的可以将问题差分,变成前缀链加和前缀链查询。于是下面我们都默认链是树上的前缀,即一条 \(x\) 到根的路径。

对原树重链剖分,注意到无论是修改还是查询,都作用在重链的一段前缀上。这给了我们一些操作的空间。

考虑对于每条重链分别维护线段树,而不是简单的用 \(\text{dfs}\) 序拍平成序列问题,但这样只是常数小一点(真的小了吗?),对于单条重链上做修改/查询的时间复杂度还是 \(O(\log n)\) 的。

我们发现时间复杂度优化不了,本质上的原因的每条重链是独立的。我们考虑用一些数据结构将树剖的 \(\log\) 和线段树的 \(\text{log}\) 联系起来。

具体而言,我们对于重链用一棵二叉搜索树维护。由于二叉搜索树的真实顺序是其中序遍历,画画图就能知道求的是什么。考虑 \(x\) 到 \(x\) 所在重链链顶的贡献,拿出对于每条树边 \(u \rightarrow v\),如果 \(u\) 是 \(v\) 右儿子则将 \(u\) 加入集合 \(s\),特别的 \(x\) 也在 \(s\) 中。会被操作覆盖的点是 \(s\) 中所有点和其左子树的并,这样可以做到一个看似 \(O(1)\) 修改/查询。

但是如果真的建一个完全二叉树,那复杂度又回去了。因为对于一条重链,还是会在该重链对应的二叉树上条 \(O(\log n)\) 步。

于是考虑构造一个类似于 每跳一条边,子树大小翻倍的结构。我们发现轻边已经满足这个要求,要是完全二叉树上的边也满足这个要求,不难想到建二叉树时不取中点为根(取中点就是完全二叉树),而取关于子树大小的带权中点即可。

但是带权中点总会向一边倾斜,所以最坏情况下要条两条边才能使子树大小翻倍。也就是说最坏情况下,\(\text{GBST}\) 上一个点的深度是 \(2 \log n\)。可能又更好的分析/构造方法得到类似 深度不超过 \(\log n + O(1)\) 的结论,不过它已经足够优秀,我就懒得想了。


下面是例题:

代码:

#include <iostream> 
#include <vector>
#include <random>
#include <algorithm>

using namespace std;
using uLL = unsigned long long;

const int N = 2e5 + 5; 

int n, q, v, seed;
vector<int> e[N];
int fa[N], siz[N], wson[N];
int lc[N], rc[N], ss[N];
uLL val[N], sum[N], tag[N];
mt19937 R;

bool Get_which (int x) {
  return x == lc[fa[x]] ? 0 : (x == rc[fa[x]] ? 1 : -1);
}

int Rand (int l, int r) {
  return uniform_int_distribution<int>(l, r)(R);
} 

void Init (int u, int r) {
  if (r) e[u].erase(find(e[u].begin(), e[u].end(), r));
  siz[u] = 1; 
  for (auto v : e[u]) {
    if (v == r) continue;
    Init(v, u);
    siz[u] += siz[v];
    if (siz[v] > siz[wson[u]]) {
      wson[u] = v; 
    }
  }
}

int Build (vector<int>::iterator itl, vector<int>::iterator itr) {
  if (itl == itr) return 0; 
  int bot = siz[*prev(itr)], top = siz[*itl] - bot;
  auto mid = upper_bound(itl, itr, top / 2, [&](int half, int x) -> bool {
    return siz[x] - bot <= half;
  });
  int u = *mid;
  lc[u] = Build(itl, mid), rc[u] = Build(mid + 1, itr);
  fa[lc[u]] = fa[rc[u]] = u, ss[u] = ss[lc[u]] + ss[rc[u]] + 1;
  return u; 
}

int Divide (int u) {
  vector<int> lis;
  for (int s = u; s; s = wson[s]) {
    lis.push_back(s);
    for (auto v : e[s]) {
      if (v != wson[s])
        fa[Divide(v)] = s;
    }

  }
  return Build(lis.begin(), lis.end());
}

void Node_add (int x, uLL y) {
  val[x] += y, sum[x] += y * ss[x], tag[x] += y; 
}

void Pushdown (int x) {
  if (tag[x]) {
    if (lc[x]) Node_add(lc[x], tag[x]);
    if (rc[x]) Node_add(rc[x], tag[x]);
    tag[x] = 0; 
  }
}

void Pushup (int x) {
  sum[x] = sum[lc[x]] + sum[rc[x]] + val[x];
}

void Add (int x, int y, int whi = -1) {
  if (fa[x]) {
    Add(fa[x], y, Get_which(x));
  }
  Pushdown(x);
  if (whi != 0) {
    val[x] += y, sum[x] += y; 
    if (lc[x]) Node_add(lc[x], y);
  }
  if (whi == -1) {
    for (int i = x; i; i = fa[i]) {
      Pushup(i);
      if (Get_which(i) == -1) break;
    }
  }
}

uLL Query (int x, int whi = -1) {
  uLL res = 0; 
  if (fa[x]) {
    res = Query(fa[x], Get_which(x));
  }
  Pushdown(x);
  if (whi != 0) {
    res += val[x] + sum[lc[x]];
  }
  return res;
}

int main () {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> q >> v >> seed, R = mt19937(seed); 
  for (int i = 1, x, y; i < n; ++i) {
    cin >> x >> y; 
    e[x].push_back(y);
    e[y].push_back(x);
  }
  Init(1, 0), Divide(1); 
  uLL ans = 0; 
  for (int i = 1, o, x, y; i <= q; ++i) {
    o = Rand(1, 2), x = Rand(1, n); 
    if (o == 1) {
      y = Rand(1, v);
      Add(x, y);
    }
    else {
      uLL ret = Query(x);
      ans ^= i * ret;
    }
  }
  cout << ans << '\n';
  return 0; 
}

下面是效率比较,以上面的例题为例(\(n = 2 \times 10^5, q = 5 \times 10^6\)):

树的形态 树剖+线段树 \(\text{GBST}\) 暴力跳
随机树(深度为 \(O(\sqrt n)\)) \(\text{3.9s}\) \(\text{1.8s}\) \(\text{3.0s}\)
\(\text{1.6s}\) \(\text{2.5s}\) \(\ge \text{100.0s}\)
菊花 \(\text{1.6s}\) \(\text{0.4s}\) \(\text{0.3s}\)
完全二叉树 \(\text{6.8s}\) \(\text{1.5s}\) \(\text{0.6s}\)
链上插点 \(\text{2.1s}\) \(\text{2.3s}\) \(\ge \text{100.0s}\)

这里暴力跳仅作为对照,参考意义不大。为什么 \(\text{GBST}\) 理论复杂度比树剖少个 \(\log n\),实际表现却没有那么优秀?主要原因有以下几个:

  • 树剖本身很难卡满,这里的难卡满指的是重链条数,因为你拍成 \(\text{dfs}\) 序做的话实际上具体查询的区间是什么对时间复杂度影响是不大的。

  • 我写的 \(\text{GBST}\) 没有标记永久化,那么每次全部 \(\text{pushdown}\) 一遍常数其实是挺大的。

  • 这题 \(n\) 较小而 \(q\) 较大,这是为了减小输入量。但是 \(\text{GBST}\) 和树剖的复杂度差距是靠 \(n\) 拉开的,所以这道题其实没出好。

并且 \(\text{GBST}\) 还有一个缺陷。前面我们讨论的都是默认差分后的情况,但任意路径的前缀修改/求和需要差分成四条前缀链,但树剖可以自然的处理掉,所以如果真要差分的话 \(\text{GBST}\) 可能还要带一个两倍左右的常数。

当然你可以用一个类似树剖的写法,而不去差分成前缀链。那么这样的问题是在修改的两个点 \(x\) 和 \(y\) 相遇时,相当于要做一个任意区间修改/查询,好在你只需做一次,那么可以接受单次 \(O(\log n)\) 的时间复杂度。还是在二叉搜索树上暴力跳,做一些分讨即可。

所以这样理论上树剖能做的问题,\(\text{GBST}\) 都是能做的,包括像链上 \(\min\) 这样的不可差分问题。不过它最经典的应用应该还是优化 \(\text{ddp}\),因为它本身就是前缀,又没有懒标记之类的东西,所以写起来非常方便。

标签:int,text,GBST,二叉树,siz,重链,小记
From: https://www.cnblogs.com/estruct/p/18542699

相关文章

  • 计算二叉树(二叉链表)的带权路径长度
    方法1#include<bits/stdc++.h>#defineintlonglong#definemod998244353usingnamespacestd;usingpii=pair<int,int>;typedefstructBtnode{ intdata; structBtnode*lc,*rc; }*Btree,Btnode;//笔试这个可以放上面,但是真的写程序应该把getwpl放在g......
  • 101. 对称二叉树
    题目链接解题思路检查一个二叉树是否轴对称,其实和根结点无关,而是和其左右子树有关。左子树头等于右子树头,然后递归调用,「左子树的右儿子」要等于「右子树的左儿子」并且「左子树的左儿子」要等于「右子树的左儿子」。代码/***Definitionforabinarytreenode.......
  • 代码随想录——二叉树-12.平衡二叉树
    自顶向下递归(前序遍历)这种方法是一开始想到的,虽然ac了但是对于它的本质却不清不楚,不知道时间复杂度,不知道属于前序遍历。思路首先得到root节点的左右子树的深度(左右),若深度差绝对值大于1(中),则root为根的树不是平衡二叉树;否则继续递归root的左右子树,其左右子树都是平衡二叉树......
  • 代码随想录——二叉树-11.完全二叉树的节点个数
    思路一、层序遍历,时间复杂度O(n)二、利用完全二叉树性质,时间复杂度O(logn*logn)(小于O(n))完全二叉树性质:若树深度为h,则前h-1层节点都达到最大值。第h层节点都集中在最左侧的位置完全二叉树要么1.是满二叉树2.最后一层没满满二叉树计算节点数太方便了,直接用公式2^h-1。......
  • 数据结构 ——— 链式二叉树oj题:对称二叉树
    目录题目要求手搓一个对称二叉树代码实现 题目要求给你一个二叉树的根节点 root ,检查它是否轴对称手搓一个对称二叉树代码演示://数据类型typedefintBTDataType;//二叉树节点的结构typedefstructBinaryTreeNode{ BTDataTypedata;//每个节点的数据......
  • 实现链式结构二叉树
    目录需要实现的操作链式结构二叉树实现结点的创建前序遍历中序遍历后序遍历计算结点个数计算二叉树的叶子结点个数 计算二叉树第k层结点个数计算二叉树的深度查找值为x的结点 销毁层序遍历判断是否为完全二叉树 总结需要实现的操作//前序遍历voidPreOr......
  • 2-sat小记
    记得好像写了,但找了一下发现没写,于是写一下2-sat用于求p→q的蕴含关系集合的一组解(或判断无解)流程:先构造蕴含关系集合,谁成立/不成立时另一个必须怎么样对每个命题p建p和非p(p'),每个蕴含关系p→q连边(p,q),(q',p'),一定要有逆否的反向边然后①跑tarjan缩点,若存在p和p'在同......
  • 每日一题之二叉树
    已知结点元素值为正整数且值不相同的一棵二叉树。该二叉树通过给出其先序遍历序列和中序遍历序列构造而成。输入一个整数x,针对此二叉树编写程序求出x的右子树中所有结点值的和(若x不在树上,输出-1)。 输入说明:第一行输入某二叉树的先序遍历序列第二行输入该二叉树的中序遍历......
  • 根据二叉树创建字符串
    题目:606.根据二叉树创建字符串-力扣(LeetCode)给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。空节点使用一对空括号对 "()" 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空......
  • 代码随想录算法训练营第18天| 530.二叉搜索树的最小绝对差, 501.二叉搜索树中的众数 , 2
    530.二叉搜索树的最小绝对差文章链接:https://programmercarl.com/0530.二叉搜索树的最小绝对差.html视频链接:https://www.bilibili.com/video/BV1DD4y11779/?vd_source=6cb513d59bf1f73f86d4225e9803d47b题目链接:https://leetcode.cn/problems/minimum-absolute-difference-in......