可持久化线段树练习题

可持久化线段树(主席树)练习题

前言:“**出题人你有那大病非得卡我空间。。。”

可持久化线段树并不是 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* &ltree,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\n",query(root[l-1],root[r],(r-l+1)>>1));
  }
  return 0;
}

此代码被卡 MLE on test # 2 ,90 pts 。

T2 [IOI2012]scrivener

题目链接:Link ,双倍经验:Link

题目描述:

维护一个字符串,支持在结尾插入,查询位置为 \(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)<<'\n';//直接查询
    }
  }
  return 0;
}

此代码 AC on 双倍经验,MLE 64 pts on IOI 2012 。

T3 [SDOI 2009]HH的项链

题目链接:Link

题目描述:

给定一个长度为 \(N\) 的序列 \(A\) ,其中 \(A_i\in [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)\) 处又出现了,那么分两种情况:

  1. \(x_2\) 在区间 \([l,r]\) 内,那么这两个 \(p\) 只对答案有 1 的贡献。
  2. \(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\n",query(root[l-1],root[r],r));
  }
  return 0;
}
posted @ 2021-07-05 08:15  ZTer  阅读(138)  评论(2编辑  收藏  举报