Day4 平衡树 & 线段树

平衡树 & 线段树

搜索树

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

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

需要支持插入/删除元素

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

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

平衡树

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

Treap

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

  • 二叉搜索树的两个性质(keykeykey);
  • 如果 vvvuuu 的子节点,那么有 rand(u)>rand(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;
}

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

FHQ-Treap

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

普通平衡树

维护以下操作:

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

平衡树的直接应用,实现时总是以分裂 & 合并为主要思路。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≤105n\le 10^5n105,操作次数 m≤105m\le 10^5m105

区间反转

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

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

维护数列

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

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

区间翻转操作同上。

区间插入

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

区间删除

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

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

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

Splay

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

替罪羊树

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

文文的摄影布置

给出 nnn 个元素,第 iii 个元素有两个权值 AiA_iAiBiB_iBi,令 f(i,j)f(i,j)f(i,j) 表示 Ai+Aj+min⁡i<k<jBkA_i+A_j+\min_{i<k<j}B_kAi+Aj+mini<k<jBk,其中 i+1<ji+1<ji+1<j。共 mmm 次操作:

  • 1 x y 使 Ax←yA_x\gets yAxy
  • 2 x y 使 Bx←yB_x\gets yBxy
  • 3 l r

max⁡l≤i<i+1<j≤rf(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

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

  • 1 pos val 表示将 第 pospospos 个垃圾桶里的垃圾的编号换成 valvalval

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

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

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

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

### 代码存在的问题 1. **拼写错误**:在代码中 `print(&quot;yes&quot;);` 应为 `printf(&quot;yes&quot;);`,`print` 不是 C 语言标准库中的输出函数,正确的输出函数是 `printf`。 2. **逻辑结构问题**:在 `if(wu&gt;200&amp;&amp;wu&lt;=400)` 分支中,`else` 语句只与最后一个 `if(day==5&amp;&amp;(a==5||a==0))` 匹配,这可能不是预期的逻辑。如果想要在 `day` 不满足前面几个 `if` 条件时输出 `&quot;yes&quot;`,需要更好的逻辑结构。可以使用 `else if` 来改进逻辑。 3. **范围检查**:代码没有对输入的 `day`、`wu` 和 `n` 进行有效性检查,例如 `day` 应该在 1 - 5 之间。 ### 修改后的代码 ```c #include&lt;stdio.h&gt; int main() { int day, wu, n; scanf(&quot;%d %d %d&quot;, &amp;day, &amp;wu, &amp;n); int a = n % 10; // 检查输入的有效性 if (day &lt; 1 || day &gt; 5) { printf(&quot;Invalid input for day.\n&quot;); return 1; } if (wu &lt;= 200) { printf(&quot;yes&quot;); } else if (wu &gt; 200 &amp;&amp; wu &lt;= 400) { if (day == 1 &amp;&amp; (a == 1 || a == 6)) { printf(&quot;no&quot;); } else if (day == 2 &amp;&amp; (a == 2 || a == 7)) { printf(&quot;no&quot;); } else if (day == 3 &amp;&amp; (a == 3 || a == 8)) { printf(&quot;no&quot;); } else if (day == 4 &amp;&amp; (a == 4 || a == 9)) { printf(&quot;no&quot;); } else if (day == 5 &amp;&amp; (a == 5 || a == 0)) { printf(&quot;no&quot;); } else { printf(&quot;yes&quot;); } } else { if ((day == 1 || day == 3 || day == 5) &amp;&amp; (a == 1 || a == 3 || a == 5 || a == 7 || a == 9)) { printf(&quot;no&quot;); } else if ((day == 2 || day == 4) &amp;&amp; (a == 0 || a == 2 || a == 4 || a == 6 || a == 8)) { printf(&quot;no&quot;); } else { printf(&quot;yes&quot;); } } return 0; } ``` ### 关于 `ld returned 1 exit status` 错误 `ld returned 1 exit status` 错误通常是链接器错误。在这个代码中,如果之前遇到这个错误,可能是因为拼写错误(如 `print`)导致链接器找不到对应的函数定义。修正拼写错误后,重新编译应该可以解决这个问题。如果仍然遇到该错误,可能是其他原因,如编译器配置问题、库文件缺失等。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值