很后悔之前在XGC大佬讲的时候没认真听(其实讲的不q不c,幸好了解了一下),现在搞搞差不多理解了。
这个东西是线段树的进化版,强大在于实现了可持久化,后一刻可以参考前一刻的状态。
裸题:给n(1<=n<=100000)个数字a[1],a[2],......,a[n](0<=a[i]<=1000000000),m(1<=m<=100000)次询问l到r之间的第k小的值。
对于这种频繁询问l~r的第k小,线段树原地爆炸。
所以说要用主席树。
首先对于线段树的定义有一点不同(线段树求他的最小值应该是按数列位置排,管理的点有个mn记录区间最小),而在主席树,他的叶子节点是记录这个区间有多少个这个数,举个例子,比如数列里有3个1,那管理1~1的点c值为3,然后回溯更新父亲,所以说,为了防止读入的数值太大以至于爆内存,所以说要离散化。
对于这个序列每一个前缀,都为他建一棵新树,这个数列管理1~i的值,然而不用想就知道,建这么多树不爆才怪,那怎么办?可以发现,管理1~i-1和管理1~i的树只差一个点,而a[i]只会影响到从叶子节点到根一条路径的值,那这两棵树就可以共用不影响的点,达到省空间的目的。
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; struct node { int lc,rc,c; }tr[2100000];int cnt; int a[110000],b[110000],rt[110000]; int maketree(int x,int l,int r,int p) { if(x==0)x=++cnt;tr[x].c++;//开新点,然后管理的人数++ if(l==r)return x; int mid=(l+r)/2; if(p<=mid)tr[x].lc=maketree(tr[x].lc,l,mid,p);//a[i]应该隶属那个子树 else tr[x].rc=maketree(tr[x].rc,mid+1,r,p); return x; } int Merge(int x,int y) { if(x==0||y==0)return x+y;//假如说x没有这个节点,而y有,那就不用开新点,直接连过去,也就是说一个的点有可能多个父亲 tr[x].c+=tr[y].c;//x的总人数加上y的 tr[x].lc=Merge(tr[x].lc,tr[y].lc); tr[x].rc=Merge(tr[x].rc,tr[y].rc); return x; } int findans(int x,int y,int l,int r,int k) { if(l==r)return b[l];//l是离散了的值,找回原来的 int c=tr[tr[x].lc].c-tr[tr[y].lc].c;//得出区间真正有的点数,就是前缀和的应用 int mid=(l+r)/2; if(k<=c)return findans(tr[x].lc,tr[y].lc,l,mid,k); else return findans(tr[x].rc,tr[y].rc,mid+1,r,k-c); } int n,m; int erfen(int k)//二分实际是得出离散值,不然线段树空间爆炸 { int l=1,r=n,mid,ans; while(l<=r) { mid=(l+r)/2; if(b[mid]<=k) { l=mid+1; ans=mid; } else r=mid-1; } return ans; } char ss[10]; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); b[i]=a[i]; } sort(b+1,b+n+1); cnt=0;memset(rt,0,sizeof(rt)); for(int i=1;i<=n;i++) { rt[i]=maketree(rt[i],1,n,erfen(a[i]));//这棵树里就记录了a[i] rt[i]=Merge(rt[i],rt[i-1]);//上一棵树记录a[1]~a[i-1],让新的和这个合并一下(上一棵树保留),就得到a[1]~a[i]了 } int x,y,k; while(m--) { scanf("%d%d%d",&x,&y,&k); printf("%d\n",findans(rt[y],rt[x-1],1,n,k));//类似求前缀和,主席树就是按前缀建树啊! } return 0; }
时间: 2024-11-05 04:31:54