• 可持久化线段树练习题


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

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

    可持久化线段树并不是 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
    ",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)<<'
    ';//直接查询
        }
      }
      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)) 处又出现了,那么分两种情况:

    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
    ",query(root[l-1],root[r],r));
      }
      return 0;
    }
    
    繁华尽处, 寻一静谧山谷, 筑一木制小屋, 砌一青石小路, 与你晨钟暮鼓, 安之若素。
  • 相关阅读:
    Ubuntu18.04下使用pip3.8报错subprocess.CalledProcessError: Command ‘(‘lsb_release‘, ‘-a‘)‘ returned non-ze
    解决报错:ModuleNotFoundError: No module named ‘_sqlite3‘
    shell命令中find的用法
    Ubuntu 中卸载软件
    git使用
    django celery 使用
    Django 学习中遇到的问题
    1
    Mac 下安装brew(文末方法亲测有效)
    经典类与新式类的继承顺序
  • 原文地址:https://www.cnblogs.com/zaza-zt/p/14970697.html
Copyright © 2020-2023  润新知