可持久化线段树
可持久化线段树
可持久化线段树,也常被叫做主席树,和发明者的外号有关,学习前需要先熟悉线段树
适用问题
一般用于处理两类问题:
①求区间第K大(也可求动态区间K大)
②查询历史版本的区间询问
原理
解决上述两种问题用到的结构,本质上是一样的。在逻辑上相当于建了n棵线段树,对于区间K大来讲,每棵树代表一个前缀,对于持久化数据来讲,每棵树记录一个更改版本。当然,如果真的建n棵线段树,在时空上都是不允许的。考虑到树与树之间其实有大量相同的节点,因此每一棵树都可以尽可能的复用前一棵树的节点,只新建有变化的节点。换句话说,实际上每加一棵树只是加一条链。想要更好地理解,可以看一下该博客的博主画的图。
实现
以三道裸题为例,分别是求静态区间K大、单点修改的历史询问,成段修改的历史询问的模版题。
先说求静态区间K大(动态区间K大以后再补),对于一组数a[1-n],第i棵树维护的是数组区间[1-i],也就是前缀。树中的节点记录,数组a[1-i]中落在某个数值区间内的数的个数(这样在求第k大的时候,就可以根据当前数值区间有多少个数,快速找到第k个数)。如果建好了第i棵树,那么建第i+1棵树的时候,由于只是多了个a[i+1],所以有变化的节点其实很少,大多数节点直接用第i棵树的即可。文字描述比较抽象,实现一次可以清楚一些细节,POJ2104是一道求区间第K大且不带修改的模板题,可以敲一下。
POJ_2014代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<map>
#include<queue>
#include<string>
#include<vector>
#include<cmath>
#include<climits>
#include<functional>
#include<set>
#define dd(x) cout<<#x<<" = "<<x<<" "
#define de(x) cout<<#x<<" = "<<x<<endl
#define fi first
#define se second
#define mp make_pair
#define pb push_back
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
typedef vector<int> V;
typedef map<int,int> M;
typedef queue<int> Q;
typedef priority_queue<int> BQ;
typedef priority_queue<int,vector<int>,greater<int> > SQ;
const int maxn=1e5+10,INF=0x3f3f3f3f;
int cntx,cntn,root[maxn],cnt[maxn*22],ls[maxn*22],rs[maxn*22];
int a[maxn],rk[maxn];
void upd(int pre,int& now,int p,int l,int r)
{
now=++cntn;
cnt[now]=cnt[pre]+1;
ls[now]=ls[pre];
rs[now]=rs[pre];
if (l==r)
return;
int m=(l+r)>>1;
if (p<=m)
upd(ls[pre],ls[now],p,l,m);
else
upd(rs[pre],rs[now],p,m+1,r);
}
int qry(int L,int R,int k,int l,int r)
{
if (l==r)
return l;
int c=cnt[ls[R]]-cnt[ls[L]];
int m=(l+r)>>1;
if (k<=c)
return qry(ls[L],ls[R],k,l,m);
else
return qry(rs[L],rs[R],k-c,m+1,r);
}
int main()
{
int n,q;
scanf("%d%d",&n,&q);
for (int i=1;i<=n;++i)
{
scanf("%d",&a[i]);
rk[++cntx]=a[i];
}
sort(rk+1,rk+1+cntx);
cntx=unique(rk+1,rk+1+cntx)-rk-1;
for (int i=1;i<=n;++i)
{
int p=lower_bound(rk+1,rk+1+cntx,a[i])-rk;
upd(root[i-1],root[i],p,1,cntx);
}
while (q--)
{
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
int ans=qry(root[l-1],root[r],k,1,cntx);
printf("%d\n",rk[ans]);
}
return 0;
}
再讲一下解决历史版本查询问题的主席树,洛谷模板题,给一组数,两种操作,1操作:选定一个版本和下标,将它的值改为v,从而生成一个新版本i;2操作:选定一个版本和下标,询问其值,并生成一份拷贝版本i。我们可以使用主席树,每个版本生成一棵线段树,记录其修改的部分,未修改的部分则复用原版本的节点。具体细节见代码
洛谷_P3919代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<map>
#include<queue>
#include<string>
#include<vector>
#include<cmath>
#include<climits>
#include<functional>
#include<set>
#define dd(x) cout<<#x<<" = "<<x<<" "
#define de(x) cout<<#x<<" = "<<x<<endl
#define fi first
#define se second
#define mp make_pair
#define pb push_back
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
typedef vector<int> V;
typedef map<int,int> M;
typedef queue<int> Q;
typedef priority_queue<int> BQ;
typedef priority_queue<int,vector<int>,greater<int> > SQ;
const int maxn=1e6+10,INF=0x3f3f3f3f;
int cnt,root[maxn],ls[maxn*22],rs[maxn*22];
int a[maxn*22];
void build(int& now,int l,int r)
{
now=++cnt;
if (l==r)
{
scanf("%d",&a[now]);
return;
}
int m=(l+r)>>1;
build(ls[now],l,m);
build(rs[now],m+1,r);
}
void upd(int pre,int& now,int p,int v,int l,int r)
{
now=++cnt;
ls[now]=ls[pre];
rs[now]=rs[pre];
if (l==r)
{
a[now]=v;
return;
}
int m=(l+r)>>1;
if (p<=m)
upd(ls[pre],ls[now],p,v,l,m);
else
upd(rs[pre],rs[now],p,v,m+1,r);
}
int qry(int p,int l,int r,int now)
{
if (l==r)
return a[now];
int m=(l+r)>>1;
if (p<=m)
return qry(p,l,m,ls[now]);
else
return qry(p,m+1,r,rs[now]);
}
int main()
{
int n,q;
scanf("%d%d",&n,&q);
build(root[0],1,n);
for (int i=1;i<=q;++i)
{
int op,ver,p,v;
scanf("%d%d%d",&ver,&op,&p);
if (op==1)
{
scanf("%d",&v);
upd(root[ver],root[i],p,v,1,n);
}
else
{
root[i]=root[ver];
printf("%d\n",qry(p,1,n,root[ver]));
}
}
return 0;
}
HDU4348,这一题属于成段修改,题意就是一组数,有成段加d操作,有当前区间询问和历史区间询问,以及时间回溯操作。关于主席树的成段修改要用到永久化标记,这里简单提一下。线段树的成段修改一般有两种方法,一种是在将要访问子节点时将标记下传更新(如HH大神的写法),还有一种就是标记永久化,具体可以看一下这篇博客。(不过有一些类型的标记和询问好像没办法用永久化标记,不是很确定)回到主席树,由于其结构决定了标记难以下传,特别是hdu这题限制了空间之后,只能用标记永久化。把线段树的标记永久化写法稍作修改就可以实现主席树的成段修改了。除此之外,没什么太特别的。细节见码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<map>
#include<queue>
#include<string>
#include<vector>
#include<cmath>
#include<climits>
#include<functional>
#include<set>
#define dd(x) cout<<#x<<" = "<<x<<" "
#define de(x) cout<<#x<<" = "<<x<<endl
#define fi first
#define se second
#define mp make_pair
#define pb push_back
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
typedef vector<int> V;
typedef map<int,int> M;
typedef queue<int> Q;
typedef priority_queue<int> BQ;
typedef priority_queue<int,vector<int>,greater<int> > SQ;
const int maxn=1e5+10,INF=0x3f3f3f3f;
int cntn;
int root[maxn],ls[maxn*80],rs[maxn*80];
ll sum[maxn*80],lazy[maxn*80];
void build(int& now,int l,int r)
{
now=++cntn;
if (l==r)
{
scanf("%lld",&sum[now]);
return;
}
int m=(l+r)>>1;
build(ls[now],l,m);
build(rs[now],m+1,r);
sum[now]=sum[ls[now]]+sum[rs[now]];
}
void upd(int L,int R,int d,int pre,int& now,int l,int r)
{
now=++cntn;
ls[now]=ls[pre];
rs[now]=rs[pre];
sum[now]=sum[pre]+d*(R-L+1);
lazy[now]=lazy[pre];
if (L==l&&r==R)
{
lazy[now]+=d;
return;
}
int m=(l+r)>>1;
if (R<=m)
upd(L,R,d,ls[pre],ls[now],l,m);
else if (L>m)
upd(L,R,d,rs[pre],rs[now],m+1,r);
else
upd(L,m,d,ls[pre],ls[now],l,m),upd(m+1,R,d,rs[pre],rs[now],m+1,r);
return;
}
ll qry(int L,int R,int add,int l,int r,int now)
{
if (L==l&&r==R)
return sum[now]+add*(R-L+1);
int m=(l+r)>>1;
if (R<=m)
return qry(L,R,add+lazy[now],l,m,ls[now]);
else if (L>m)
return qry(L,R,add+lazy[now],m+1,r,rs[now]);
else
return qry(L,m,add+lazy[now],l,m,ls[now])+qry(m+1,R,add+lazy[now],m+1,r,rs[now]);
}
int main()
{
int n,m;
while (scanf("%d%d",&n,&m)!=EOF)
{
int t=0;
cntn=0;
build(root[0],1,n);
while (m--)
{
char op[2];
int l,r,_t,d;
scanf("%s",op);
if (op[0]=='C')
{
scanf("%d%d%d",&l,&r,&d);
++t;
upd(l,r,d,root[t-1],root[t],1,n);
}
else if (op[0]=='Q')
{
scanf("%d%d",&l,&r);
printf("%lld\n",qry(l,r,0,1,n,root[t]));
}
else if (op[0]=='H')
{
scanf("%d%d%d",&l,&r,&_t);
printf("%lld\n",qry(l,r,0,1,n,root[_t]));
}
else
{
scanf("%d",&_t);
t=_t;
}
}
}
return 0;
}