主席树
问题
给出n个数,q个询问,求l-r内的第k小值(n,q<=2e5)
方法一:平衡树
方法二:主席树
下面来看一看主席树是怎么做的。
主席树是一种特殊的线段树,针对这一题,我们可以对每个区间[x,y]维护一颗线段树,[l,r]表示在[x,y]区间内,数的大小在[l,r]范围内的数的个数。
但这种思想的实现一定超过了空间与时间的限制。
- 考虑优化时间。考虑前缀和的思想,首先离散化数据,sum[1,i]-sum[1,j-1]即代表了区间sum[j,i],这里不妨也可以运用这种思想,只对每一个前缀进行维护。
- 但光有上述这点还是不够的。我们会发现,对于sum[1,i]和sum[1,i+1]这两个状态,只有一个元素的差异,所以可以考虑持久化。
即对sum[1,i+1]的[l,r]来说,若[l,mid]和sum[1,i]是相同的,则可以直接指向sum[1,i]的[l,mid],若是不相同的,则新建一个节点。很容易发现,这样的空间复杂度是logn*n的。
补充1:对于2,有其一定的实现技巧。
//此处的insert指插入x节点
procedure insert(pre,x,h,t:longint);
var tmp,mid:longint;
begin
inc(now); p[now]:=p[pre]; inc(p[now].x); tmp:=now;
if h=t then exit;
mid:=(h+t) div 2;
if (x<=mid ) then
begin
insert(p[now].h,x,h,mid);
p[tmp].h:=tmp+1;
end else
begin
insert(p[now].t,x,mid+1,t);
p[tmp].t:=tmp+1;
end;
end;
补充2:对于query操作,考虑左边数的个数>=sum,则往左边走,反之往右走。但此处由于前缀和操作的存在增加了一定的复杂性。其实也就是要同时统计出两个区间1-x,1-y的数值的当前范围。
function query(x,y,h,t,sum:longint):longint;
var tmp,mid:longint;
begin
if (h=t) then exit(h);
tmp:=p[p[y].h].x-p[p[x].h].x;
mid:=(h+t) div 2;
if tmp<sum then exit(query(p[x].t,p[y].t,mid+1,t,sum-tmp))
else exit(query(p[x].h,p[y].h,h,mid,sum));
end;
以上的是离线的主席树,时间复杂度是o(nlogn) 空间(nlogn);
完整代码:
uses math;
type re=record
a,b,c:longint;
end;
type ree=record
x,h,t:longint;
end;
var
i,j,m,n,now,l,c,d,e,tmp:longint;
a:array[0..202222]of re;
num,f,real:array[0..222222]of longint;
p:array[0..10002222]of ree;
procedure swap(var x,y:re);
var tmp:re;
begin
tmp:=x; x:=y; y:=tmp;
end;
procedure qsort(h,t:longint);
var i,j,mid:longint;
begin
i:=h; j:=t; mid:=a[(i+j) div 2].a;
repeat
while a[i].a<mid do inc(i);
while a[j].a>mid do dec(j);
if i<=j then
begin
swap(a[i],a[j]); inc(i); dec(j);
end;
until i>j;
if i<t then qsort(i,t);
if h<j then qsort(h,j);
end;
procedure build(x,h,t:longint);
var mid:longint;
begin
p[x].h:=x*2; p[x].t:=x*2+1; now:=max(now,x*2+1);
if h=t then exit;
mid:=(h+t) div 2;
build(x*2,h,mid); build(x*2+1,mid+1,t);
end;
procedure insert(pre,x,h,t:longint);
var tmp,mid:longint;
begin
inc(now); p[now]:=p[pre]; inc(p[now].x); tmp:=now;
if h=t then exit;
mid:=(h+t) div 2;
if (x<=mid ) then
begin
insert(p[now].h,x,h,mid);
p[tmp].h:=tmp+1;
end else
begin
insert(p[now].t,x,mid+1,t);
p[tmp].t:=tmp+1;
end;
end;
function query(x,y,h,t,sum:longint):longint;
var tmp,mid:longint;
begin
if (h=t) then exit(h);
tmp:=p[p[y].h].x-p[p[x].h].x;
mid:=(h+t) div 2;
if tmp<sum then exit(query(p[x].t,p[y].t,mid+1,t,sum-tmp))
else exit(query(p[x].h,p[y].h,h,mid,sum));
end;
begin
readln(n,m);
for i:=1 to n do
begin
read(a[i].a);
a[i].b:=i;
end;
qsort(1,n);
a[0].a:=-maxlongint; l:=0;
for i:=1 to n do
begin
if a[i].a<>a[i-1].a then inc(l);
num[a[i].b]:=l;
real[l]:=a[i].a;
end;
build(1,1,n);
f[0]:=1;
for i:=1 to n do
begin
f[i]:=now+1;
insert(f[i-1],num[i],1,n);
end;
for i:=1 to m do
begin
read(c,d,e);
writeln(real[query(f[c-1],f[d],1,n,e)]);
end;
end.
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000000;
struct re{int a,b;};
struct ree{int h,t,x;};
re a[maxn];
ree p[maxn*20];
int n,m,l,now,rea[maxn],num[maxn],f[maxn];
void build(int x,int h,int t)
{
p[x].h=x*2; p[x].t=x*2+1; now=max(now,x*2+1);
if (h==t) return;
int mid=(h+t)/2;
build(x*2,h,mid); build(x*2+1,mid+1,t);
};
void insert(int pre,int x,int h,int t)
{
now++; p[now]=p[pre]; p[now].x++;
if (h==t) return;
int mid=(h+t)/2;
if (x<=mid) p[now].h=now+1,insert(p[pre].h,x,h,mid);
else p[now].t=now+1,insert(p[pre].t,x,mid+1,t);
};
int query(int x,int y,int h,int t,int sum)
{
if (h==t) return(h);
int tmp=p[p[y].h].x-p[p[x].h].x;
int mid=(h+t)/2;
if (tmp<sum) return(query(p[x].t,p[y].t,mid+1,t,sum-tmp));
else return(query(p[x].h,p[y].h,h,mid,sum));
};
bool cmp(re x,re y)
{
return(x.a<y.a);
};
int main(){
cin>>n>>m;
for (int i=1;i<=n;i++) cin>>a[i].a,a[i].b=i;
sort(a+1,a+n+1,cmp);
a[0].a=-500000000;
for (int i=1;i<=n;i++)
{
if (a[i].a!=a[i-1].a) l++;
num[a[i].b]=l;
rea[l]=a[i].a;
}
build(1,1,n);
f[0]=1;
for (int i=1;i<=n;i++)
{
f[i]=now+1;
insert(f[i-1],num[i],1,n);
}
for (int i=1; i<=m;i++)
{
int c,d,e;
cin>>c>>d>>e;
cout<<(rea[query(f[c-1],f[d],1,n,e)])<<endl;
}
}
总的来说,主席树是前缀和+动态开点的线段树
接下来的是在线版的主席树,要求支持区间的修改。
这种算法保留了主席树中的权值线段树和动态开点的思想
同时加入了树状数组维护(因此没有了前缀和维护)
即对于每一个节点开一颗权值线段树,查询和修改时类似树状数组的思想
时间复杂度(nlognlogn) 空间复杂度(nlognlogn)