可持久化线段树(主席树)练习题
前言:“**出题人你有那大病非得卡我空间。。。”
可持久化线段树并不是 NOIp 考点,但是赛场上谁管你用什么算法,能得分就行。我学习可持久化线段树的目的其实在于能多骗点分,在赛场上万一不会了可以拿来乱搞。所以了解可持久化线段树能用在哪些地方还是十分有必要的。
另外,据机房大佬说有些题就是卡空间,MLE 没关系,保证解法正确性就可以,毕竟指针在洛谷上算 2 倍空间。
T1 [POI2014]KUR-Couriers
题目链接:Link
题目描述:
给定一个长度为 (n) 的正整数序列 (A) 。共 (m) 组询问,每次询问在区间 ([l,r]) 内是否存在一个数出现次数严格超过一半,如果有,输出这个数,否则输出 0 。
Solution:
这是我唯二没看题解自己做出来的一道可持久化题目。
题目中一提“数的出现次数”,令我马上想到了主席树。回忆起:在“区间第 (k) 小”问题中,主席树的每个节点记录了大小在 ([L,R]) 之间的数的数量,同时不断作差,决定递归左儿子还是右儿子。
这题可以效仿一下,(cnt) 同上记录。考虑整数区间 ([L,R]) 之间有 (p) 个数,其中有一个数 (x) 其出现次数严格大于 (frac p2) ,那么显然不可能有另一个数 (y) ,使得 (y) 的出现次数也严格大于 (p) 。取 (L,R) 的均值 (M) ,把 ([L,R]) 分为两段: ([L,M],[M+1,R]) ,(x) 必定属于且仅属于这两个区间的其中一个。则 (x) 所属的区间的数的数量也一定严格大于 (frac p2) ,另一个区间的数的数量一定严格小于 (frac p2) (因为 (y) 不存在)。此时去 (x) 所在区间继续寻找,重复这个二分寻找的过程,直到把值域 ([L,R]) 缩小到只有一个数,这个数就是所求的 (x) 。
Code:
因为思路很简单,代码大部分和主席树差不多,不详细注释。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
//using namespace std;
const int maxn=500005;
#define ll long long
template <typename T>
inline T const& read(T &x){
x=0;int fh=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')
fh=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+ch-'0';
ch=getchar();
}
x*=fh;
}
int n,m;
int A[maxn];
struct chairman_tree{
int l,r,cnt;
chairman_tree *ls,*rs;
};
struct chairman_tree byte[maxn*19],*pool=byte,*root[maxn];
chairman_tree* New(){
return ++pool;
}
void update(chairman_tree* &node){
node->cnt=node->ls->cnt+node->rs->cnt;
}
inline bool outofrange(chairman_tree* &node,const int L,const int R){
return (node->r<L)||(R<node->l);
}
chairman_tree* Build(const int L,const int R){
chairman_tree *u=New();
u->l=L,u->r=R;
if(L==R){
u->cnt=0;
u->ls=u->rs=NULL;
}else{
int Mid=(L+R)>>1;
u->ls=Build(L,Mid);
u->rs=Build(Mid+1,R);
update(u);
}
return u;
}
void modify(chairman_tree* &pre,chairman_tree* &now,const int p){
*now=*pre;
if(pre->l==p && pre->r==p){
now->cnt++;
return;
}
if(!outofrange(pre->ls,p,p)){
now->ls=New();
modify(pre->ls,now->ls,p);
update(now);
}else{
now->rs=New();
modify(pre->rs,now->rs,p);
update(now);
}
}
int query(chairman_tree* <ree,chairman_tree* &rtree,const int half){
if(rtree->cnt-ltree->cnt<=half)//不满足条件
return 0;
if(ltree->l==ltree->r)//已经找到
return ltree->l;
int lcnt=rtree->ls->cnt-ltree->ls->cnt;//左区间的数的个数
if(lcnt>half) return query(ltree->ls,rtree->ls,half);//左区间的数的个数是否大于一半
else return query(ltree->rs,rtree->rs,half);
}
signed main(){
read(n),read(m);
for(int i=1;i<=n;++i)
read(A[i]);
root[0]=Build(1,n);
for(int i=1;i<=n;++i)
modify(root[i-1],root[i]=New(),A[i]);
for(int i=0,l,r;i<m;++i){
read(l),read(r);
printf("%d
",query(root[l-1],root[r],(r-l+1)>>1));
}
return 0;
}
此代码被卡 MLE on test # 2 ,90 pts 。
T2 [IOI2012]scrivener
题目描述:
维护一个字符串,支持在结尾插入,查询位置为 (x) 的字符,撤销之间的 (x) 次插入或者撤销操作。
Solution:
看到撤销操作,很自然的想到了可持久化数据结构。
- 对于撤销 (x) 次操作,直接让根变成 (x) 次操作之前的版本就行了。
- 对于插入操作,主席树不支持直接返回最后一个元素的位置(可以 (logn) 查一下,但是代价太大)。于是要维护一个指针数组 (seq) ,表示第 (i) 次插入在 (seq_i) 之后进行,撤销时根据 (seq) 数组更新下一次在哪插入就行了。
- 对于查询操作:直接查询给定位置字符即可。
Code:
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
//using namespace std;
const int maxn=1000005;
#define ll long long
template <typename T>
inline void read(T &x){
x=0;int fh=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')
fh=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+ch-'0';
ch=getchar();
}
x*=fh;
}
int n,m,tot;
int seq[maxn];
struct chairman_tree{
int l,r;
int value;//记录这个节点的字符的 ASCII 码
chairman_tree *ls,*rs;
};
struct chairman_tree byte[maxn*20],*pool=byte,*root[maxn];
chairman_tree* New(){
return ++pool;
}
inline bool outofrange(chairman_tree* &node,const int L,const int R){
return (node->r<L)||(R<node->l);
}
chairman_tree* Build(const int L,const int R){
chairman_tree *u=New();
u->l=L,u->r=R;
if(L==R){
u->ls=u->rs=NULL;
u->value=0;
}else{
int Mid=(L+R)>>1;
u->ls=Build(L,Mid);
u->rs=Build(Mid+1,R);
}
return u;
}
void insert(chairman_tree* &pre,chairman_tree* &now,const int p,const int ch){
*now=*pre;
if(pre->l==p && pre->r==p){
now->value=ch;
return;
}
if(!outofrange(pre->ls,p,p)){
now->ls=New();
insert(pre->ls,now->ls,p,ch);
}else{
now->rs=New();
insert(pre->rs,now->rs,p,ch);
}
}
//这一堆都同普通主席树
int query(chairman_tree* &node,const int p){
if(node->l==node->r)
return node->value;
if(!outofrange(node->ls,p,p))
return query(node->ls,p);
else return query(node->rs,p);
}//这里不断向包含查询位置的区间递归
signed main(){
read(n);
root[0]=Build(1,n);
for(int i=1,x;i<=n;++i){
char ch,c;
std::cin>>ch;
if(ch=='T'){
std::cin>>c;
tot++;
seq[tot]=seq[tot-1]+1;//维护 seq,直接对上一个版本的 seq +1
insert(root[tot-1],root[tot]=New(),seq[tot],(int)c);
}else if(ch=='U'){
read(x);
tot++;//撤销也算操作,要生成一个一模一样的版本
root[tot]=New();
*root[tot]=*root[(tot-x-1>0)?(tot-x-1):0];//复制根
seq[tot]=seq[(tot-x-1)>0?(tot-x-1):0];//更新下一个版本在哪里插入
}else{
read(x);
std::cout<<(char)query(root[tot],x)<<'
';//直接查询
}
}
return 0;
}
此代码 AC on 双倍经验,MLE 64 pts on IOI 2012 。
T3 [SDOI 2009]HH的项链
题目链接:Link
题目描述:
给定一个长度为 (N) 的序列 (A) ,其中 (A_iin [1,10^6]),每次询问 ([l,r]) 区间内有多少个不同的数。
Solution:
这题乍一看似乎可以主席树直接 (cnt) 维护区间数的个数,实则不然。如果直接维护 (cnt) 的话,要做到去重就得访问到叶子节点,这样每次查询可能退化为 (O(n)) 的复杂度,会爆掉。
考虑一个数 (p) 什么时候对查询区间 ([l,r]) 的答案有贡献。不妨设 ([l,r]) 中 (p) 第一次出现的位置为 (x_1) ,再假设 (p) 在 (x_2(x_1<x_2)) 处又出现了,那么分两种情况:
- (x_2) 在区间 ([l,r]) 内,那么这两个 (p) 只对答案有 1 的贡献。
- (x_2) 不在区间 ([l,r]) 内,那么 (x_1) 位置的 (p) 对答案有 1 的贡献。
得到这样一个结论,一个数 (p) 在 ([l,r]) 中出现,如果它下一次出现不在 ([l,r]) 内,这个 (p) 才对答案有贡献。于是答案就转化为:求 ([l,r]) 中每个数下一次出现位置 (x),如果 (x>r) 答案 +1 ,反之则答案不变。
预处理序列 (A) ,得到一个数组 (next) 其中 (next_i) 表示 (A_i) 下一次出现的位置,如果 (A_i) 没有再次出现过,令 (next_i= n+1) 。用主席树维护 (next) 数组,主席树 ([l,r]) 版本之间查询大于 (r) 的 (next_i) 的数量即为答案。
Code:
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<vector>
//using namespace std;
const int maxn=1000005;
#define ll long long
template <typename T>
inline void read(T &x){
x=0;int fh=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')
fh=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+ch-'0';
ch=getchar();
}
x*=fh;
}
int n,m;
int a[maxn],vis[maxn];
int next[maxn];
std::vector<int>v[maxn];
void Init(){
read(n);
for(int i=1;i<=n;++i){
read(a[i]);
v[a[i]].push_back(i);//a[i]在位置i出现过
next[i]=n+1;
}
for(int i=1;i<=n;++i)
if(v[a[i]].size()>=2 && !vis[a[i]]){
int k=i;vis[a[i]]=1;
for(int j=1;j<v[a[i]].size();j++)
next[k]=v[a[i]][j],k=v[a[i]][j];
}
//这段求 next[i] 也太丑了吧...
// for(int i=1;i<=n;++i)
// printf("%d ",next[i]);
}
struct chairman_tree{
int l,r,cnt;
chairman_tree *ls,*rs;
};
struct chairman_tree byte[maxn*20],*pool=byte,*root[maxn];
chairman_tree* New(){
return ++pool;
}
inline void update(chairman_tree* &node){
node->cnt=node->ls->cnt+node->rs->cnt;
}
inline bool outofrange(chairman_tree* &node,const int L,const int R){
return (node->r<L)||(R<node->l);
}
chairman_tree* Build(const int L,const int R){
chairman_tree *u=New();
u->l=L,u->r=R;
if(L==R){
u->ls=u->rs=NULL;
u->cnt=0;
}else{
int Mid=(L+R)>>1;
u->ls=Build(L,Mid);
u->rs=Build(Mid+1,R);
update(u);
}
return u;
}
void modify(chairman_tree* &pre,chairman_tree* &now,const int p){
*now=*pre;
if(pre->l==p && pre->r==p){
now->cnt++;
return;
}
if(!outofrange(pre->ls,p,p)){
modify(pre->ls,now->ls=New(),p);
update(now);
}else{
modify(pre->rs,now->rs=New(),p);
update(now);
}
}
int query(chairman_tree* &Ltree,chairman_tree* &Rtree,const int k){//查询大于k的数的个数
if(Ltree->l==Ltree->r)
return Rtree->l>k?Rtree->cnt-Ltree->cnt:0;
int Mid=(Ltree->l+Ltree->r)>>1;
int rcnt=Rtree->rs->cnt-Ltree->rs->cnt;
return k>Mid?query(Ltree->rs,Rtree->rs,k):rcnt+query(Ltree->ls,Rtree->ls,k);
}
signed main(){
Init();
root[0]=Build(1,n+1);
for(int i=1;i<=n;i++)
modify(root[i-1],root[i]=New(),next[i]);
read(m);
for(int i=1,l,r;i<=m;i++){
read(l),read(r);
printf("%d
",query(root[l-1],root[r],r));
}
return 0;
}