学习笔记:fhq-Treap

fhq-Treap

无旋 Treap 的操作方式使得它天生支持维护序列、可持久化等特性。

无旋 Treap 又称分裂合并 Treap。它仅有两种核心操作,即为 分裂合并。通过这两种操作,在很多情况下可以比旋转 Treap 更方便的实现别的操作。下面逐一介绍这两种操作。讲解无旋 Treap 应当提到 FHQ-Treap(by 范浩强)。即可持久化,支持区间操作的无旋 Treap。更多内容请参照《范浩强谈数据结构》PPT。

分裂(split)

按值分裂

分裂过程接受两个参数:根指针 $\textit{cur}$、关键值 $\textit{key}$。结果为将根指针指向的 Treap 分裂为两个 Treap,第一个 Treap 所有结点的值($\textit{val}$)小于等于 $\textit{key}$,第二个 Treap 所有结点的值大于 $\textit{key}$。

该过程首先判断 $\textit{key}$ 是否小于 $\textit{cur}$ 的值,若小于,则说明 $\textit{cur}$ 及其右子树全部大于 $\textit{key}$,属于第二个 Treap。当然,也可能有一部分的左子树的值大于 $\textit{key}$,所以还需要继续向左子树递归地分裂。对于大于 $\textit{key}$ 的那部分左子树,我们把它作为 $\textit{cur}$ 的左子树,这样,整个 $\textit{cur}$ 上的节点都是大于 $\textit{key}$ 的。

相应的,如果 $\textit{key}$ 大于等于 $\textit{cur}$ 的值,说明 $\textit{cur}$ 的整个左子树以及其自身都小于 $\textit{key}$,属于分裂后的第一个 Treap。并且,$\textit{cur}$ 的部分右子树也可能有部分小于 $\textit{key}$,因此我们需要继续递归地分裂右子树。把小于 $\textit{key}$ 的那部分作为 $\textit{cur}$ 的右子树,这样,整个 $\textit{cur}$ 上的节点都小于 $\textit{key}$。

下图展示了 $\textit{cur}$ 的值小于等于 $\textit{key}$ 时按值分裂的情况。

void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void splitval(int node, int val, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[node].val > val){
        y = node;splitval(a[node].l, val, x, a[node].l);
    }else{
        x = node;splitval(a[node].r, val, a[node].r, y);
    }
    update(node);
}

按排名分裂

比起按值分裂,这个操作更像是旋转 Treap 中的根据排名(某个节点的排名是树中所有小于此节点值的节点的数量 $+ 1$)查询值:

此函数接受两个参数,节点指针 $\textit{cur}$ 和排名 $\textit{rk}$,返回分裂后的三个 Treap。

其中,第一个 Treap 中每个节点的排名都小于 $\textit{rk}$,第二个的排名等于 $\textit{rk}$,并且第二个 Treap 只有一个节点(不可能有多个等于的,如果有的话会增加 Node 结构体中的 cnt),第三个则是大于。

此操作的重点在于判断排名和 $\textit{cur}$ 相等的节点在树的哪个部分,这也是旋转 Treap 根据排名查询值操作时的重要部分,在前文有非常详细的解释,这里不过多讲解。

并且,此操作的递归部分和按值分裂也非常相似,这里不赘述。

void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void splitsiz(int node, int siz, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[a[node].l].siz >= siz){
        y = node;splitsiz(a[node].l, siz, x, a[node].l);
    }else{
        x = node;splitsiz(a[node].r, siz - a[a[node].l].siz - 1, a[node].r, y);
    }
    update(node);
}

合并(merge)

合并过程接受两个参数:左 Treap 的根指针 $\textit{u}$、右 Treap 的根指针 $\textit{v}$。必须满足 $\textit{u}$ 中所有结点的值小于等于 $\textit{v}$ 中所有结点的值。一般来说,我们合并的两个 Treap 都是原来从一个 Treap 中分裂出去的,所以不难满足 $\textit{u}$ 中所有节点的值都小于 $\textit{v}$。

在旋转 Treap 中,我们借助旋转操作来维护 $\textit{priority}$ 符合堆的性质,同时旋转时还不能改变树的性质。在无旋 Treap 中,我们用合并达到相同的效果。

因为两个 Treap 已经有序,所以我们在合并的时候只需要考虑把哪个树「放在上面」,把哪个「放在下面」,也就是是需要判断将哪个一个树作为子树。显然,根据堆的性质,我们需要把 $\textit{priority}$ 小的放在上面(这里采用小根堆)。

同时,我们还需要满足搜索树的性质,所以若 $\textit{u}$ 的根结点的 $\textit{priority}$ 小于 $\textit{v}$ 的,那么 $\textit{u}$ 即为新根结点,并且 $\textit{v}$ 因为值比 $\textit{u}$ 更大,应与 $\textit{u}$ 的右子树合并;反之,则 $\textit{v}$ 作为新根结点,然后因为 $u$ 的值比 $\textit{v}$ 小,与 $v$ 的左子树合并。

int merge(int x, int y){
    if(x == 0 || y == 0)return x | y;
    int ans;
    if(a[x].hap > a[y].hap){
        ans = x;a[x].r = merge(a[x].r, y);
    }else{
        ans = y;a[y].l = merge(x, a[y].l);
    }
    update(ans);return ans;
}

插入

在无旋 Treap 中,插入,删除,根据值查询排名等基础操作既可以用普通二叉查找树的方法实现,也可以用分裂和合并来实现。通常来说,使用分裂和合并来实现更加简洁,但是速度会慢一点。为了帮助更好的理解无旋 Treap,下面的操作全部使用分裂和合并实现。

在实现插入操作时,我们利用了分裂操作的一些性质。也就是值小于等于 $\textit{val}$ 的节点会被分到第一个 Treap。

所以,假设我们根据 $\textit{val}$ 分裂当前这个 Treap。会有下面两棵树,并符合以下条件:

$$ \begin{aligned} T_1 &\le val\\ T_2 &> val \end{aligned} $$

其中 $T_1$ 表示分裂后所有被分到第一个 Treap 的节点的集合,$T_2$ 则是第二个。

如果我们再按照 $\textit{val} - 1$ 继续分裂 $T_1$,那么会产生下面两棵树,并符合以下条件:

$$ \begin{gathered} T_{1\ \text{left}} \le val - 1\\ T_{1\ \text{right}} > val - 1 \ \And \ T_{1\ \text{right}} \le val \end{gathered} $$

其中 $T_{1\ \text{left}}$ 表示 $T_1$ 分裂后所有被分到第一个 Treap 的节点的集合,$T_{1\ \text{right}}$ 则是第二个。并且上面的式子中,后半部分的 $\And \ T_{1\ \text{right}} \le val$ 来自于 $T_1$ 所符合的条件 $T_1 \le val$。

不难发现,只要 $\textit{val}$ 和节点的值是一个整数(大多数使用场景下会使用整数)那么符合 $T_{1\ \text{right}}$ 条件的节点只有一个,也就是值等于 $\textit{val}$ 的节点。

在插入时,如果我们发现符合 $T_{1\ \text{right}}$ 的节点存在,那就可以直接增加重复次数,否则,就新开一个节点。

注意把树分裂好了还需要用合并操作把它「粘」回去,这样下次还能继续使用。并且,还需要注意合并操作的参数顺序是有要求的,第一个树的所有节点的值都需要小于第二个。

void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void splitval(int node, int val, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[node].val > val){
        y = node;splitval(a[node].l, val, x, a[node].l);
    }else{
        x = node;splitval(a[node].r, val, a[node].r, y);
    }
    update(node);
}
int merge(int x, int y){
    if(x == 0 || y == 0)return x | y;
    int ans;
    if(a[x].hap > a[y].hap){
        ans = x;a[x].r = merge(a[x].r, y);
    }else{
        ans = y;a[y].l = merge(x, a[y].l);
    }
    update(ans);return ans;
}
int add(int val){
    tot++;a[tot].hap = rand();
    a[tot].siz = 1;a[tot].val = val;
    return tot;
}
void insert(int val){
    int x, y;
    splitval(root, val - 1, x, y);
    root = merge(merge(x, add(val)), y);
}

删除

删除操作也使用和插入操作相似的方法,找到值和 $\textit{val}$ 相等的节点,并且删除它。

void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void splitsiz(int node, int siz, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[a[node].l].siz >= siz){
        y = node;splitsiz(a[node].l, siz, x, a[node].l);
    }else{
        x = node;splitsiz(a[node].r, siz - a[a[node].l].siz - 1, a[node].r, y);
    }
    update(node);
}
void splitval(int node, int val, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[node].val > val){
        y = node;splitval(a[node].l, val, x, a[node].l);
    }else{
        x = node;splitval(a[node].r, val, a[node].r, y);
    }
    update(node);
}
int merge(int x, int y){
    if(x == 0 || y == 0)return x | y;
    int ans;
    if(a[x].hap > a[y].hap){
        ans = x;a[x].r = merge(a[x].r, y);
    }else{
        ans = y;a[y].l = merge(x, a[y].l);
    }
    update(ans);return ans;
}
void remove(int val){
    int x, y, z, t;
    splitval(root, val - 1, x, y);
    splitsiz(y, 1, z, t);
    root = merge(x, t);
}

根据值查询排名

排名是比这个值小的节点的数量 $+ 1$,所以我们根据 $\textit{val} - 1$ 分裂当前树,那么分裂后的第一个树就符合:

$$ T_1 \le val - 1 $$

如果树的值和 $\textit{val}$ 为整数,那么 $T_1$ 就包含了所有值小于 $\textit{val}$ 的节点。

void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void splitval(int node, int val, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[node].val > val){
        y = node;splitval(a[node].l, val, x, a[node].l);
    }else{
        x = node;splitval(a[node].r, val, a[node].r, y);
    }
    update(node);
}
int merge(int x, int y){
    if(x == 0 || y == 0)return x | y;
    int ans;
    if(a[x].hap > a[y].hap){
        ans = x;a[x].r = merge(a[x].r, y);
    }else{
        ans = y;a[y].l = merge(x, a[y].l);
    }
    update(ans);return ans;
}
int getrank(int val){
    int x, y, ans;
    splitval(root, val - 1, x, y);
    ans = a[x].siz + 1;
    root = merge(x, y);
    return ans;
}

根据排名查询值

调用 getval() 函数后,会返回分裂好的三个 Treap,其中第二个只包含一个节点,它的排名等于 $\textit{rk}$,所以我们直接返回这个节点的 $\textit{val}$。

void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void splitsiz(int node, int siz, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[a[node].l].siz >= siz){
        y = node;splitsiz(a[node].l, siz, x, a[node].l);
    }else{
        x = node;splitsiz(a[node].r, siz - a[a[node].l].siz - 1, a[node].r, y);
    }
    update(node);
}
int merge(int x, int y){
    if(x == 0 || y == 0)return x | y;
    int ans;
    if(a[x].hap > a[y].hap){
        ans = x;a[x].r = merge(a[x].r, y);
    }else{
        ans = y;a[y].l = merge(x, a[y].l);
    }
    update(ans);return ans;
}
int getval(int rank){
    int x, y, z, t, ans;
    splitsiz(root, rank - 1, x, y);
    splitsiz(y, 1, z, t);
    ans = a[z].val;
    root = merge(merge(x, z), t);
    return ans;
}

查询第一个比 val 小的节点

可以把这个问题转化为,在比 $\textit{val}$ 小的所有节点中,找出排名最大的。我们根据 $\textit{val}$ 来分裂这个 Treap,返回的第一个 Treap 中的节点的值就全部小于 $\textit{val}$,然后我们调用 getpre() 找出这个树中值最大的节点。

void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void splitsiz(int node, int siz, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[a[node].l].siz >= siz){
        y = node;splitsiz(a[node].l, siz, x, a[node].l);
    }else{
        x = node;splitsiz(a[node].r, siz - a[a[node].l].siz - 1, a[node].r, y);
    }
    update(node);
}
void splitval(int node, int val, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[node].val > val){
        y = node;splitval(a[node].l, val, x, a[node].l);
    }else{
        x = node;splitval(a[node].r, val, a[node].r, y);
    }
    update(node);
}
int merge(int x, int y){
    if(x == 0 || y == 0)return x | y;
    int ans;
    if(a[x].hap > a[y].hap){
        ans = x;a[x].r = merge(a[x].r, y);
    }else{
        ans = y;a[y].l = merge(x, a[y].l);
    }
    update(ans);return ans;
}
int getpre(int val){
    int x, y, z, t, ans;
    splitval(root, val - 1, x, y);
    splitsiz(x, a[x].siz - 1, z, t);
    ans = a[t].val;
    root = merge(merge(z, t), y);
    return ans;
}

查询第一个比 val 大的节点

和上个操作类似,可以把这个问题转化为,在比 $\textit{val}$ 大的所有节点中,找出排名最大的。那么根据 $\textit{val}$ 分裂后,返回的第二个 Treap 中的所有节点的值就大于 $\textit{val}$。

然后我们去查询这个树中排名为 $1$ 的节点(也就是值最小的节点)的值,就可以成功查到第一个比 $\textit{val}$ 大的节点。

void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void splitsiz(int node, int siz, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[a[node].l].siz >= siz){
        y = node;splitsiz(a[node].l, siz, x, a[node].l);
    }else{
        x = node;splitsiz(a[node].r, siz - a[a[node].l].siz - 1, a[node].r, y);
    }
    update(node);
}
void splitval(int node, int val, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[node].val > val){
        y = node;splitval(a[node].l, val, x, a[node].l);
    }else{
        x = node;splitval(a[node].r, val, a[node].r, y);
    }
    update(node);
}
int merge(int x, int y){
    if(x == 0 || y == 0)return x | y;
    int ans;
    if(a[x].hap > a[y].hap){
        ans = x;a[x].r = merge(a[x].r, y);
    }else{
        ans = y;a[y].l = merge(x, a[y].l);
    }
    update(ans);return ans;
}
int getnxt(int val){
    int x, y, z, t, ans;
    splitval(root, val, x, y);
    splitsiz(y, 1, z, t);
    ans = a[z].val;
    root = merge(merge(x, z), t);
    return ans;
}

建树(build)

将一个有 $n$ 个节点的序列 $\{a_n\}$ 转化为一棵 Treap。

可以依次暴力插入这 $n$ 个节点,每次插入一个权值为 $v$ 的节点时,将整棵 Treap 按照权值分裂成权值小于等于 $v$ 的和权值大于 $v$ 的两部分,然后新建一个权值为 $v$ 的节点,将两部分和新节点按从小到大的顺序依次合并,单次插入时间复杂度 $O(\log n)$,总时间复杂度 $O(n\log n)$。

在某些题目内,可能会有多次插入一段有序序列的操作,这是就需要在 $O(n)$ 的时间复杂度内完成建树操作。

方法一:在递归建树的过程中,每次选取当前区间的中点作为该区间的树根,并对每个节点钦定合适的优先值,使得新树满足堆的性质。这样能保证树高为 $O(\log n)$。

方法二:在递归建树的过程中,每次选取当前区间的中点作为该区间的树根,然后给每个节点一个随机优先级。这样能保证树高为 $O(\log n)$,但不保证其满足堆的性质。这样也是正确的,因为无旋式 Treap 的优先级是用来使 merge 操作更加随机一点,而不是用来保证树高的。

方法三:观察到 Treap 是笛卡尔树,利用笛卡尔树的 $O(n)$ 建树方法即可,用单调栈维护右链即可。

无旋 Treap 的区间操作

建树

无旋 Treap 相比旋转 Treap 的一大好处就是可以实现各种区间操作,下面我们以文艺平衡树的 模板题 为例,介绍 Treap 的区间操作。

您需要写一种数据结构(可参考题目标题),来维护一个有序数列。

其中需要提供以下操作:翻转一个区间,例如原有序序列是 $5\ 4\ 3\ 2\ 1$,翻转区间是 $[2,4]$ 的话,结果是 $5\ 2\ 3\ 4\ 1$。 对于 $100\%$ 的数据,$1 \le n$(初始区间长度)$m$(翻转次数)$\le 1e5$

在这道题目中,我们需要实现的是区间翻转,那么我们首先需要考虑如何建树,建出来的树需要是初始的区间。

我们只需要把区间的下标依次插入 Treap 中,这样在中序遍历(先遍历左子树,然后当前节点,最后右子树)时,就可以得到这个区间。

我们知道在朴素的二叉查找树中按照递增的顺序插入节点,建出来的树是一个长链,按照中序遍历,自然可以得到这个区间。

如上图,按照 $1\ 2\ 3\ 4\ 5$ 的顺序给朴素搜索树插入节点,中序遍历时,得到的也是 $1\ 2\ 3\ 4\ 5$。

但是在 Treap 中,按增序插入节点后,在合并操作时还会根据 $\textit{priority}$ 调整树的结构,在这样的情况下,如何确保中序遍历一定能正确的输出呢?

可以参考笛卡尔树的单调栈建树方法来理解这个问题。

设新插入的节点为 $\textit{u}$。

首先,因为是递增地插入节点,每一个新插入的节点肯定会被连接到 Treap 的右链(即从根结点一直往右子树走,经过的结点形成的链)上。

从根节点开始,右链上的节点的 $\textit{priority}$ 是递增的(小根堆)。那我们可以找到右链上第一个 $\textit{priority}$ 大于 $\textit{u}$ 的节点,我们叫这个节点 $\textit{v}$,并把这个节点换成 $\textit{u}$。

因为 $\textit{u}$ 一定大于这个树上其他的全部节点,我们需要把 $\textit{v}$ 以及它的子树作为 $\textit{u}$ 的左子树。并且此时 $\textit{u}$ 没有右子树。

可以发现,中序遍历时 $\textit{u}$ 一定是最后一个被遍历到的(因为 $\textit{u}$ 是右链中的最后一个,而中序遍历中,右子树是最后被遍历到的)。

下图是一个 Treap 根据递增顺序插入 $1 \sim 5$ 号节点时,插入 $5$ 号节点时的变化,可以用这张图更好的理解按照增序插入的过程。

区间翻转

翻转 $[l, r]$ 这个区间时,基本思路是将树分裂成 $[1, l - 1],\ [l, r],\ [r + 1, n]$ 三个区间,再对中间的 $[l, r]$ 进行翻转。

翻转的具体操作是把区间内的子树的每一个左,右子节点交换位置。如下图就展示了翻转上图中 Treap 的 $[3, 4]$ 和 $[3, 5]$ 区间后的 Treap。

注意如果按照这个方法翻转,那么每次翻转 $[l, r]$ 区间时,就会有 $r - l$ 个节点会被交换位置,这样频繁的操作显然不能满足 $10^5$ 的数据范围,其 $O(n \times \log_2 n)$ 的单次翻转复杂度甚至不如暴力(因为我们除了需要花线性时间交换节点外,还需要在树中花费 $O(\log_2 n)$ 的时间找到需要交换的节点)。

再观察题目要求,可以发现因为只需要最后输出操作完的区间,所以并不需要每次都真的去交换。如此一来,便可以使用线段树中常用的懒标记(lazy tag)来优化复杂度。交换时,只需要在父节点打上标记,代表这个子树下的每个左右子节点都需要交换就行了。

在线段树中,我们一般在更新和查询时下传懒标记。这是因为,在更新和查询时,我们想要更新/查询的范围不一定和懒标记代表的范围重合,所以要先下传标记,确保查到和更新后的值是正确的。

在无旋 Treap 中也是一样。具体操作时我们会把 Treap 分裂成前文讲到的三个树,然后给中间的树打上懒标记后合并这三棵树。因为我们想要翻转的区间和懒标记代表的区间不一定重合,所以要在分裂时下传标记。并且,分裂和合并操作会造成每个节点及其懒标记所代表的节点发生变动,所以也需要在合并前下传懒标记。

换句话说,是当树的结构发生改变的时候,当我们进行分裂或合并操作时需要改变某一个点的左右儿子信息时之前,应该下放标记,而非之后,因为懒标记是需要下传给儿子节点的,但更改左右儿子信息之后若懒标记还未下放,则懒标记就丢失了下放的对象。

因为区间操作中大部分操作都和普通的无旋 Treap 相同,所以这里只讲解和普通无旋 Treap 不同的地方。

下传标记

需要注意这里的懒标记代表需要把这个树中的每一个子节点交换位置。所以如果当前节点的子节点也有懒标记,那两次翻转就抵消了。如果子节点不需要翻转,那么这个懒标记就需要继续被下传到子节点上。

void pushdown(int node){
    swap(a[node].l, a[node].r);
    if(a[node].l != 0)a[a[node].l].flag ^= 1;
    if(a[node].r != 0)a[a[node].r].flag ^= 1;
    a[node].flag = false;
}

分裂

注意在这个题目中,因为翻转操作,Treap 中的 $\textit{val}$ 会不符合二叉搜索树的性质(见区间翻转部分的图),所以我们不能根据 $\textit{val}$ 来判断应该往左子树还是右子树递归。

所以这里的分裂跟普通无旋 Treap 中的按排名分裂更相似,是根据当前树的大小判断往左还是右子树递归的,换言之,我们是按照开始时这个节点在树中的位置来判断的。

返回的第一个 Treap 中节点的排名全部小于等于 $\textit{sz}$,而第二个 Treap 中节点的排名则全部大于 $\textit{sz}$。

void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void pushdown(int node){
    swap(a[node].l, a[node].r);
    if(a[node].l != 0)a[a[node].l].flag ^= 1;
    if(a[node].r != 0)a[a[node].r].flag ^= 1;
    a[node].flag = false;
}
void splitsiz(int node, int siz, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[node].flag == true)pushdown(node);
    if(a[a[node].l].siz >= siz){
        y = node;splitsiz(a[node].l, siz, x, a[node].l);
    }else{
        x = node;splitsiz(a[node].r, siz - a[a[node].l].siz - 1, a[node].r, y);
    }
    update(node);
}

合并

唯一需要注意的是在合并前下传懒标记。

void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void pushdown(int node){
    swap(a[node].l, a[node].r);
    if(a[node].l != 0)a[a[node].l].flag ^= 1;
    if(a[node].r != 0)a[a[node].r].flag ^= 1;
    a[node].flag = false;
}
int merge(int x, int y){
    if(x == 0 || y == 0)return x | y;
    if(a[x].hap > a[y].hap){
        if(a[x].flag == true)pushdown(x);
        a[x].r = merge(a[x].r, y);
        update(x);return x;
    }else{
        if(a[y].flag == true)pushdown(y);
        a[y].l = merge(x, a[y].l);
        update(y);return y;
    }
}

区间翻转

和前面介绍的一样,分裂出 $[1, l - 1],\ [l, r],\ [r + 1, n]$ 三个区间,然后对中间的区间打上标记后再合并。

    for(int i = 1 ; i <= m ; i ++){
        int l, r, x, y, z;
        l = read();r = read();
        splitsiz(root, l - 1, x, y);
        splitsiz(y, r - l + 1, y, z);
        a[y].flag ^= 1;
        root = merge(x, merge(y, z));
    }

中序遍历打印

要注意在打印时要下传标记。

void getans(int node){
    if(node == 0)return;
    if(a[node].flag == true)pushdown(node);
    getans(a[node].l);
    if(flag == true)putchar(' ');
    write(a[node].val);flag = true;
    getans(a[node].r);
}

完整代码

无旋 Treap

以下是前文讲解的代码的完整版本,是普通平衡树的模板代码。

#include <iostream>
#include <cstdlib>
#define MAXN 100005
using namespace std;
int n, opt, x, root, tot;
struct Treap{
    int l, r, val, siz, hap;
}a[MAXN];
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 ^ 48);
}
int add(int val){
    tot++;a[tot].hap = rand();
    a[tot].siz = 1;a[tot].val = val;
    return tot;
}
void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void splitsiz(int node, int siz, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[a[node].l].siz >= siz){
        y = node;splitsiz(a[node].l, siz, x, a[node].l);
    }else{
        x = node;splitsiz(a[node].r, siz - a[a[node].l].siz - 1, a[node].r, y);
    }
    update(node);
}
void splitval(int node, int val, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[node].val > val){
        y = node;splitval(a[node].l, val, x, a[node].l);
    }else{
        x = node;splitval(a[node].r, val, a[node].r, y);
    }
    update(node);
}
int merge(int x, int y){
    if(x == 0 || y == 0)return x | y;
    int ans;
    if(a[x].hap > a[y].hap){
        ans = x;a[x].r = merge(a[x].r, y);
    }else{
        ans = y;a[y].l = merge(x, a[y].l);
    }
    update(ans);return ans;
}
void insert(int val){
    int x, y;
    splitval(root, val - 1, x, y);
    root = merge(merge(x, add(val)), y);
}
void remove(int val){
    int x, y, z, t;
    splitval(root, val - 1, x, y);
    splitsiz(y, 1, z, t);
    root = merge(x, t);
}
int getrank(int val){
    int x, y, ans;
    splitval(root, val - 1, x, y);
    ans = a[x].siz + 1;
    root = merge(x, y);
    return ans;
}
int getval(int rank){
    int x, y, z, t, ans;
    splitsiz(root, rank - 1, x, y);
    splitsiz(y, 1, z, t);
    ans = a[z].val;
    root = merge(merge(x, z), t);
    return ans;
}
int getpre(int val){
    int x, y, z, t, ans;
    splitval(root, val - 1, x, y);
    splitsiz(x, a[x].siz - 1, z, t);
    ans = a[t].val;
    root = merge(merge(z, t), y);
    return ans;
}
int getnxt(int val){
    int x, y, z, t, ans;
    splitval(root, val, x, y);
    splitsiz(y, 1, z, t);
    ans = a[z].val;
    root = merge(merge(x, z), t);
    return ans;
}
int main(){
    n = read();srand(time(0));
    for(int i = 1 ; i <= n ; i ++){
        opt = read();x = read();
        switch(opt){
            case 1:insert(x);break;
            case 2:remove(x);break;
            case 3:write(getrank(x));putchar('\n');break;
            case 4:write(getval(x));putchar('\n');break;
            case 5:write(getpre(x));putchar('\n');break;
            case 6:write(getnxt(x));putchar('\n');break;
            default:break;
        }
    }
    return 0;
}

无旋 Treap 的区间操作

以下是前文讲解的代码的完整版本,是文艺平衡树的模板代码。

#include <iostream>
#include <cstdlib>
#define MAXN 100005
using namespace std;
int n, m, root, tot;
struct Treap{
    int l, r, val, siz, hap;
    bool flag;
}a[MAXN];
bool flag;
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 ^ 48);
}
int add(int val){
    tot++;a[tot].hap = rand();
    a[tot].siz = 1;a[tot].val = val;
    return tot;
}
void update(int node){
    a[node].siz = a[a[node].l].siz + a[a[node].r].siz + 1;
}
void pushdown(int node){
    swap(a[node].l, a[node].r);
    if(a[node].l != 0)a[a[node].l].flag ^= 1;
    if(a[node].r != 0)a[a[node].r].flag ^= 1;
    a[node].flag = false;
}
void splitsiz(int node, int siz, int &x, int &y){
    if(node == 0){x = 0;y = 0;return;}
    if(a[node].flag == true)pushdown(node);
    if(a[a[node].l].siz >= siz){
        y = node;splitsiz(a[node].l, siz, x, a[node].l);
    }else{
        x = node;splitsiz(a[node].r, siz - a[a[node].l].siz - 1, a[node].r, y);
    }
    update(node);
}
int merge(int x, int y){
    if(x == 0 || y == 0)return x | y;
    if(a[x].hap > a[y].hap){
        if(a[x].flag == true)pushdown(x);
        a[x].r = merge(a[x].r, y);
        update(x);return x;
    }else{
        if(a[y].flag == true)pushdown(y);
        a[y].l = merge(x, a[y].l);
        update(y);return y;
    }
}
void getans(int node){
    if(node == 0)return;
    if(a[node].flag == true)pushdown(node);
    getans(a[node].l);
    if(flag == true)putchar(' ');
    write(a[node].val);flag = true;
    getans(a[node].r);
}
int main(){
    n = read();m = read();srand(time(0));
    for(int i = 1 ; i <= n ; i ++)
        root = merge(root, add(i));
    for(int i = 1 ; i <= m ; i ++){
        int l, r, x, y, z;
        l = read();r = read();
        splitsiz(root, l - 1, x, y);
        splitsiz(y, r - l + 1, y, z);
        a[y].flag ^= 1;
        root = merge(x, merge(y, z));
    }
    getans(root);putchar('\n');return 0;
}
posted @ 2023-10-12 10:44  tsqtsqtsq  阅读(57)  评论(0)    收藏  举报  来源