可持久化线段树,也叫作函数式线段树,也就是主席树,(。。。因为先驱就是fotile主席。。Orz。。。)
这里,我将从查找区间第k小值(不带修改)题的可持久化线段树做法中,讲一讲主席树。
可持久化数据结构(Persistent data structure)就是利用函数式编程的思想使其支持询问历史版本、同时充分利用它们之间的共同数据来减少时间和空间消耗。/*找不到比较科学的定义,就拿这个凑凑数吧~~~*/
这个数据结构很坑啊,我研究了一整天才差不多理解了一些。所以,要理解好每一个域或变量的意义。
开讲!
既然叫“函数式线段树”,那么就应该有跟普通线段树相同的地方。一颗线段树,只能维护一段区间里的元素。但是,每个询问的区间都不一样,若是对每段区间都单独建立的线段树,那~萎定了~。因此,就要想,如何在少建,或建得快的情况下,能利用一些方法,得出某个区间里的情况。
比如一棵线段树,记为tree[i][j],表示区间[i,j]的线段树。那么,要得到它的情况,可以利用另外两棵树,tree[1][i-1]和tree[1][j],得出来。也就是说,可以由建树的一系列历史版本推出。
那么,怎么创建这些树呢?
首先,离散化数据。因为如果数据太大的话,线段树会爆~~
在所有树中,是按照当前区间元素的离散值(也就是用大小排序)储存的,在每个节点,存的是这个区间每个元素出现的次数之和(data域)。出现的次数,也就是存了多少数进来(建树时,是一个数一个数地存进来的)。
先建棵线段树,所有的节点data域为0。再一个节点一个节点地添加。把每个数按照自己的离散值,放到树中合适的位置,然后data域+1,回溯的时候也要+1。当然,不能放到那棵空树中,要重新建树。第i棵树存的是区间(原序列)[1,i]。但是,如果是这样,那么会MLE+TLE。因此,要充分利用历史版本。用两个指针,分指当前空树和前一棵树。因为每棵树的结构是一样的,只是里面的data域不同,但是两棵相邻的树,只有一数只差,因此,如果元素要进左子树的话,右子树就会跟上个树这个区间的右子树是完全一样的,因此,可以直接将本树本节点的右子树指针接到上棵树当前节点的右儿子,这样即省时间,又省空间。
每添加一个节点(也就是新建一棵树)的复杂度是O(logn),因此,这一步的复杂度是O(nlogn)。
建完之后,要怎么查找呢?
跟一般的,在整棵树中找第k个数是一样的。如果一个节点的左权值(左子树上点的数量之和)大于k,那么就到左子树查找,否则到右子树查找。其实主席树是一样的。对于任意两棵树(分别存区间[1,i]和区间[1,j] i<j),在同一节点上(两节点所表示的区间相同),data域之差表示的是,原序列区间[i,j]在当前节点所表示的区间里,出现多少次(有多少数的大小是在这个区间里的)。同理,对于同一节点,如果在两棵树中,它们的左权值之差大于等于k,那么要求的数就在左孩子,否则在右孩子。当定位到叶子节点时,就可以输出了。
常数优化的技巧
一种在常数上减小内存消耗的方法:插入值时候先不要一次新建到底,能留住就留住,等到需要访问子节点时候再建下去。这样理论内存复杂度依然是O(Nlg^2N),但因为实际上很多结点在查询时候根本没用到,所以内存能少用一些。
动态第K小值
每一棵线段树是维护每一个序列前缀的值在任意区间的个数,如果还是按照静态的来做的话,那么每一次修改都要遍历O(n)棵树,时间就是O(2*M*nlogn)->TLE。考虑到前缀和,我们通过树状数组来优化,即树状数组套主席树,每个节点都对应一棵主席树,那么修改操作就只要修改logn棵树,O(nlognlogn+Mlognlogn)时间是可以的,但是直接建树要nlogn*logn(10^7)会MLE。我们发现对于静态的建树我们只要nlogn个节点就可以了,而且对于修改操作,只是修改M次,每次改变俩个值(减去原先的,加上现在的)也就是说如果把所有初值都插入到树状数组里是不值得的,所以我们分两部分来做,所有初值按照静态来建,内存O(nlogn),而修改部分保存在树状数组中,每次修改logn棵树,每次插入增加logn个节点O(M*logn*logn+nlogn)。
—————————————————————————————————————————————————————————————————————————————
鄙人的一些理解:所谓主席树呢,就是对原来的数列[1..n]的每一个前缀[1..i](1≤i≤n)建立一棵线段树,线段树的每一个节点存某个前缀[1..i]中属于区间[L..R]的数一共有多少个(比如根节点是[1..n],一共i个数,sum[root] = i;根节点的左儿子是[1..(L+R)/2],若不大于(L+R)/2的数有x个,那么sum[root.left] = x)。若要查找[i..j]中第k大数时,设某结点x,那么x.sum[j] - x.sum[i - 1]就是[i..j]中在结点x内的数字总数。而对每一个前缀都建一棵树,会MLE,观察到每个[1..i]和[1..i-1]只有一条路是不一样的,那么其他的结点只要用回前一棵树的结点即可,时空复杂度为O(nlogn)。
Time Limit: 20000MS | Memory Limit: 65536K | |
Total Submissions: 36853 | Accepted: 11837 | |
Case Time Limit: 2000MS |
Description
That is, given an array a[1...n] of different integer numbers, your program must answer a series of questions Q(i, j, k) in the form: "What would be the k-th number in a[i...j] segment, if this segment was sorted?"
For example, consider the array a = (1, 5, 2, 6, 3, 7, 4). Let the question be Q(2, 5, 3). The segment a[2...5] is (5, 2, 6, 3). If we sort this segment, we get (2, 3, 5, 6), the third number is 5, and therefore the answer to the question is 5.
Input
The second line contains n different integer numbers not exceeding 10 9 by their absolute values --- the array for which the answers should be given.
The following m lines contain question descriptions, each description consists of three numbers: i, j, and k (1 <= i <= j <= n, 1 <= k <= j - i + 1) and represents the question Q(i, j, k).
Output
Sample Input
7 3 1 5 2 6 3 7 4 2 5 3 4 4 1 1 7 3
Sample Output
5 6 3
Hint
Source
#include<stdio.h>
#include<algorithm>
#define N 100005
using namespace std;
struct node
{
int x,y,sum;
}a[N*20];
struct pp
{
int x,id;
}b[N];
int num,root[N],rank[N];
bool cmp(pp a,pp b)
{
return a.x<b.x;
}
void update(int k,int &c,int x,int y)
{
a[num++]=a[c]; c=num-1;
++a[c].sum;
if(x==y) return;
int mid=(x+y)>>1;
if(k<=mid)
update(k,a[c].x,x,mid);
else
update(k,a[c].y,mid+1,y);
}
int query(int i,int j,int k,int x,int y)
{
if(x==y) return x;
int t=a[a[j].x].sum-a[a[i].x].sum;
int mid=(x+y)>>1;
if(k<=t)
return query(a[i].x,a[j].x,k,x,mid);
else
return query(a[i].y,a[j].y,k-t,mid+1,y);
}
int main()
{
int n,m,i,j,k;
while(scanf("%d%d",&n,&m)!=EOF)
{
for(i=1;i<=n;i++)
{
scanf("%d",&b[i].x);
b[i].id=i;
}
sort(b+1,b+n+1,cmp);
for(i=1;i<=n;i++)
rank[b[i].id]=i;
a[0].x=a[0].y=a[0].sum=0; root[0]=0; num=1;
for(i=1;i<=n;i++)
{
root[i]=root[i-1];
update(rank[i],root[i],1,n);
// printf("%d\n",root[i]);
}
while(m--)
{
scanf("%d%d%d",&i,&j,&k);
printf("%d\n",b[query(root[i-1],root[j],k,1,n)].x);
}
}
return 0;
}
在整棵树的基础上建立其他树
#include<stdio.h>
#include<algorithm>
#define N 100005
#define M N*20
using namespace std;
int num,n,sum[M],lson[M],rson[M],t[N],rank[N];
struct node
{
int x,id;
}a[N];
bool cmp(node a,node b)
{
return a.x<b.x;
}
int build(int x,int y)
{
int root=num++;
sum[root]=0;
if(x<y)
{
int mid=(x+y)>>1;
lson[root]=build(x,mid);
rson[root]=build(mid+1,y);
}
return root;
}
int update(int root ,int p,int val)
{
int newroot=num++, tmp=newroot;
sum[newroot]=sum[root]+val;
int l=1, r=n;
while(l<r)
{
int mid=(l+r)>>1;
if(p<=mid)
{
lson[newroot]=num++; rson[newroot]=rson[root];
newroot=lson[newroot]; root=lson[root];
r=mid;
}
else
{
rson[newroot]=num++; lson[newroot]=lson[root];
newroot=rson[newroot]; root=rson[root];
l=mid+1;
}
sum[newroot]=sum[root]+val;
}
return tmp;
}
int query(int x,int y,int k)
{
int l=1,r=n;
while(l<r)
{
int mid=(l+r)>>1;
int m=sum[lson[y]]-sum[lson[x]];
if(m>=k)
{
r=mid;
x=lson[x];
y=lson[y];
}
else
{
k-=m;
l=mid+1;
x=rson[x];
y=rson[y];
}
}
return l;
}
int main()
{
int m,i,j,k;
while(scanf("%d%d",&n,&m)!=EOF)
{
for(i=1;i<=n;i++)
{
scanf("%d",&a[i].x);
a[i].id=i;
}
sort(a+1,a+n+1,cmp);
for(i=1;i<=n;i++)
rank[a[i].id]=i;
num=0;
t[0]=build(1,n);
for(i=1;i<=n;i++)
{
t[i]=update(t[i-1],rank[i],1);
}
while(m--)
{
scanf("%d%d%d",&i,&j,&k);
printf("%d\n",a[query(t[i-1],t[j],k)].x);
}
}
return 0;
}