主席树是 以前缀和形式基于权值线段树建立的可持久化线段树,可持久化指的是它保存了这棵树的所有历史版本.
最简单的办法是:如果你输入了n个数,那么每输入一个数字a[i],就构造一棵保存了从a[1]到a[i]的权值线段树,由于只增加了log的节点数,我们增加改变的节点并将没有改变的子树指向该节点,这样需要的空间开销只有n*(4+logn)
我们可以把第j棵树和第(i-1)棵树上的每个点的权值相减,来得到一棵新的权值线段树,而这个新的权值线段树相当于是输入了a[i]到a[j]以后得到的。
模板题 K-th Number
后面将第 k小/大 说成kth
解决什么问题:
给定一段区间,静态求区间kth
想想方法:
暴力:对于每一个询问,排个序,就行了,时间复杂度O(nmlogn)
莫队+树状数组:树状数组可以求给定区间kth kthkth,使用二分+树状数组,具体不展开,但是多个区间的话,需要不断地进行树状数组的add/del操作,那么使用莫队来优化区间端点的移动问题,时间复杂度O((n+m)√n logn) 莫队复杂度*树状数组复杂度
莫队+平衡树:把树状数组的部分替换成二叉查找树,用splay的一部分操作,需要用到kth操作,不用翻转标记什么的,时间复杂度O((n+m)√n logn)跟上面的一样
目前想想,也就这三种方法,各有优劣,暴力时间复杂度不行,但是可以在线
后面两种因为莫队的原因必须离线
但是这三种方法时间都太慢,这个题目我们需要一个O(nlogn) 的做法
于是主席树就诞生了
#include <cstdio> #include <vector> #include <cstring> #include <iostream> #include <algorithm> using namespace std; typedef long long ll ; const int oo=0x7f7f7f7f ; const int maxn=1e5+7; const int mod=1e9+7; int n,m,cnt,root[maxn],a[maxn],x,y,k; struct node{ int l,r,sum; }T[maxn*25]; vector<int> v; int getid(int x){ return lower_bound(v.begin(),v.end(),x)-v.begin()+1; } void update(int l,int r,int &x,int y,int pos){ T[++cnt]=T[y],T[cnt].sum++,x=cnt; if(l==r) return; int mid=(l+r)/2; if(mid>=pos) update(l,mid,T[x].l,T[y].l,pos); else update(mid+1,r,T[x].r,T[y].r,pos); } int query(int l,int r,int x,int y,int k){ if(l==r) return l; int mid=(l+r)/2; int sum=T[T[y].l].sum-T[T[x].l].sum; if(sum>=k) return query(l,mid,T[x].l,T[y].l,k); else return query(mid+1,r,T[x].r,T[y].r,k-sum); } int main(void){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]),v.push_back(a[i]); sort(v.begin(),v.end()); v.erase(unique(v.begin(),v.end()),v.end()); for(int i=1;i<=n;i++) update(1,n,root[i],root[i-1],getid(a[i])); for(int i=1;i<=m;i++){ scanf("%d%d%d",&x,&y,&k); printf("%d\n",v[query(1,n,root[x-1],root[y],k)-1]); } return 0; }
参考博客:
https://blog.csdn.net/Stupid_Turtle/article/details/80445998