- 主席树作为最常用的可持久化数据结构,广泛运用与各种区间、树上问题的在线求解已经对DP的优化上。这里主要讨论其单纯作为数据结构的应用。
P1972 [SDOI2009] HH的项链
- 这是一道极其经典的题——静态区间种类数,其变体非常多,树上的,待修的,强制在线的等等。
这题做法也很多样,离线后树状数组、线段树以及分块都可以做。不过既然是主席树,那就要讲最正统的在线的主席树写法。
思路
-
主席树是如何解决“种类数”这一问题的呢?
首先,主席树维护区间问题与一般的线段树可能有些不同。普通线段树是叶子节点的值对应区间上的相应位置店的值。
而主席树一般而言不这么干。主席树的叶子节点一般是一个数轴,维护每种数的信息,在本质上是一棵权值线段树。
当然这也不绝对,还是要具体情况具体分析。 -
如果考虑直接用主席树维护每种数的出现次数,这是没办法做到的。
因此考虑改变一下维护的东西。有一个很常见的trick是,对于类似种类数的问题,维护与其数值相等的下一个数出现的位置。
想一下这个东西有什么性质。如果对于一个询问区间,当且仅当这个区间中的某一种数下一个数出现的位置比询问区间的右端点大,这种数就能对答案产生1的贡献。 -
因此直接维护对于每个数下一个与其数值相等的数的位置,每次查询的就是以r和l-1为根的线段树区间右端点到n+1(为什么是n+1一会说)的值
小细节
-
首先仍然是主席树的套路。对于原序列上的每个点,都建立线段树记录前缀和。对于上面我们维护的信息显然有可差分性,因此对于询问直接正常差分做就可以了。
-
为什么右端点是n+1呢?因为对于一些数,其之后就没有出现过了,因此我们就直接将其的值赋为n+1,因此值域就是1~n+1了
-
主席树的空间复杂度需要特别注意。每次新插入一个节点时最多新建 \(log_n\) 个节点,因此一般而言开n的20到30倍左右。我为了保险开的25倍
code
(略有压行)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=2e6+7;
int n,m,a[N],tr[N*25],rt[N],idcnt=0,lst[N],val[N],ls[N*25],rs[N*25];
inline void push_up(int u){tr[u]=tr[ls[u]]+tr[rs[u]];}
inline int insert(int last,int l,int r,int w)
{
int u=++idcnt,mid=(l+r)>>1;
ls[u]=ls[last],rs[u]=rs[last],tr[u]=tr[last];
if(l==r){tr[u]+=1;return u;}
if(w<=mid) ls[u]=insert(ls[last],l,mid,w);
else rs[u]=insert(rs[last],mid+1,r,w);
push_up(u);return u;
}
inline int query(int u1,int u2,int l,int r,int ql,int qr)
{
if(l>=ql&&r<=qr) return tr[u1]-tr[u2];
int mid=(l+r)>>1,ans=0;
if(ql<=mid) ans+=query(ls[u1],ls[u2],l,mid,ql,qr);
if(qr>mid) ans+=query(rs[u1],rs[u2],mid+1,r,ql,qr);
return ans;
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;rt[0]=0;
for(int i=1;i<=n;i++) cin>>a[i],lst[a[i]]=n+1;
for(int i=n;i>=1;i--) val[i]=lst[a[i]],lst[a[i]]=i;
for(int i=1;i<=n;i++) rt[i]=insert(rt[i-1],1,n+1,val[i]);
cin>>m;
for(int i=1,l,r;i<=m;i++) {cin>>l>>r;cout<<query(rt[r],rt[l-1],1,n+1,r+1,n+1)<<'\n';}
return 0;
}
P4137 Rmq Problem / mex
- 此题也是非常经典的——静态区间求mex。mex指是指定区间最小的没出现过的自然数(包括0)
思路
-
还是套路地对于每个点建立线段树作为前缀,考虑如何求mex
由于我们求的是前缀,因此我们肯定是基于查询区间右端点为基础去查询的。
对于这种“某数没出现过”的题,我们将维护的值设为其最后一次出现的位置,这样主席树仍然本质上维护的是前缀,即从头开始的、所有数出现的最后的位置。没出现过的数设为0 -
而从叶子节点向上合并统计的时候,我们让非叶子节点记录其管辖的区域内所有点出现的最前面的位置。
这样统计的原因是我们可以以查询区间左端点直接查找。如果区间出现的最左边的数比查询区间左端点小那就向左搜,反之向右搜。
小细节
- 这道题的数值范围很大,比n大得多,因此有很多人使用了离散化。然而其实并不用。显然最终的答案不会大于n+1(给出的是n的排列)
因此对于比n大的数直接赋为n+1就好了。 - 空间复杂度不多赘述,但还是要注意。
- 注意,由于主席树是动态开点的,因此要注意查询时的边界条件。如果这个点从来没有被创建或访问过那就要直接返回其所代表区间的左端点
即这个区间里没有一个点,那这个区间的左端点就是最小的没出现过的数(请注意这是权值线段树)
code
(还是小压行)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+7;int M;
int n,m,a[N],loc[N<<5],ls[N<<5],rs[N<<5],cnt=0,rt[N];
void push_up(int u){loc[u]=min(loc[ls[u]],loc[rs[u]]);}
int modify(int last,int l,int r,int x,int w)
{
int u=++cnt;ls[u]=ls[last],rs[u]=rs[last];
if(l==r) {loc[u]=w;return u;}
int mid=(l+r)>>1;
if(x<=mid) ls[u]=modify(ls[last],l,mid,x,w);
else rs[u]=modify(rs[last],mid+1,r,x,w);
push_up(u);return u;
}
int build(int l,int r)
{
int u=++cnt;int mid=(l+r)>>1;
if(l==r) {loc[u]=0;return u;}
ls[u]=build(l,mid),rs[u]=build(mid+1,r);
push_up(u);return u;
}
int query(int u,int l,int r,int x)
{
if(!u||l==r) return l;
int mid=(l+r)>>1;
if(loc[ls[u]]<x) return query(ls[u],l,mid,x);
else return query(rs[u],mid+1,r,x);
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;M=n+1;rt[0]=build(0,M);
for(int i=1;i<=n;i++){cin>>a[i];if(a[i]>n) a[i]=M;rt[i]=modify(rt[i-1],0,M,a[i],i);}
for(int i=1,l,r;i<=m;i++){cin>>l>>r;cout<<query(rt[r],0,M,l)<<'\n';}
return 0;
}