Day4 平衡树 & 线段树

平衡树 & 线段树

搜索树

满足左子树权值比自己小、右子树权值比自己大(感觉操作很像线段树/树状数组上二分)

  • rank(x) < x <x <x 的元素个数,按照根节点与 x x x 的大小关系递归到左右子树中去
  • kth 求第 k k k 小元素,对于每个节点维护子树大小,与求 rank(x) 类似

需要支持插入/删除元素

  • 对于插入元素也是不断与根节点比较,递归到左右子树中,跑到叶子节点时构造一个新点,然后更新子树大小即可
  • 如果有重复元素就记录一个 c n t cnt cnt 表示每个值的出现次数即可

如果树构成一条链那么就会退化成 O ( n ) O(n) O(n) 的操作复杂度。由此出现了平衡树

平衡树

思路是不断改变树的形态使树的深度尽量小(达到 O ( log ⁡ n ) O(\log n) O(logn)

Treap

每个点随机分配一个权值,期望时间复杂度 O ( log ⁡ n ) O(\log n) O(logn)。Treap 上的每个点总是有有 ( k e y , r a n d ) (key,rand) (key,rand) 两个权值,后者为附加的随机值,则 Treap 上节点满足:

  • 二叉搜索树的两个性质( k e y key key);
  • 如果 v v v u u u 的子节点,那么有 r a n d ( u ) > r a n d ( v ) rand(u)>rand(v) rand(u)>rand(v)。堆的性质,保证平衡性。
struct Treap {
    int l,r,siz,v,rnd,w;
    // 左儿子 右儿子 子树大小 关键字 随机值 出现次数
    // 根据需要添加 tag、区间和等,和线段树类似
} tr[maxn];
void update(int k) {
    tr[k].siz = tr[tr[k].l].siz + tr[tr[k].r].siz + tr[k].w;
}

维护序列时的 k e y key key 总是设为下标,优势在于可以插入/删除元素。

FHQ-Treap

基本操作只有分裂和合并,实现思路主要通过将需要操作的部分分裂出来,进行修改后再使用合并操作粘回去。每个操作都是 O ( log ⁡ n ) O(\log n) O(logn) 的,实现简单故更为主流,对于整段区间的操作也很适合维护(如区间翻转、区间删除、区间插入)。

普通平衡树

维护以下操作:

  • 插入一个数
  • 删除一个数
  • 查询 x x x 的排名
  • 查询排名为 x x x 的数
  • x x x 前驱
  • x x x 后继

平衡树的直接应用,实现时总是以分裂 & 合并为主要思路。FHQ-Treap 记忆套路:对于实现中对左右子树不同的操作,总是截然不同甚至完全相反的。

-namespace FHQ_Treap {
    struct Node {
        int L, R;
        int key, pri, siz;
    } T[maxn << 1];
    int ntot = 0, root = 0;
    // 可以加入一个栈存储删除的节点,新创建节点时可以考虑使用之前空置的编号。
    int Create(int x) {
        T[++ ntot].siz = 1;
        T[ntot].L = T[ntot].R = 0;
        T[ntot].key = x, T[ntot].pri = rand();
        return ntot;
    }
    void update(int rt) {
        T[rt].siz = T[T[rt].L].siz + T[T[rt].R].siz + 1;
    }
    // l, r 表示分裂后的两颗子树的根节点。
    // 类似于二分,由于平衡树满足搜索树的性质,故按照自己的权值与目标权值进行比较,从而决定向哪颗子树继续分裂。
    void split(int rt,int val,int &l,int &r) { // 这是按照权值进行分裂。
        if (rt == 0) return l = r = 0, void(0); // 边界情况,分裂到了叶节点之外。
        if (T[rt].key <= val) { // 这一刀砍在哪里
            l = rt; split(T[rt].R,val,T[rt].R,r);
        } else {
            r = rt; split(T[rt].L,val,l,T[rt].L);
        } return update(rt);
    } // 按照子树大小进行分裂是类似的,不过递归解决时需要更改目标的子树大小
    int merge(int l,int r) {
        if (l == 0 || r == 0) return l + r;
        if (T[l].pri < T[r].pri) { // 按照优先级维护 FHQ-Treap 的平衡性
            T[l].R = merge(T[l].R, r);
            return update(l), l;
        } else {
            T[r].L = merge(l, T[r].L);
            return update(r), r;
        }
    }
    int insert(int val) {
        int l, r; split(root, val, l, r); // 把 val 的地方分裂出来
        int rt = Create(val);
        root = merge(merge(l,rt),r); // 逐步合并
        return rt;
    }
    int Delete(int val) {
        int l, mid, r;
        // 先得到包含待删除节点的根节点的子树,然后将子树中不属于待删除节点子树的节点进一步剥离
        split(root, val, l, r); split(l, val - 1, l, mid);
        mid = merge(T[mid].L, T[mid].R); // 此时 mid 就是待删除的节点,将 mid 两颗子树直接合并以湮灭 mid。
        // 如果需要保存删除节点的空置位,那就将 mid 的编号递扔进栈里。
        return root = merge(merge(l, mid), r); // 最后粘回去。
    }
    int findkth(int rt, int rnk) {
    	if (rnk == T[T[rt].L].siz + 1) // 如果找到了
    		return rt;
        // 类似于线段树上二分,讨论左右子树的大小,判断目标节点的方向
    	if (rnk <= T[T[rt].L].siz) 
    		return findkth(T[rt].L, rnk);
    	else return findkth(T[rt].R, rnk - T[T[rt].L].siz - 1);
    }
} 
区间操作

文艺平衡树

实现一颗支持区间翻转的平衡树。其中序列长度 n ≤ 1 0 5 n\le 10^5 n105,操作次数 m ≤ 1 0 5 m\le 10^5 m105

区间反转

区间翻转就是平衡树专属的操作。我们将区间下标作为第一键值建树,那么对于 FHQ-Treap 上的每个节点,都可以代表一段区间信息。具体地,我们存储这个节点自己的信息、左子树的信息和右子树的信息。不同于线段树的是,Treap 上的每个点不仅代表区间,在计算这个点的贡献时自己的值也需要被考虑。

考虑把每次操作的区间分裂出来,类似于线段树打个旋转 tag。在分裂与合并等基本操作时应该及时下传旋转标记并进行旋转操作。

维护数列

给定一个数列,维护以下操作:

  • 区间插入,即在指定位置插入一段区间
  • 区间删除
  • 区间修改
  • 区间翻转
  • 求区间和
  • 询问整体最大子段和

区间翻转操作同上。

区间插入

对于待加入的数列,我们考虑将它们先建成一个 Treap,然后再将它粘到主 Treap 里去。类似于线段树的建树操作,每次分治操作先将 [ L , m i d ] [L,mid] [L,mid] [ m i d + 1 , R ] [mid+1,R] [mid+1,R]中的点建好树,然后粘在一起。一开始对初始序列建树也可以这么做。

区间删除

由于我们将下标作为第一键值,所以对于一段连续的区间 [ l , r ] [l,r] [l,r] 一定可以被一个节点的子树表示出来,我们要做的就是把这个子树拆出来。将 FHQ-Treap 中前 l − 1 l-1 l1 个元素和后 n − r n-r nr 个元素剥离出来,对于中间的元素我们递归地删除它们,并将它们的编号扔进栈中。

区间修改/求区间和/求最大子段和

这一类在线段树上好实现的操作可以几乎照搬到平衡树上来。不同的是,合并区间信息时需要考虑根节点的信息。剩下的就和线段树操作基本相同了。

Splay

每次操作都考虑把操作的点转到根上,但是旋转时要判断三点共线折线两种情况进行旋转,这样操作复杂度就正确了(?noip 说不重要所以短时间内不打算学了。

替罪羊树

考虑左右孩子大小的比例,保持平衡的方法:定一个平衡因子 α \alpha α,操作与 bst 相同,但是插入时如果认为不平衡就拍扁重构。意思就是先中序遍历得到序列,然后用一个分治,每次把 m i d mid mid 作为根节点,左右区间递归下去作为左右子树。据说卡常专用。

文文的摄影布置

给出 n n n 个元素,第 i i i 个元素有两个权值 A i A_i Ai B i B_i Bi,令 f ( i , j ) f(i,j) f(i,j) 表示 A i + A j + min ⁡ i < k < j B k A_i+A_j+\min_{i<k<j}B_k Ai+Aj+mini<k<jBk,其中 i + 1 < j i+1<j i+1<j。共 m m m 次操作:

  • 1 x y 使 A x ← y A_x\gets y Axy
  • 2 x y 使 B x ← y B_x\gets y Bxy
  • 3 l r

max ⁡ l ≤ i < i + 1 < j ≤ r f ( i , j ) \max_{l\le i<i+1<j\le r}f(i,j) li<i+1<jrmaxf(i,j)

分类讨论 ( i , j , k ) (i,j,k) (i,j,k) 三元组的位置,然后线段树维护需要的值即可。

火星人

维护一个字符串序列,支持以下操作:

  • 单点插入;
  • 单点修改;
  • 查询两个区间的 LCP 即最长公共前缀的长度。

不管前面两个平衡树操作,考虑第三个询问。LCP 的一个做法即为二分答案,前缀哈希比对前面一段的长度。先考虑线段树,维护区间的前缀哈希,询问就做线段树上二分。最后把线段树操作改为平衡树即可支持插入操作。神奇的是这题直接用 STL 的 string 能过,均摊下来复杂度正确。

查找 Search

给定 n n n 个垃圾桶,你需要维护一个数据结构,支持以下操作:

  • 1 pos val 表示将 第 p o s pos pos 个垃圾桶里的垃圾的编号换成 v a l val val

  • 2 l r 询问在 [ l , r ] [l, r] [l,r] 内是否存在垃圾编号和为 w w w两个 垃圾桶。

对于每个操作 2,若存在请输出 Yes,不存在请输出 No。强制在线。

只考虑询问:记录前驱,即 p r e ( i ) pre(i) pre(i) 表示 i i i 左边最近的元素使得和为 w w w,预处理开一个桶从左往右扫一遍 O ( n ) O(n) O(n) 就能算完。考虑区间 [ l , r ] [l,r] [l,r],若对于所有的 l ≤ i ≤ r l\le i\le r lir p r e ( i ) < l pre(i)<l pre(i)<l 那么就是无解的。于是记录 p r e pre pre 的区间最大值即可。

现在考虑修改,仍然考虑 p r e pre pre 数组。注意到对于序列 { 1 , 2 , 2 } \{1,2,2\} {1,2,2},最后一个 2 2 2 对答案的贡献被离 1 1 1 最近的那个 2 2 2 覆盖了。于是我们每次只用修改最左边的 p r e pre pre 即可。将 x x x 替换为 y y y 相当于删去 x x x 然后原地插入 y y y。对于删除,我们要找到分别找到左右第 1 1 1 个的 x x x w − x w-x wx,即支持找到 i i i 位置右边第一个 = x =x =x 的值。考虑对于每个值用一个平衡树维护它的下标,那么查询就到对应的平衡树中去。实现时完全可以用 setlower_bound 实现。

  • 25
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值