【填坑】可持久化线段树解决无修改的区间k大问题

区间k大问题是一个比较经典的问题,各种方法层出不穷,写暴力的、树套树的、主席树的、分块大法好(@LOI_DQS)的……
这里讲一下权值线段树+可持久化解决的方法(不支持修改操作)
首先是权值线段树,顾名思义,就是按权值大小保存而不是按照序列顺序保存的线段树,如1,4,2,5,3,在权值线段树里保存的顺序应该为1,2,3,4,5
可持久化数据结构,就是指可以“持久”的数据结构,具体就是假设你现在做到了第100步,却突然想查询第50步时的状态(听起来很坑对不对)
可持久化如何实现呢?想具体了解的可以看wc2012 FHQ的论文《谈谈各种数据结构》,大概比较靠后的位置;不想具体了解的可以看我讲的……

Q:可持久化最简单的实现方法是什么?
A:[手动斜眼]每次操作后的数据结构存一遍,操作N次就存N棵树
Q:说的吼!但是空间太大了怎么办?
A:……
Q:一个数据结构修改要动多少东西?就拿线段树说吧
A:修改一次要改logN个节点的值
Q:那么剩下那些不用改的节点还用存吗?
A:……似乎不用了
Q:那么修改一次就等于把所有修改的节点另建立新节点,对吗?
A:我好像明白了些什么…………

这里写图片描述
黄色节点为要修改的节点,红色节点为新建的节点,黑色圈里圈着的是旧树,红色的圈里圈着的是新树,可以看到,修改后的新树只是在旧树的基础上修改了几个节点,所以只需要新建这几个节点,并把新树的部分指向指回旧树

这个就是可持久化的实现方式了,那么,可持久化和权值线段树怎么解决区间k大问题呢?
首先把值全部排序去重,用于建权值线段树,可以用stl中的sort和unique解决……权值线段树里保存的内容是值的数量,比如插入三个3就记录为3,再然后按照序列顺序依次插入节点,由于这是可持久化线段树,所以请用可持久化的方法插入……
什么,你说线段树不支持插入?你开始建棵空树啊,反正你都知道权值了,照着往下找,把插入改成单点+1不久完了吗?

然后就到了查询了,首先因为你是可持久化的,那么查询l到r区间就是你第r次插入减去第l-1次插入后的线段树的样子,想想对不对,可持久化后你总共建立了n棵线段树,两棵线段树相减,得到的就是一棵只有中间过程的线段树,在这里也就是第l次操作到第r次操作了

那么我们得到了这个区间,就可以继续找第k大了,怎么找?因为这是权值线段树,所以是排好序的~,那么就可以像平衡树一样找啦

代码么……

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int ls[5000000];    //左儿子
int rs[5000000];    //右儿子
int num[5000000];   //过会再讲
int san[5000000];   //过会再讲
int sum[5000000];   //线段树里保存的值
int T[5000000];     //每个节点在线段树里的标号
int n,m,tot = 0;

建树函数:

void build(int l,int r,int &x)
{
    x = ++tot;//不再使用堆式存储,而是动态开点
    sum[x] = 0;//初始是一棵空树
    if(l == r)
        return ;
    int mid = (l+r)/2;
    build(l,mid,ls[x]);
    build(mid+1,r,rs[x]);
}

修改操作:

void update(int last,int p,int l,int r,int &x)//p点加1
{
    x = ++tot;
    ls[x] = ls[last];
    rs[x] = rs[last];
    sum[x] = sum[last] + 1;//首先继承之前的线段树
    if(l == r)
        return ;
    int mid = (l+r)/2;
    if(p <= mid)
        update(ls[last],p,l,mid,ls[x]);
    else
        update(rs[last],p,mid+1,r,rs[x]);//分清情况,只往一边建
}

查询操作:

int query(int s,int t,int l,int r,int k)//查询s到t区间第k大的数
{
    if(l == r)
        return l;
    int mid = (l+r)/2;
    int cnt = sum[ls[t]] - sum[ls[s]];//cnt为左子树新树减旧树
    if(k <= cnt)
        return query(ls[s],ls[t],l,mid,k);
    return query(rs[s],rs[t],mid+1,r,k-cnt);
}

主函数及预处理:

int main()
{
    int x,y,z;
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i ++)
    {
        scanf("%d",&san[i]);
        num[i] = san[i];
    }
    sort(san+1,san+n+1);
    int cnt = unique(san+1,san+n+1)-san-1;
    build(1,cnt,T[0]);
    for(int i = 1;i <= n;i ++)
        num[i] = lower_bound(san+1,san+cnt+1,num[i]) - san;
    for(int i = 1;i <= n;i ++)
        update(T[i-1],num[i],1,cnt,T[i]);
    for(int i = 1;i <= m;i ++)
    {
        scanf("%d%d%d",&x,&y,&z);
        printf("%d\n",san[query(T[x-1],T[y],1,cnt,z)]);
    }
    return 0;
}

代码不长,比较简单,也就懒得解释了……

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值