主席树:
(不要管名字)
我们有的时候,会遇到很多种情况,对于每一种情况,都需要通过线段树的操作实现。
碰巧的是,相邻两种情况下的线段树的差异不大。(总体的差异次数是O(N)级别的,均摊就是O(常数)的了)
显然的是,我们不能对于每种情况都建造一棵线段树。n^n 空间直接MLE无疑。
救命稻草是:发现相邻两种情况下的线段树的差异不大。
所以,我们是否可以让不同的线段树共用同一个节点呢?!?!?
这就是主席树的本质。也是精妙之处所在。
代码实现不是很麻烦。
我一般用传返回值形式,每次返回一个节点编号,便于设置儿子编号。比较方便。
注意的是,我们必须记录lson,rson,不能采用x<<1,x<<1|1的形式。因为没有这样的规律可循。
你不知道子节点和自己有什么关系。(这是谁家的孩子?公家的)
经典例题:
1.区间第k小(大)。
离散化必须的。
对于每一个区间节点开一个权值线段树 。i的线段树的节点l~r表示,在真正的区间1~i中,大小在l~r的数出现的次数。
记录每个线段树节点根的所在位置。
查询的时候,l-1,r两棵线段树同时出发,区间[a,b]sum值做一个差,就是l~r这个区间内,数值在[a,b]之间的数的个数。
对于区间第k小,选择左儿子区间做差,u<k,就进入右儿子,同时k-=u
否则进入左儿子。
区间第k大正相反。
对于n棵主席树,相邻两个主席树i,i-1只在i的数值位置的值不一样。
所以,相邻的主席树只会增加logn个节点。
总空间复杂度nlogn
代码:
#include<bits/stdc++.h> using namespace std; const int N=2e5+10; int n,m; int a[N],num[N]; int cnt,rt[N]; int li(int x){ return lower_bound(num+1,num+cnt+1,x)-num; } struct node{ int sum,lson,rson; }t[N*18]; int tot; int add(int x,int l,int r,int c){ tot++; int ret=tot; t[tot].sum=t[x].sum+1; if(l==r){ return ret; } int mid=(l+r)>>1; if(c<=mid){ t[tot].rson=t[x].rson; t[tot].lson=add(t[x].lson,l,mid,c); } else{ t[tot].lson=t[x].lson; t[tot].rson=add(t[x].rson,mid+1,r,c); } return ret; } int query(int x,int y,int l,int r,int k){ if(l==r){ return l; } int mid=(l+r)>>1; int u=t[t[y].lson].sum-t[t[x].lson].sum; if(u>=k){ return query(t[x].lson,t[y].lson,l,mid,k); } else{ return query(t[x].rson,t[y].rson,mid+1,r,k-u); } } int main() { scanf("%d%d",&n,&m);int df; for(int i=1;i<=n;i++) scanf("%d",&a[i]),num[++cnt]=a[i]; sort(num+1,num+cnt+1); cnt=unique(num+1,num+cnt+1)-num-1; rt[0]=++tot; for(int i=1;i<=n;i++) rt[i]=add(rt[i-1],1,cnt,li(a[i])); int op,l,r; while(m--){ scanf("%d%d%d",&l,&r,&op); int ot=query(rt[l-1],rt[r],1,cnt,op); printf("%d\n",num[ot]); } ret