Treap 平衡树

\(Treap\) 平衡树

概念

\(Treap\) 是一种代码复杂度与思维都较为简单的平衡树,它在普通 \(BST\) 的基础上,给每一个节点赋予了一个优先级属性。对于 \(Treap\) 中的每一个节点,除了它的权值满足 \(BST\) 的基本关系,它的优先级也满足小根堆的性质。

我们发现 \(BST\) 退化为的原因是因为插入的数据有序(所有数据均被插在左子树或右子树),我们想办法随机构造优先级,这样它就无序了。

所以,\(Treap=Tree+heap\)。(本文为小根堆)

基本操作

旋转

为了保证 \(Treap\) 在满足 \(BST\) 的基础上又满足小根堆的性质,我们每次操作都需要对它进行调整,而调整的方法是旋转:

  1. 左旋(\(Zag\):左旋一棵子树(根为 \(u\)),我们可以把它理解为将该子树的左子树的右子树提起来,然后对下方节点特殊处理

    graph TB 1((1)) 2((2)) 3((3)) 4((4)) 5((5)) 4---2 4---5 2---1 2---3 a1((1)) a2((2)) a3((3)) a4((4)) a5((5)) a2---a1 a2---a4 a4---a3 a4---a5

    旋转后 \(u\) 会变为该子树的新根的右子节点\(u\)左子节点变为新根

  2. 右旋(\(Zig\):右旋一棵子树(根为 \(u\)),我们可以把它理解为将该子树的右子树的右子树提起来,然后对下方节点特殊处理

graph TB a1((1)) a2((2)) a3((3)) a4((4)) a5((5)) a2---a1 a2---a4 a4---a3 a4---a5 b1((1)) b2((2)) b3((3)) b4((4)) b5((5)) b4---b2 b4---b5 b2---b1 b2---b3

​ 旋转后 \(u\) 会变为该子树的新根的左子节点,\(u\)右子节点变为新根

显然旋转不会使 \(Treap\) 失去 \(BST\) 的性质,我们可以通过旋转将 \(BST\) 变为 \(Heap\)

struct node {
  int lson,rson; // 左右子节点
  int val,pri; // 权值与优先级
  int cnt; // 计数器,重复的元素
  int size; // 子树节点个数(后面其它操作需要)
};
node tree[maxn]; // Treap
// 注意地址引用&!
inline void zig(int &uid) { // 右旋
		node &u = tree[uid];
		node &v = tree[u.lson]; int vid = u.lson;
		u.lson = v.rson; // 更新节点关系
		v.rson = uid;
		u.size = tree[u.lson].size + tree[u.rson].size + u.cnt; // 更新大小
		v.size = tree[v.lson].size + tree[v.rson].size + v.cnt;
		uid = vid; // 更新根节点
	}
	inline void zag(int &uid) { // 左旋
		node& u = tree[uid];
		node& v = tree[u.rson]; int vid = u.rson;
		u.rson = v.lson; // 更新节点关系
		v.lson = uid;
		u.size = tree[u.lson].size + tree[u.rson].size + u.cnt; // 更新大小
		v.size = tree[v.lson].size + tree[v.rson].size + v.cnt;
		uid = vid; // 更新根节点
	}

注意:注意使用引用&我就是这样错的,呜呜呜……

插入节点

\(Treap\) 中插入节点与 \(BST\) 中类似:

插入:

  1. 从根节点开始插入
  2. 若插入值小于(等于)当前值,则在当前节点的左子树中寻找插入位置
  3. 若插入值大于当前值,则在当前节点的右子树中寻找插入位置
  4. 若当前值为空节点,则在此建立空节点,为该节点生成一个随机的优先值

回溯(维护 \(Heap\) 的性质):

  1. 若左子节点的优先级小于当前节点的优先级,对以该节点为根的子树右旋
  2. 若右子节点的优先级小于当前节点的优先级,对以该节点为根的子树左旋(仅可能存在一个子节点小于的情况,再插入前保证本身就是一个 \(Heap\)
inline void insert(int &u,int val) {
  if (!u) { // 空节点
    u = ++ nodecnt; // 新节点编号
    tree[u].val = val,tree[u].pri = rand(); // 权值与优先级
    tree[u].cnt = tree[u].size = 1; // 重复元素数量与大小
    tree[u].lson = tree[u].rson = 0; // 初始化左右子节点
    return ;
  }
  tree[u].size ++; // 经过的每一个子树大小都需增加
  if (tree[u].val == val) tree[u].cnt ++; // 相同节点
  else if (val < tree[u].val) {
    insert(tree[u].lson,val);
    if (tree[tree[u].lson].pri < tree[u].pri) zig(u);
  } else {
    insert(tree[u].rson,val);
    if (tree[tree[u].rson].pri < tree[u].pri) zag(u);
  }
}

删除节点

\(Treap\) 中删除节点与 \(Heap\) 中类似:

  1. 该节点为叶子节点或链节点(只有一个子节点的节点),按照 \(BST\) 的方法直接删除
  2. 该节点为有两个子节点的节点,我们通过旋转将这个节点下去(可类比 \(Heap\) 中一步步将根比较下来),达成条件 \(1\) 后直接删除:
    1. 该节点左子节点优先级小于右子节点优先级,右旋该子树(将该节点降为右子节点)
    2. 该节点左子节点优先级大于右子节点优先级,左旋该子树(将该节点降为左子节点)
inline void deleteNode(int &u,int val) { // 删除权值为 val 的元素
  if (tree[u].val == val) { // 找到节点
    if (tree[u].cnt > 1) // 重复元素
      tree[u].cnt --,tree[u].size --;
    else if (!tree[u].lson || !tree[u].rson) // 链节点(叶节点)
      u = (tree[u].lson ? tree[u].lson : tree[u].rson);
    else if (tree[tree[u].lson].pri < tree[tree[u].rson].pri) { // 右旋
      zig(u);
      deleteNode(u,val); // 同层递归,类似循环这一步
    } else { // 左旋
      zag(u);
      deleteNode(u,val);
    }
    return ;
  }
  tree[u].size --; // 类比插入操作第9行
  if (val < tree[u].val) deleteNode(tree[u].lson,val);
  else deleteNode(tree[u].rson,val);
}

求节点在 \(Treap\) 中的前驱

定义:在平衡树中不大于该元素的最大元素(类似二分查找\(BST\)的元素查找)

实现方法:

  1. 从根节点开始访问(最初的答案为空节点
  2. 如果当前节点的值不大于查询值,更新答案,并查找右子节点
  3. 如果当前节点的值大于查询值,查找左子节点
  4. 当前节点为空,结束查找

因为答案的更新在递归中实现麻烦,所以采用循环的方法:

inline int queryPre(int val) { // 求前驱
  int u = root,ans = INT_MIN; // INT_MIN 只是用来判断该元素有没有前驱,可以根据题目取值
  while (u) { // 非空节点
    if (tree[u].val <= val) ans = tree[u].val,u = tree[u].rson;
    else u = tree[u].lson;
  }
  return ans;
}

求节点在 \(Treap\) 中的后继

定义:在平衡树中不小于该元素的最大元素(与求前驱思路一致)

实现方法:

  1. 从根节点开始访问(最初的答案为空节点
  2. 如果当前节点的值不小于查询值,更新答案,并查找左子节点
  3. 如果当前节点的值大于查询值,查找右子节点
  4. 当前节点为空,结束查找

因为答案的更新在递归中实现麻烦,所以采用循环的方法:

inline int querySuf(int val) { // 求后继
  int u = root,ans = INT_MAX; // INT_MAX 只是用来判断该元素有没有前驱,可以根据题目取值
  while (u) { // 非空节点
    if (tree[u].val >= val) ans = tree[u].val,u = tree[u].lson;
    else u = tree[u].rson;
  }
  return ans;
}

求在 \(Treap\) 中排名第 \(k\) 的元素

根据一条显而易见但非常重要的性质(\(Treap\) 中每一颗子树都是 \(Treap\)),我们显然可以想到利用分治的思想解决这个问题:

在一颗子树中,根 \(u\) 节点的排名取决于左子树的大小(左子树所有节点的值均小于 \(u\) ,则排名为 \(u.lson.size + 1\))。如果根有 \(cnt\) 个重复元素,那么 \(u\) 的排名为 \([u.lson.size+1,u.lson.size+u.cnt]\)。反着说就是:

  • \(k<u.lson.size+1\) 时,这个元素一定在左子树中
  • \(k>u.lson.size+u.cnt\) 时,这个元素一定在右子树中,且右子树中为排名第 \((k-(u.lson.size+u.cnt))\)减去左子树与根的排名)的元素
  • \(k\in [u.lson.size+1,u.lson.size+u.cnt]\) 时,当前节点包含排名第 \(k\) 的元素
inline int queryKth(int k) {
  int u = root;
  while (u) {
    int l = tree[tree[u].lson].size + 1,r = tree[tree[u].lson].size + tree[u].cnt;
    if (l <= k && k <= r) return tree[u].val; // 当前节点
    else if (k < l) u = tree[u].lson; // 左子树
    else u = tree[u].rson,k -= r; // 右子树
  }
  return 0; // 没找到
}

求元素在 \(Treap\) 中的排名

注意:有重复元素时,根据题意判断排名是第一个数还是其它数(本文为第一个数)

求元素的排名,本质上就是求有多少个元素小于它,排名就是个数加一

\(u\) 为当前节点,\(ans\) 为答案(小于它的元素个数),则:

  1. 若查询值等于当前节点,返回答案为 \(res + u.lson.size + 1\)
  2. 若查询值小于当前节点,在左子树中寻找排名
  3. 若查询值大于当前节点,更新 \(res=res+u.lson.size+u.cnt\),在右子树中寻找排名
inline void queryRank(int val) {
  int u = root,ans = 0;
  while (u) {
    if (val == tree[u].val) return ans + tree[tree[u].lson].size + 1;
    else if (val < tree[u].val) u = tree[u].lson;
    else ans += tree[tree[u].lson].size + tree[u].cnt,u = tree[u].rson;
  }
  return ans;
}
posted @ 2025-04-08 23:07  nightmare_lhh  阅读(22)  评论(0)    收藏  举报