主席树(Kth number,HDU 2665)

题目链接:https://vjudge.net/problem/HDU-2665

参考博客:http://blog.csdn.net/acdreamers/article/details/8656644

感觉网上的资料不是很好,讲得也模糊,模板也没有。

还是大致看一下原理,然后通过模板题及代码来学习比较有效。

原理的话大白书P397那一段介绍挺好的,后面的图也挺直观的。当然网上搜罗一下也挺不错。

模板题就这道了,代码找个清晰靠谱的参考一下就挺好。


个人理解

主席树就是一个可以保留历史版本的线段树,通过动态开点和连接的方式来减少空间开销。

可以保留历史版本直观上理解就是可以解决询问历史情况的问题。

扩展的理解和使用就是可以维护一系列的线段树,每颗线段树都是前一棵线段树加一个或多个变化得到的,线段树之间有某种顺序上的关系。

然后利用线段树之间的关系(很多时候是分别询问,然后做差),可以解决很多新的问题,可以优化时间和空间复杂度。

假设维护n个点,有n个操作,那么跟普通线段树相比,使用主席树不会增加时间复杂度,都是O(nlogn),而空间复杂度也只是从O(n)增加到O(nlogn)而已。


关于区间第K大的问题。

静态问题可以使用主席树。

缺点是只能解决静态的问题,优点是时空复杂度低。

动态问题可以使用树套树或整体二分。

树套树和主席树相比,优点是能解决动态问题,缺点是时空复杂度高,特别是空间复杂度高。


关于本题的话,就是主席树的扩展使用。

第i棵线段树可以查询前i个数,位于特定范围的数的个数。

版本更新就是插入第i+1个数,并得到新版本,同时保留历史版本。

最后如果询问区间[l,r]的第k大。

那就考虑T[r]和T[l-1]两颗线段树,两颗线段树维护的值一相减就得到[l,r]这个区间,位于特定范围的数的个数。(可以理解为得到了一个新的线段树,这颗线段树维护了区间[l,r]上位于特定范围的数的个数)我们就假装有那么一颗线段树,然后再递归找到第k大的数是多大。

讲的具体一点。

假装就有这么一颗线段树,维护了数轴上一段范围的数的个数。

我一开始在根,所以看到了所有数的个数。

把数轴分成两半,左子树维护者左半边,右子树维护着右半边。

但是我要找第k大,我要考虑往左子树或者右子树走。

如果左子树中数的个数大于等于k,那答案就肯定在左子树中,我就往左走。

否则左子树肯定有x个数,且x<k,那答案就一定在右子树中,我们就往右走,然后找第k-x大的数。

直到到达了一个节点,只维护了一种数,那这个数就是答案了。


代码

#include<stdio.h>
#include<algorithm>
#define m ((l+r)>>1)
using namespace std;
const int maxn = 100010;

int n,q;
int a[maxn];
int b[maxn];
int N;

//主席树
int rt[maxn],ls[maxn<<5],rs[maxn<<5],tree[maxn<<5],tot;
/*
void init(int n)
{
    tot=0;
    for(int i=0;i<=n;i++)
        rt[i]=0;
}
*/
void build(int l,int r,int& now)
{
    now=++tot;
    tree[now]=0;
    if(l==r) return;
    build(l,m,ls[now]);
    build(m+1,r,rs[now]);
}
void update(int last,int l,int r,int& now,int pos)
{
    now=++tot;
    ls[now]=ls[last];
    rs[now]=rs[last];
    tree[now]=tree[last]+1;
    if(l==r) return;
    if(pos<=m) update(ls[last],l,m,ls[now],pos);
    else update(rs[last],m+1,r,rs[now],pos);
}
int qry(int a,int b,int k)
{
    int t1=rt[a-1];
    int t2=rt[b];
    int l = 1;
    int r = N;
    while(l<r)
    {
        int num = tree[ls[t2]]-tree[ls[t1]];
        if(num>=k)
        {
            t1=ls[t1];
            t2=ls[t2];
            r=m;
        }
        else
        {
            t1=rs[t1];
            t2=rs[t2];
            l=m+1;
            k-=num;
        }
    }
    return l;
}
//主席树

void read()
{
    scanf("%d %d",&n,&q);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",a+i);
        b[i]=a[i];
    }
}

void solve()
{
    read();
    sort(b+1,b+1+n);
    N = unique(b+1,b+1+n)-b-1;
    tot=0;
    build(1,N,rt[0]);
    for(int i=1;i<=n;i++)
        update(rt[i-1],1,N,rt[i],lower_bound(b+1,b+1+N,a[i])-b);
    int l,r,k;
    while(q--)
    {
        scanf("%d %d %d",&l,&r,&k);
        printf("%d\n",b[qry(l,r,k)]);
    }
}

int main()
{
    int T;
    scanf("%d",&T);
    while(T--) solve();
    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值