静态主席树学习小记
语文不好的人的开头
之前一直觉得主席树是一种黑科技,非常的神奇,区间第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\n",Query(root[l-1],root[r],1,n,k)); } }
在日渐沉没的世界里,我发现了你。