• 主席树


    目录:

    • 个人理解
    • 时空复杂度分析
    • 应用及例题
    • 拓展

    一、个人理解:

    主席树的全称是可持续化权值线段树,是一种可以维护静态区间第K小的高级数据结构。

    主席树的主要思想就是:保存每次插入操作时的历史版本,以便查询区间第 (k) 小。

    因为主席树每次都要插入操作,所以是不能用堆式建树的(rt<<1 ,rt<<1|1),所以我们使用动态开点线段树,并用ls[]rs[]保存当前节点的左右儿子。

    联系前缀和,可以预处理达到 (O(1)) 的时间复杂度。我们发现主席树也满足这个性质,所以若需要统计 ([l,r]) 的信息,只需要 (O(1)) 查询sum[r]-sum[l-1]即可。


    二、时空复杂度分析:

    1. 时间复杂度:

      同线段树的时间复杂度:(O(n ext{log}n))

    2. 空间复杂度:

      我们是动态开点,所以一颗线段树只会出现 $2n-1$ 个节点,而每次插入会增加 (O(n ext{log}n)) 个节点,则最坏情况下会有 $2n-1+O(n extn)$ 个节点。故空间复杂度为:(O(n ext{log}n))

      在实际运用的时候,ls[],rs[],sum[]等数组都需要开 $2^5$ 倍空间,即MAXN<<5(MAXN(n) 的值域)。

      :在常数上减小内存消耗:

      插入值时候先不要一次新建到底,能留住就留住,等到需要访问子节点时候再建下去。

      这样理论内存复杂度依然是$O(n extn)$,但因为实际上很多结点在查询时候根本没用到,所以内存能少用一些。

      ————cyendra


    三、应用及例题:

    1. 静态区间第K小(P3834 【模板】可持久化线段树 1(主席树))

      Description:

      给定数列 ({a_n}) ,求闭区间 ([l,r]) 的第 (k) 小的数。

      Method:

      先对数据进行离散化,然后按照权值建立线段树。

      若要寻找 ([1,p]) 的第 (k) 小,则从根节点开始处理。定义$Son_$ 表示左儿子的集合,(Son_{right}) 表示右儿子的集合。若 (|Son_{left}|ge k) 时,说明第$k$小的数在左子树中,以左儿子为新的根向下递归更新,寻找左子树中第 (k) 小的数;反之,说明第$k$小的数在右子树中,以左儿子为新的根向下递归更新,寻找左子树中第 (k-|Son_{left}|) 小的数。

      拓展一下,我们先预处理建树,得到 (n+1) 个版本的线段树(包括初始的线段树),编号为 $0 sim n$ 。

      前文提到过,主席树满足前缀和查询的思想,故我们要求 ([l,r]) 的第 (k) 小值,即可用sum[r]-sum[l-1]

      Code:

      #include<bits/stdc++.h>
      #define int long long 
      #define Maxn 200010
      using namespace std;
      inline void read(int &x)
      {
          int f=1;x=0;char s=getchar();
          while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
          while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
          x*=f;
      }
      int n,m;
      struct Segtree
      {
       int ls,rs,sum;
      }tree[Maxn<<5];
      int rt[Maxn];
      int a[Maxn],ins[Maxn]; 
      int len,tot=0;
      inline void Init(){tot=0;}
      inline int getid(const int &x)
      {
       return lower_bound(ins+1,ins+len+1,x)-ins;
      }
      inline void pushup(int rt)
      {
       tree[rt].sum=tree[tree[rt].ls].sum+tree[tree[rt].rs].sum;
      }
      inline int build(int l,int r)
      {
       int rt=++tot;
       if(l==r) 
       {
       	tree[rt].sum=0;
       	return rt;
       }
       int mid=(l+r)/2;
       tree[rt].ls=build(l,mid);
       tree[rt].rs=build(mid+1,r);
       pushup(rt);
       return rt;
      }
      int update(int k,int l,int r,int root,int val)
      {
       int rt=++tot;
       tree[rt]=tree[root];
       if(l==k&&r==k)
       {
       	tree[rt].sum+=val;
       	return rt;
       }
       int mid=(l+r)/2;
       if(k<=mid) tree[rt].ls=update(k,l,mid,tree[rt].ls,val);
       else tree[rt].rs=update(k,mid+1,r,tree[rt].rs,val);
       pushup(rt);
       return rt;
      }
      int query(int u,int v,int l,int r,int k)
      {
       if(l==r) return l;
       int mid=(l+r)/2,x=tree[tree[v].ls].sum-tree[tree[u].ls].sum;
       if(k<=x) return query(tree[u].ls,tree[v].ls,l,mid,k);
       else return query(tree[u].rs,tree[v].rs,mid+1,r,k-x);
      }
      signed main()
      {
       Init();
       read(n),read(m);
       for(int i=1;i<=n;i++)
       {
       	read(a[i]);
       }
       memcpy(ins,a,sizeof(ins));
       sort(ins+1,ins+n+1);
       len=unique(ins+1,ins+n+1)-ins-1;
       rt[0]=build(1,len);
       for(int i=1;i<=n;i++)
       {
       	rt[i]=update(getid(a[i]),1,len,rt[i-1],1);
       }
       while(m--)
       {
       	int l,r,k;
       	read(l),read(r),read(k);
       	printf("%lld
      ",ins[query(rt[l-1],rt[r],1,len,k)]);
       }
       return 0;
      }
      

      Warning:

      • ls[],rs[],sum[]等数组都要乘上 $2^5$ 。
      • 离散化取lower_bound时,是最后减去0开头的地址,而不是1开头的地址。(即是lower_bound(ins+1,ins+n+1,x)-ins,而不是lower_bound(ins+1,ins+n+1,x)-ins-1
      • 查询时递归右子树时查找第 (k-|Son_{left}|) 小,而不是 (k) 小。
    2. 静态区间互异的个数(SP3267 DQUERY - D-query

      Description:

      给定数列 ({a_n}) ,求闭区间 ([l,r]) 的互异的个数。

      Method:

      扫描序列建立可持续化线段树,若此元素是第一次出现,就将对应的线段树中的位置加1;反之,就将上一个出现的元素对应的线段树中的位置减1,将此元素对应的线段树中的位置加1。

      对于查询的 ([l,r]) ,在第 (r) 个版本的线段树上查询位置 (l) ,对经过的区间中的和累加一下即可。

      Code:

      #include<bits/stdc++.h>
      #define int long long 
      #define Maxn 30010
      using namespace std;
      inline void read(int &x)
      {
          int f=1;x=0;char s=getchar();
          while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
          while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
          x*=f;
      }
      int n,q;
      int a[Maxn]; 
      int tot=0;
      struct Segtree
      {
       int ls,rs,sum;
      }tree[Maxn<<5];
      int rt[Maxn];
      inline void Init(){tot=0;}
      inline void pushup(int rt)
      {
       tree[rt].sum=tree[tree[rt].ls].sum+tree[tree[rt].rs].sum;
      }
      inline int build(int l,int r)
      {
       int rt=++tot;
       if(l==r) 
       {
       	tree[rt].sum=0;
       	return rt;
       }
       int mid=(l+r)/2;
       tree[rt].ls=build(l,mid);
       tree[rt].rs=build(mid+1,r);
       pushup(rt);
       return rt;
      }
      int update(int k,int l,int r,int root,int val)
      {
       int rt=++tot;
       tree[rt]=tree[root];
       if(l==k&&r==k)
       {
       	tree[rt].sum+=val;
       	return rt;
       }
       int mid=(l+r)/2;
       if(k<=mid) tree[rt].ls=update(k,l,mid,tree[rt].ls,val);
       else tree[rt].rs=update(k,mid+1,r,tree[rt].rs,val);
       pushup(rt);
       return rt;
      }
      int query(int l,int r,int rt,int pos)
      {
       if(l==r) return tree[rt].sum;
       int mid=(l+r)/2;
       if(pos<=mid) return tree[tree[rt].rs].sum+query(l,mid,tree[rt].ls,pos);
       else return query(mid+1,r,tree[rt].rs,pos);
      }
      map<int,int>mp;
      signed main()
      {
       Init();
       read(n);
       for(int i=1;i<=n;i++)
       {
       	read(a[i]);
       }
       rt[0]=build(1,n);
       for(int i=1;i<=n;i++)
       {
       	if(mp.find(a[i])==mp.end())
       	{
       		mp[a[i]]=i;
       		rt[i]=update(i,1,n,rt[i-1],1);	
       	}else
       	{
       		int tmprt=update(mp[a[i]],1,n,rt[i-1],-1);
       		rt[i]=update(i,1,n,tmprt,1);
       	}
       	mp[a[i]]=i;
       }
       read(q);
       while(q--)
       {
       	int l,r;
       	read(l),read(r);
       	int ans=query(1,n,rt[r],l);
       	printf("%lld
      ",ans);
       } 
       return 0;
      }
      

    四、扩展

    1. 动态区间第K小

      Description:

      给定数列 ({a_n}) ,支持两种操作:

      • (pos) 位置的值改为 (p)
      • 查询闭区间 ([l,r]) 的第 (k) 小值。

      Mothod

      考虑树套树(树状数组套主席树)

      咕咕咕……

  • 相关阅读:
    动态规划算法-3
    动态规划算法-2
    动态规划算法-1
    滑动窗口算法-3
    央行副行长提示金融风险:地方偿债高峰期到来
    银行卡换“芯” 更要银行换心
    破解IT运维成本困境,专业化分工是妙方
    php连接mysql
    ajax原生验证用户名是否存在
    ajax跨域问题
  • 原文地址:https://www.cnblogs.com/nth-element/p/11755820.html
Copyright © 2020-2023  润新知