【模板】fhq-treap

一、什么是\(fhq-treap\)

\(fhq-treap\):非旋转\(treap\),顾名思义,不用像普通\(treap\)那样繁琐的旋转,只需要通过分裂和合并,就可以实现基本上是所有数据结构能实现的操作,并且短小、精悍,时间复杂度与\(splay\)齐当,算是一个十分易懂且优秀的算法(并不需要提前学习普通\(treap\))

接下来,我们就来看看\(fhq-treap\)是怎么实现的


二、\(fhq-treap\) 的性质

在这棵平衡树里面,我们保证

左子树节点的权值(在序列中的位置)全部小于根的权值(在序列中的位置),
右子树节点的权值(在序列中的位置)全部大于根的权值(在序列中的位置),
所有分裂和合并操作都满足这个性质

我们还得考虑一个灵魂拷问:为什么\(fhq-treap\)叫平衡树?

因为它满足平衡性,可以达到期望层数\(logn\)层,大大减小了时间复杂度

妈妈再也不用担心我的时间复杂度了


三、\(fhq-treap\) 的两大操作

首先,我们先来明确一下\(fhq-treap\)里面的基本变量

\(ch[2]\):两个儿子
\(siz\):以这个节点为根的子树的大小
\(val\):这个节点的权值
\(rd\):这个节点的随机权值,用来保证树的平衡性,在新建节点时由\(rand\)得到
\(rev\):这个节点所代表区间的旋转标记,为0或1

\(Split\):分裂操作

在题目中,有可能以权值排序建树,也可能以序列顺序建树

这里以权值排序建树

我们将一棵树分裂为两部分:权值<=k和权值>k的,我们称这两部分的节点组成的树为\(x\)和\(y\)

那么怎么分裂呢?

我们考虑,如果当前节点的权值大于k

那么因为右子树的节点权值明显全部都比k大,所以当前节点以及右子树的节点全部加入\(y\)树中,接着继续分裂左子树

如果小于等于k同理

因为它的\(siz\)改变了,所以最后记得\(update\)当前的树

void split(int now,int k,int &x,int &y)
//将以now为根的子树中权值<=k的节点分裂为x子树,>k的节点分裂为y子树
{
    if(!now)x=y=0;
    else
    {
        if(k<t[now].val)//如果当前根的val大于k
        {
            y=now;//加入y树中
            split(t[now].ch[0],k,x,t[y].ch[0]);
            //分裂左子树,因为接下来遍历到的节点权值一定小于当前的权值
            //所以如果有满足要求的,全部接到y的左子树上,满足性质
            up(y);
        }
        else
        {
            x=now;//加入x树中
            split(t[now].ch[1],k,t[x].ch[1],y);//与上同理
            up(x);
        }
    }
}

这是以序列顺序建树的:

void split(int now,int k,int &x,int &y)
{
    if(!now)x=y=0;
    else
    {
        if(k<=t[t[now].ch[0]].siz)
        {
            y=now;
            split(t[now].ch[0],k,x,t[y].ch[0]);
            up(y);
        }
        else
        {
            x=now;
            split(t[now].ch[1],k-t[t[now].ch[0]].siz-1,t[x].ch[1],y);
            //因为k>左子树的siz,说明k在右子树中
            //那么在遍历右子树的时候,所查找的排名要减去左子树的siz和这个节点
            up(x);;
        }
    }
}

\(Merge\):合并操作

我们把子树分裂出来后,肯定要将子树合并回去

所以合并要怎么写呢?

我们在合并时,要考虑到上述的平衡性,所以在合并时要满足平衡性,就要用到随机权值\(rd\)辅助

如果要将\(x\),\(y\)合并,那么当\(t[x].rd<t[y[.rd\)时,我们就将树\(y\)接到树\(x\)上,反之将树\(x\)接到树\(y\)上

在这里合并时,因为已经满足了左子树节点的权值小于右子树节点的权值,所以每次合并时

对于接到树\(x\)上的情况,要接到树\(x\)的右子树上,对于接到树\(y\)上的情况,要接到树\(y\)的左子树上

同样最后也要\(update\)

int merge(int x,int y)
{
    if(!(x&&y))return x+y;
    if(t[x].rd<t[y].rd)
    {
        t[x].ch[1]=merge(t[x].ch[1],y);
        up(x);
        return x;
    }
    else
    {
        t[y].ch[0]=merge(x,t[y].ch[0]);
        up(y);
        return y;
    }
}

四、其他操作

\(newnode\):新建节点

int newnode(int x)
{
    t[++nodetot].val=x;
    t[nodetot].rd=rand();
    t[nodetot].siz=1;
    return nodetot;
}

\(ins\):插入操作

我们找出当前节点\(x\)要插入的位置,将整棵平衡树分裂为\(a\),\(b\)两棵子树,分别表示\(<=x\)和\(>x\),再依次合并\(a,x,b\)

以按大小顺序建树:

void ins(int x)
{
    int a,b,c;
    split(rt,x,a,b);//a树表示<=x,b树表示>x
    rt=merge(merge(a,newnode(x)),b);//将x插入其中
}

\(del\):删除操作

对于要删除的节点权值\(x\),我们先分裂出\(a,b\)子树,表示\(<=x\)和\(>x\),再把\(a\)分裂出一个\(c\)子树,使得\(a\)树\(<=x-1\),这样,树\(c\)就是\(x\)

这时,树\(a,b,c\)分别表示\(<=x-1\),\(x\),\(>x\)

再直接合并树\(c\)的左子树和右子树,相当于删除权值为\(x\)的节点,再依次合并\(a,c,b\)

void del(int x)

{
    int a,b,c;
    split(rt,x,a,b);
    split(a,x-1,a,c);
    c=merge(t[c].ch[0],t[c].ch[1]);
    rt=merge(merge(a,c),b);

}

\(rnk\):查询\(x\)数的排名

先将原树分裂成\(a(<=(x-1)),b(>(x-1))\)树,括号里面代表节点范围,那么\(t[a].siz\)代表\(<=(x-1)\)的数的个数,再加上\(1\)就是\(x\)的排名

int rnk(int x)
{
    int a,b,c;
    split(rt,x-1,a,b);
    int ans=t[a].siz+1;
    rt=merge(a,b);
    return ans;
}

\(kth\):查询排名为\(k\)的数

这是少数几个仅仅用循环就可以解决的问题。

我们可以不断的更新\(now\),当\(now\)的左子树的\(siz+1=k\)时,那么就寻找到了排名为\(k\)的数,返回答案

在循环中,我们判断当左子树的\(siz>=k\)时,那么说明排名为\(k\)的数肯定在左子树中,所以遍历左子树

否则,就更新:\(k=k-t[t[now].ch[0]].siz-1\),再遍历右子树,不断循环,相当于一个递归的过程

int kth(int now,int k)
{
    while((t[t[now].ch[0]].siz+1)!=k)
    {
        if(t[t[now].ch[0]].siz>=k)now=t[now].ch[0];
        else
        {
            k=k-t[t[now].ch[0]].siz-1;
            now=t[now].ch[1];
        }
    }
    return t[now].val;
}

\(pre\):求\(x\)的前驱(前驱定义为小于\(x\),且最大的数)

将原树分离为\(a(<=x-1),b(>x-1)\),那么相当于\(a\)树中的树都是\(<x\)的数,并且全部都在\(a\)树中,按照定义,我们找到\(a\)树中排名\(t[a].siz\)的数,就是\(a\)树中最大的数(右子树的数大于左子树的数),返回答案。

int pre(int x)
{
    int a,b,c;
    split(rt,x-1,a,b);
    int ans=kth(a,t[a].siz);
    rt=merge(a,b);
    return ans;
}

\(nxt\):求\(x\)的后继(后继定义为大于\(x\),且最小的数)

同\(pre\)操作很像,将原树分离为\(a(<=x),b(>x)\),那么相当于\(b\)树中的数都是\(>x\)的数,按照定义,寻找\(b\)树中排名第一的数(最小),返回答案。

int nxt(int x)
{
    int a,b,c;
    split(rt,x,a,b);
    int ans=kth(b,1);
    rt=merge(a,b);
    return ans;
}

\(turn\):翻转区间

例题:P3391 【模板】文艺平衡树(Splay)

\(p.s:\)例如:原有序序列是\(5\) \(4\) \(3\) \(2\) \(1\),翻转区间是\([2,4]\)的话,结果是\(5\) \(2\) \(3\) \(4\) \(1\)

这个是按照\(siz\)来排序建树的,因为题目要求的是翻转区间,并不是按照数大小排序,所以用\(siz\)排序

在这里,我们将\([x,y]\)区间提取出来为\(b\)树,在\(b\)树上打标记\(rev\) ^ \(=1\)

void turn(int x,int y)
{
    int a,b,c,d;
    split(rt,x-1,a,b);
    split(b,y-x+1,b,c);
    t[b].rev^=1;
    rt=merge(a,merge(b,c));
}

五、后记

在这里要注意几个点

1.每次拆分子树后,要记得把原树合并回去,一家人就要整整齐齐的

2.上面的操作都是基本操作,都是板子,一些更高深的题可能需要更多操作和打标记,但主要都是灵活运用\(split\)和\(merge\)函数,就可以实现很多操作

六、例题

P3224 [HNOI2012]永无乡

【BZOJ3786】星系探索



深深感到自己的弱小

原文地址:https://www.cnblogs.com/ShuraEye/p/11663631.html

时间: 2024-10-12 01:26:54

【模板】fhq-treap的相关文章

浅谈fhq treap

一.简介 fhq treap 与一般的treap主要有3点不同 1.不用旋转 2.以merge和split为核心操作,通过它们的组合实现平衡树的所有操作 3.可以可持久化 二.核心操作 代码中val表示节点权值,pri表示节点的优先级,维护小根堆 1.split 将1个treap分裂为两个treap 分裂主要有两种:以权值k作为分界点.以位置k作为分界点 ①以权值k作为分界点 设原来的treap根节点为root,分裂后的<=k的treap A 的根节点为x,>k的treap B 的根节点为y

【bzoj1251】序列终结者——fhq treap

Description 给定一个长度为N的序列,每个序列的元素是一个整数.要支持以下三种操作: 1. 将[L,R]这个区间内的所有数加上V. 2. 将[L,R]这个区间翻转,比如1 2 3 4变成4 3 2 1. 3. 求[L,R]这个区间中的最大值. 最开始所有元素都是0. Input 第一行两个整数N,M.M为操作个数. 以下M行,每行最多四个整数,依次为K,L,R,V.K表示是第几种操作,如果不是第1种操作则K后面只有两个数. Output 对于每个第3种操作,给出正确的回答. Sampl

【POJ2761】【fhq treap】A Simple Problem with Integers

Description You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. One type of operation is to add some given number to each number in a given interval. The other is to ask for the sum of numbers in a given interval. In

FHQ Treap小结(神级数据结构!)

首先说一下, 这个东西可以搞一切bst,treap,splay所能搞的东西 pre 今天心血来潮, 想搞一搞平衡树, 先百度了一下平衡树,发现正宗的平衡树写法应该是在二叉查找树的基础上加什么左左左右右左右右的旋转之类的, 思路比较好理解,但是 代码量........ 一看就头大,, 然后,在洛谷翻题解的时候无意间看到了远航之曲发的一篇非常短小精悍的题解, 于是就学了一下 FHQ Treap 这个东西的学名应该是叫做fhq treap,应该是treap的强化版. 整个数据结构中只有两个操作: 1.

fhq treap

fhq-treap 小结 粗浅地学习了这个神奇的数据结构,下面瞎写一些感受 首先fhq treap是一个基于分裂与合并的平衡树,那么怎么分裂,怎么合并呢 我们分两种情况考虑 一.权值平衡树(我自己取的名字) 所谓权值平衡树,就是任何操作都只与权值有关的平衡树 比如最基础的分裂,合并操作 分裂就是把平衡树按照权值\(k\)分成两半,一边所有点的权值\(\leq k\),另一边权值\(\gt k\) 怎么分裂呢 首先根据\(treap\)的定义,所有点的权值是一颗二叉搜索树(BST),就是左边比他小

平衡树合集(Treap,Splay,替罪羊,FHQ Treap)

今天翻了翻其他大佬的博客,发现自己有些...颓废... 有必要洗心革面,好好学习 序:正常的BST有可能退化,成为链,大大降低效率,所以有很多方法来保持左右size的平衡,本文将简单介绍Treap,Splay,替罪羊,FHQ Treap: 另:代码都是普通平衡树 1.Treap 树堆,在数据结构中也称Treap,是指有一个随机附加域满足堆的性质的二叉搜索树,其结构相当于以随机数据插入的二叉搜索树.其基本操作的期望时间复杂度为O(logn).相对于其他的平衡二叉搜索树,Treap的特点是实现简单,

P2710 数列[fhq treap]

调了一辈子的fhq treap- 如果不会最大子段和 如果不会fhq treap 7个操作- 其中三个查询 单点查询其实可以和区间查询写成一个( fhq treap 的修改操作大概就是 \(split\) 完了然后把修改区间的根 打上标记 等着下传就完事了- 那这题没了-我给个好一点的小数据-反正我照着这个调了挺久的- .in 50 10 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 3

洛谷P3391 【模板】文艺平衡树(Splay)(FHQ Treap)

题目背景 这是一道经典的Splay模板题——文艺平衡树. 题目描述 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1 输入输出格式 输入格式: 第一行为n,m n表示初始序列有n个数,这个序列依次是(1,2, \cdots n-1,n)(1,2,?n−1,n) m表示翻转操作次数 接下来m行每行两个数 [l,r][l,r] 数据保证 1 \leq l \leq r

模板 - 数据结构 - 无旋Treap / FHQ Treap

普通平衡树: #include<bits/stdc++.h> using namespace std; typedef long long ll; #define ls(p) ch[p][0] #define rs(p) ch[p][1] const int MAXN = 100000 + 5; int val[MAXN], ch[MAXN][2], rnd[MAXN], siz[MAXN], tot, root; void Init() { tot = root = 0; } void Pu

fhq treap ------ luogu P3369 【模板】普通平衡树(Treap/SBT)

二次联通门 : LibreOJ #104. 普通平衡树 #include <cstdio> #include <iostream> #include <algorithm> const int BUF = 12312323; char Buf[BUF], *buf = Buf; inline void read (int &now) { bool temp = false; for (now = 0; !isdigit (*buf); ++ buf) if (*