主席树
抛出问题
如题,给定N个整数构成的序列,将对于指定的闭区间查询其区间内的第K小值。
输入输出格式
输入格式:
第一行包含两个正整数N、M,分别表示序列的长度和查询的个数。
第二行包含N个整数,表示这个序列各项的数字。
接下来M行每行包含三个整数l, r, kl,r,k , 表示查询区间[l, r][l,r]内的第k小值。
输出格式:
输出包含k行,每行1个整数,依次表示每一次查询的结果
解决问题
主席树(可持久化线段树)法
于是针对这个问题,新的数据结构诞生了,也就是主席树。
主席树本名可持久化线段树,也就是说,主席树是基于线段树发展而来的一种数据结构。其前缀”可持久化”意在给线段树增加一些历史点来维护历史数据,使得我们能在较短时间内查询历史数据,图示如下。
图中的橙色节点为历史节点,其右边多出来的节点是新节点(修改节点)。
下面我们来讲怎么构建这个数据结构。
主席树教程
- 要求:掌握线段树这个数据结构。
- 注意:一般主席树一类的题目,难的不是写主席树,而是主席树的运用。
主席树的点修改
不同于普通线段树的是主席树的左右子树节点编号并不能够用计算得到,所以我们需要记录下来,但是对应的区间还是没问题的。
//节点o表示区间[l,r],修改点为p,修改值根据题意设定(此处我们先不谈题目,只谈数据结构) int modify(int o, int l, int r, int p) { int oo = ++node_cnt; lc[oo] = lc[o]; rc[oo] = rc[o]; sum[oo] = sum[o] + 1;//新节点,这里是根据模板题来的 if(l == r)//递归底层返回新节点编号,修改父节点的儿子指向 { //sum[oo] = t;如果题目要求sum是加t的再这样弄,然后上面的+1就去掉 return oo; } int mid = (l + r) >> 1; if(p <= mid) lc[oo] = modify(lc[oo], l, mid); else rc[oo] = modify(rc[oo], mid+1, r); //sum[oo] = sum[lc[oo]] + sum[rc[oo]];在该题中,不需要这样做,但是很多情况下是要这样更新的 return oo; }
至于主席树的区间修改,其实也不难,但是复杂度有点高,简单点的题目一般只有点修改,有时候区间修改可以转化为点修改(比如NOIP2012借教室,有区间修改的解法也有点修改的解法)。
主席树的询问(历史区间和)
int ql, qr;//查询区间[l,r] int query(int o, int l, int r)//节点o代表区间[l,r] { int ans = 0, mid = ((l + r) >> 1); if(!o) return 0;//不存在的子树 if(ql <= l && r <= qr) return sum[o];//区间包含返回区间值 //都是线段树标准操作,只不过是左右子树多了一个记录而已 if(ql <= mid) ans += query(lc[o], l, mid); if(qr > mid) ans += query(rc[o], mid+1, r); return ans; //点操作就不用说了 }
主席树完整代码
1 #include<cstdio> 2 #include<iostream> 3 #include<algorithm> 4 using namespace std; 5 int cnt,n,m,ans,p; 6 struct qwq{ 7 int rt,l,r,sum; 8 }tree[5000005]; 9 int zh[200010],b[200010]; 10 void build(int l,int r,int &k) 11 { 12 k=++cnt; 13 if(l==r) 14 return; 15 int mid=(l+r)>>1; 16 build(l,mid,tree[k].l); 17 build(mid+1,r,tree[k].r); 18 } 19 int modify(int l,int r,int k) 20 { 21 int kkk=++cnt; 22 tree[kkk].l=tree[k].l; 23 tree[kkk].r=tree[k].r; 24 tree[kkk].sum=tree[k].sum+1; 25 if(l==r) 26 return kkk; 27 int mid=(l+r)>>1; 28 if(p<=mid) 29 tree[kkk].l=modify(l,mid,tree[kkk].l); 30 else tree[kkk].r=modify(mid+1,r,tree[kkk].r); 31 return kkk; 32 } 33 long long query(int ll,int rr,int l,int r,int k) 34 { 35 int ans,mid=((l+r)>>1),x=tree[tree[rr].l].sum-tree[tree[ll].l].sum; 36 if(l==r) 37 return l; 38 if(x>=k) ans=query(tree[ll].l,tree[rr].l,l,mid,k); 39 else ans=query(tree[ll].r,tree[rr].r,mid+1,r,k-x); 40 return ans; 41 } 42 int main() 43 { 44 scanf("%d%d",&n,&m); 45 for(int i=1;i<=n;i++) 46 { 47 scanf("%d",&zh[i]); 48 b[i]=zh[i]; 49 } 50 sort(b+1,b+n+1); 51 int q=unique(b+1,b+n+1)-b-1; 52 build(1,q,tree[0].rt); 53 for(int i=1;i<=n;i++) 54 { 55 p=lower_bound(b+1,b+q+1,zh[i])-b; 56 tree[i].rt=modify(1,q,tree[i-1].rt); 57 } 58 for(int i=1;i<=m;i++) 59 { 60 int ll,rr,k; 61 scanf("%d%d%d",&ll,&rr,&k); 62 ans=query(tree[ll-1].rt,tree[rr].rt,1,q,k); 63 printf("%d ",b[ans]); 64 } 65 return 0; 66 }
主席树复杂度分析
如果只按照上述做法去做的话,每次修改的时间复杂度是O(lgn)O(lgn),每次询问的复杂度也是O(lgn)O(lgn)。