主席树

问题

给出n个数,q个询问,求l-r内的第k小值(n,q<=2e5)

方法一:平衡树

方法二:主席树

下面来看一看主席树是怎么做的。

主席树是一种特殊的线段树,针对这一题,我们可以对每个区间[x,y]维护一颗线段树,[l,r]表示在[x,y]区间内,数的大小在[l,r]范围内的数的个数。

但这种思想的实现一定超过了空间与时间的限制。

  1. 考虑优化时间。考虑前缀和的思想,首先离散化数据,sum[1,i]-sum[1,j-1]即代表了区间sum[j,i],这里不妨也可以运用这种思想,只对每一个前缀进行维护。
  2. 但光有上述这点还是不够的。我们会发现,对于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)

原文地址:https://www.cnblogs.com/yinwuxiao/p/7892787.html