静态主席树学习小记

语文不好的人的开头

之前一直觉得主席树是一种黑科技,非常的神奇,区间第k大只会暴力或者分块

其实也没有很复杂

主席树是什么?

你可以将它理解为对于i=1~n,对于每个1..i的前缀建了一棵线段树

至于为什么是前缀,等下再说

主席树解决什么问题?

emmm目前蒟蒻我只会区间第k小

主席树如何解决区间第k小问题呢?

我们如果想要用线段树来写也很简单,只要对[l,r]这个区间建一棵线段树就行了,但这显然是O(Q*nlogn)的

那么我们先引入前缀和求和的概念

我们平时用前缀和,要求[l..r]的和时,都是这样:sum[r]-sum[l-1]

然后我们再回想一下在线段树中我们是如何求第k小的

很简单,我们对一个有序的序列建线段树,然后在一个节点处判断其左儿子的大小是否大于等于k,大于则k一定在左儿子内,否则k减去左儿子的大小,到右儿子中找

那么主席树也是基本一样的,不过它要用到前缀和的思想

首先我们知道,左儿子的大小就是小于等于mid的数的个数,那么1..r线段树的左儿子大小-1..l-1线段树的左儿子大小,不就是l..r线段树中左儿子的大小了吗?

我这样说可能比较难懂,但是笔者条件所限,暂时无法作图

这样时间复杂度解决了,可是空间复杂度并没有解决

怎么解决空间复杂度呢?

我们可以与之前建线段树共用大部分节点,真正修改的只有需要修改的一条链

详情见代码

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N=2e5+10;
struct Seg {
    int l,r,sz;
}t[20*N];
int cnt,root[N];//每个线段树的根
struct ForSort {
    int a,id;
}a[N];
int rank[N];//离散化后的数组
int n,m;

bool CMP(ForSort a,ForSort b) {
    return a.a<b.a;
}

void Build_New(int &x,int l,int r,int val) {
    t[++cnt]=t[x];x=cnt;t[x].sz++;
    //在这里,我们显然不是要修改之前的树。
    //我们将之前的树中需要改的节点克隆一个出来,这样不用改的节点就被共用了。
    //我们只需要修改当前克隆出来的节点
    if (l==r) return;
    int mid=l+r>>1;
    if (val<=mid) Build_New(t[x].l,l,mid,val);
    else Build_New(t[x].r,mid+1,r,val);
}

int Query(int x,int y,int l,int r,int val) {
    int rk=t[t[y].l].sz-t[t[x].l].sz,mid=l+r>>1;
    if (l==r) return a[l].a;
    if (val<=rk) return Query(t[x].l,t[y].l,l,mid,val);
    else return Query(t[x].r,t[y].r,mid+1,r,val-rk);
}

int main() {
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++) scanf("%d",&a[i].a),a[i].id=i;
    sort(a+1,a+n+1,CMP);
    for (int i=1;i<=n;i++) rank[a[i].id]=i;
    for (int i=1;i<=n;i++) {
        root[i]=root[i-1];
        Build_New(root[i],1,n,rank[i]);
    }
    for (int i=1;i<=m;i++) {
        int l,r,k;
        scanf("%d%d%d",&l,&r,&k);
        printf("%d
",Query(root[l-1],root[r],1,n,k));
    }
}
View Code
在日渐沉没的世界里,我发现了你。
原文地址:https://www.cnblogs.com/mastervan/p/10302667.html