可持久化线段树(主席树)+ treap 学习笔记

可持久化线段树(主席树)

可持久化概念:
可持久化实质上就是存储该数据结构所有的历史状态,以达到高效的处理某些信息的目的。

问题:(第k小数)

题目链接:给定长度为N的序列A,有M次询问,求在[l,i]区间内第k小的数是多少。
做法
由题意知道我们肯定要对区间进行排序,但是我们的排序不是每次询问才排序,是初始化就排序并离散化——针对数字较大但数据不大的情况下排序离散化完毕后,以离散化数组建主席树,设ii属于区间[1,n],对原数组的[1,i]区间的数做统计(例如下图,区间中按离散化数组顺序统计1的个数、2的个数、3的个数、4的个数、8的个数、9的个数),有序地插入节点到离散化数组的主席树中,记录好原数组每个节点对应的线段树起点,针对样例有几个示意图。注意,这里的橙色节点是新节点,与之前出现的那个图不一样。

[1,1]的情况:在这里插入图片描述

[1,4]的情况:
在这里插入图片描述我们按照上面的做法构建的主席树是为了方便我们查找第k小值。因为我们是以离散数组构建的主席树,那么从根节点出发,左子树部分的数必定不大于右子树部分的数。于是就可以将左儿子的节点个数x与k做比较,若 k≤x,则第kk小值一定在左子树里面,若x≤k,则第k小值一定在右子树里面,然后递归往下走,缩小范围。值得注意的是,前者递归时,k直接传下去即可,后者递归时,需要将k减去左子树的数的个数再传递这个k值。

例如我们查找[1,4]中第2小的值,图示如下,绿色节点为该值存在的区间位置。
在这里插入图片描述
需要注意的是,第二个绿色节点才是绿色根节点的左子树,因为左子树表示的区间是靠前的那一半。

我们发现可以用前缀和来维护:
只要用预处理大法分别以[1,l]和[1,r]的数建立权值线段树,每个点的值对位相减即可。
区间[l,r] 我们只要[1.r]-[1,l-1]即可。

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=10010;

struct node
{
    int l ,r;//左右儿子节点
    int cnt;//在区间范围内的数
}tr[20*N];
int a[N];
int root[N],idx;
vector<int>nums;//离散化数组
int n,m;
int find(int x)//离散化时新的下标
{
    return lower_bound(nums.begin(), nums.end(), x) - nums.begin();
}


int  build(int l,int r)//建树
{
    int p=++idx;
    if(l==r) return p;
    int mid=l+r>>1;
    tr[p].l=build(l,mid);
    tr[p].r=build(mid+1,r);
    return p;
}

//增加一个数 pre 为上一个的根节点。
int insert(int pre,int l,int r,int x)
{
    int q = ++idx, mid = (l + r) >> 1;
    tr[q].l = tr[pre].l, tr[q].r = tr[pre].r, tr[q].cnt = tr[pre].cnt + 1;
    if(l < r){
        //应该更新哪一个值域区间
        if(x <= mid) tr[q].l = insert(tr[pre].l, l, mid, x);
        else tr[q].r = insert(tr[pre].r, mid + 1, r, x); 
    }
    return q;
}

//查询
int query(int x,int y,int l,int r,int k)
{   //找到了
    if(l==r) return l;
    else
    {
        int mid=l+r>>1;
        //对位相减
        int cnt=tr[tr[y].l].cnt-tr[tr[x].l].cnt;
        if(k<=cnt) return query(tr[x].l,tr[y].l,l,mid,k);
        else return query(tr[x].r,tr[y].r,mid+1,r,k-cnt);
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        nums.push_back(a[i]);
    }
    //离散化
     sort(nums.begin(), nums.end());//从小到大排序
    nums.erase(unique(nums.begin(), nums.end()), nums.end());//去重

    root[0] = build(0, nums.size() - 1);//建树初始化版本
    
    for(int i=1;i<=n;i++)
    {
        root[i]=insert(root[i-1],0,nums.size()-1,find(a[i]));
    }
    
    while(m--)
    {
        int l,r,d;
        cin>>l>>r>>d;
        printf("%d\n",nums[query(root[l-1],root[r],0,nums.size()-1,d)]);
    }
    
    return 0;
}

treap(平衡树)

众所周知Treap = BST + heap
BST又是中序遍历 是从小到大的 左中右
说说这个BST,就是说一个根节点p,左儿子一定小于他,右儿子大于它。

也就是BST的中序遍历是严格单调递增的。

那么就可以进行一些操作了。

首先为了维护这个BST我们需要一个左旋zag和右旋zig,分别表示将根节点和左右儿子交换位置,使交换后还满足BST的性质。

他可以有几项操作
1 插入操作
1.1插入已有的值(就cnt++);
1.2插入没有的值

2删除操作
2.1删除中间节点(把中间节点转为叶子节点)
2.2删除叶子节点(直接删)

3找前驱后继(比某个数小的最大值/比某个数大的最小值)
前驱就是左边最大的,后继就是右边最小的

node
{
    int l,r;左儿子编号 右儿子编号
    int key,val;排序序号(BST,有序) 优先级(大根堆heap里的)
}tr[N]
root.val >= root.left.val 
root.val >= root.right.val
val取随机,则树的结构就随机(高度的平均就接近logn)

加两个哨兵
-∞  +∞ 处理边界

平衡树模板题

旋转的样例
hh,tt分别在整个序列的最前和最后 在旋转前后没有变化
同时 中序遍历(递增序列)的结构在旋转前后都是 hh y z x tt

     x              y
   / \  右旋zig    / \
  y   tt   ->     hh  x
 / \      <-         / \
hh  z   左旋zag     z   tt

右旋zig :即根节点向右旋转

void zig(int &p)
{
    int q=tr[p].l;
    tr[p].l=tr[q].r;
    tr[q].r=p;
    p=q;pushup(tr[p].r);
    pushup(p);
}

左旋zag

void zag(int &p)
{
    int q=tr[p].r;
    tr[p].r=tr[q].l;
    tr[q].l=p;
    p=q;
    pushup(tr[p].l);pushup(p);
}

题目代码

#include <bits/stdc++.h>
using namespace std;
const int N = 100010, INF = 1e8;

int n;
struct Node
{
    int l, r;
    int key, val;//节点键值 || 权值
    int cnt, size;//这个数出现次数 || 每个(节点)子树里数的个数
}tr[N];

int root, idx;

void pushup(int p)//儿子信息更新父节点信息
{
    tr[p].size = tr[tr[p].l].size + tr[tr[p].r].size + tr[p].cnt;
    //左儿子树 数个数+ 右儿子树 数个数+ 当前节点数出现次数(重复次数)
}

int get_node(int key)
{
    tr[++idx].key = key;
    tr[idx].val = rand();
    tr[idx].cnt = tr[idx].size = 1;//默认创建时都是叶子节点
    return idx;
}
void zig(int &p)//右旋  p指针指向的根节点
{
    int q = tr[p].l;//先把p的左儿子q存下来
    tr[p].l = tr[q].r;//b从q的右儿子变为p的左儿子
    tr[q].r = p;//最终q的右儿子是p指向的x
    p = q;//p从指向原来根节点x变为指向新根节点q
    pushup(tr[p].r),pushup(p);//先更新 右子树(x bc) 再更新根节点y
}

void zag(int &p)
{
    int q = tr[p].r;//先把p的右儿子y(q)存下来
    tr[p].r = tr[q].l;//b挂x(p)
    tr[q].l = p;//p挂y的左儿子
    p=q;//p维护指向根节点
    pushup(tr[p].l),pushup(p);
}

void build()//建树
{
    get_node(-INF),get_node(INF);
    root = 1,tr[1].r = 2;//根节点1号点,1号点的右儿子2号点
    //build的时候不要漏了维护树的结构
    pushup(root);
    if (tr[1].val < tr[2].val) zag(root);
}

void insert(int &p,int key)
{
    if(!p) p = get_node(key);//如果到达叶子节点fa 并进一步递归tr[fa].l or tr[fa].r == 空 新增一个key叶子节点
    else if(tr[p].key == key) tr[p].cnt++;//如果插入已有的值 cnt++
    else if(tr[p].key>key) //插入新的值 判断插入左子树还是右子树 直到叶子节点
    {//如果要插入的key比p的key小 递归tr[p]的左子树tr[p].l
        insert(tr[p].l,key);
        if(tr[tr[p].l].val > tr[p].val) zig(p);//如果左子树val大于root.val 就换上来
    }
    //维护val是根节点root.val > 子树 root.l.val or root.r.val 
    else
    {//如果要插入的key比p的key大 递归tr[p]的右子树tr[p].r
        insert(tr[p].r,key);
        if(tr[tr[p].r].val > tr[p].val) zag(p);//如果右子树val大于root.val 就换上来
    }
    pushup(p);
}

void remove(int &p,int key)
{
    if(!p) return;//如果删除的值不存在
    if(tr[p].key == key)//如果有值
    {
        if(tr[p].cnt>1) tr[p].cnt--;//如果cnt>1 cnt--
        else if(tr[p].l || tr[p].r)//否则判断是否叶子节点 
        {//如果不是叶子节点
            if(!tr[p].r || tr[tr[p].l].val > tr[tr[p].r].val)//右旋把p放到右子树 递归到叶子节点删除
            {//如果没有右子树 肯定可以右旋 || 左子树的.val > 右子树的val
                zig(p);
                remove(tr[p].r,key);
            }
            else//否则左旋把p放到左子树 递归到叶子节点删除
            {
                zag(p);
                remove(tr[p].l,key);
            }
        }
        else p=0;//如果是叶子节点 直接删除
    }
    else if(tr[p].key > key) remove(tr[p].l,key);//如果 要删除的中序顺序key>当前节点p的tr[p].key 去p左子树删
    else remove(tr[p].r,key);
    pushup(p);
}

int get_rank_by_key(int p,int key)//通过数值找排名  传p而不是传&p
{
    if(!p) return 0;//如果p不存在 返回0
    if(tr[p].key==key) return tr[tr[p].l].size+1;//如果key和当前节点key相等 排名=左子树数的大小+1(当前节点)
    if(tr[p].key>key) return get_rank_by_key(tr[p].l,key);//漏了return
    //说明要找的key比当前节点小 去当前节点左子树找
    return tr[tr[p].l].size + tr[p].cnt + get_rank_by_key(tr[p].r,key);
    //去右边找返回的是右边子树的排名,所以还得加上根节点root数的个数以及根节点左子树总数root.l.size
}

int get_key_by_rank(int p,int rank)//通过排名找数值 传p而不是传&p
{
    if(!p) return INF;//p不存在 则返回不可能存在的排名
    if(tr[tr[p].l].size >=rank) return get_key_by_rank(tr[p].l,rank);//如果左子树数的个数>rank,则去左子树找
    if(tr[tr[p].l].size+tr[p].cnt>=rank) return tr[p].key;//左子树+当前节点个数>=rank,则就是当前节点
    //左子树+当前节点个数<rank,则递归右子树,同时往下传的rank-左子树-当前节点个数
    return get_key_by_rank(tr[p].r,rank-tr[tr[p].l].size-tr[p].cnt);//-tr[p].cnt
}

int get_prev(int p,int key)//找到严格小于key的数中的最大数 传p而不是传&p
{
    if(!p)return -INF;
    if(tr[p].key>=key) return get_prev(tr[p].l,key);//当前这个数>=key 则要找的数在当前节点的左子树里
    return max(tr[p].key, get_prev(tr[p].r, key));
    // if(tr[p].key<key) //当前节点<key max(当前节点(无右子树) || 当前节点的右子树中的最大值(有右子树))
}

int get_next(int p,int key)//找到严格大于key的数中的最小数 传p而不是传&p
{
    if (!p) return INF;
    if (tr[p].key <= key) return get_next(tr[p].r, key);
    return min(tr[p].key, get_next(tr[p].l, key));
}

int main()
{
    build();
    scanf("%d",&n);
    while(n--)
    {
        int op,x;
        scanf("%d%d",&op,&x);
        if(op==1) insert(root,x);
        else if (op == 2) remove(root,x);
        else if (op == 3) printf("%d\n", get_rank_by_key(root, x) - 1);
        else if (op == 4) printf("%d\n", get_key_by_rank(root, x + 1));
        else if (op == 5) printf("%d\n", get_prev(root, x));
        else printf("%d\n", get_next(root, x));
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值