zoukankan      html  css  js  c++  java
  • 主席树 笔记

    废话

    首先介绍一下主席树与其名字的来历。主席树是“可持久化线段树”(Persistant Segment Tree)的中文民间俗称。不知道是因为有人把 Persistant 看成了 Presidant,还是因为它的发明者是 HJT(和某一任国家主席简称相同),被叫做“主席树”。

    但是,可持久化是啥呢?

    可持久化是啥

    可持久化是亿点小优化,当你要开 (n) 个线段树,但是线段树之间只差一次修改的时候,把空间复杂度降到 (O(nlog n))

    如何优化呢?考虑我们现在要对一个单点进行修改,那么我们能影响到的位置(就是这个点以及它的祖先),一共才 (log n) ,个而其它位置都一样,所以我们可以 只新建 (log n) 个位置,整一个副本出来,其余位置直接继承原树即可。

    蒯一张图举个例子,(n=5),修改了第四个点:

    原作者

    原博客

    这样就能节省下超级多的空间!

    但是,有利总有弊。这样的结构破坏的原来线段树的编号的完美特性,不能再用 index<<1index<<1|1 来访问左右儿子了。我们要在线段树中多维护两个元素,左儿子和右儿子的编号。

    经典题型

    静态/动态区间第 (k) 位:给定一个序列,每次给出 (l,r,k),求 ([l,r]) 中的数排序后,第 (k) 个位置的数是谁。(当然,我们不会实际去排序,所以询问操作对原序列没有影响)

    如果是动态的,那你还要支持单点修改的操作,会给定 (x,y),要支持把第 (x) 个位置上的数改成 (y)

    (假设我们离散化过了所有 (a_i),和所有询问修改后变成的 (y)

    静态的问题

    板子见 洛谷 3834

    对于一个查询 ((l,r,k)) ,(假设我们能)把 (a[lcdots r]) 拿出来建一颗权值线段树 (T),查询就很容易了:把区间分成左半部分和右半部分,设左半部分中数字的个数为 (lsum)。如果 (kle lsum),那么就在左半部分中查找,否则就在右半部分中查找第 (k-lsum) 个。

    但是我们怎么把 (a[lcdots r]) 拿出来建一颗权值线段树呢?这个时空复杂度都是 (O(nlogn)) 每次,再乘一个询问次数,肯定炸。

    但是我们发现,线段树满足一种微妙的“可减性”:我们考虑建 (n) 颗线段树 (T)(T_i) 表示 (a[1cdots i]) 组成的权值线段树。然后 (a[lcdots r]) 组成的权值线段树的对应位置就是 (T_r) 的对应位置减去 (T_{l-1})的对应位置。但是把 (T) 建出来,光空间就够炸的了,是 (O(n^2))

    考虑用上面“可持久化”的办法来优化求 (T) 的过程:(T_i)(T_{i-1})之间,差的只是在(离散化后的) (a_i) 位置上加一。那么我们就让 (T_i)(T_{i-1}) 的基础上,复制其中的 (log) 条链即可。这样就可以空间复杂度 (O(nlog n)) ,时间复杂度 (O(nlog^2 n)) 的过去了。

    一个伏笔

    请把它理解成“前缀和套线段树”。

    那么恭喜您,现在您已经会了一个嵌套型的数据结构了

    静态问题(板子)的代码
    点击展开/折叠
    #include <bits/stdc++.h>
    using namespace std;
    namespace Flandre_Scarlet
    {   
        #define N 200005
        #define F(i,l,r) for(int i=l;i<=r;++i)
        #define D(i,r,l) for(int i=r;i>=l;--i)
        #define Fs(i,l,r,c) for(int i=l;i<=r;c)
        #define Ds(i,r,l,c) for(int i=r;i>=l;c)
        #define MEM(x,a) memset(x,a,sizeof(x))
        #define FK(x) MEM(x,0)
        #define Tra(i,u) for(int i=G.Start(u),v=G.To(i);~i;i=G.Next(i),v=G.To(i))
        #define p_b push_back
        #define sz(a) ((int)a.size())
        #define iter(a,p) (a.begin()+p)
        int I()
        {
            int x=0;char c=getchar();int f=1;
            while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar();
            while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
            return (x=(f==1)?x:-x);
        }
        void Rd(int cnt,...)
        {
            va_list args; va_start(args,cnt);
            F(i,1,cnt) {int* x=va_arg(args,int*);(*x)=I();}
            va_end(args);
        }
        class Persistant_Tree
        {
        public:
            struct node{int lid,rid; int l,r; int s;} t[N<<5];
            // 注意空间往死里开
            // lid,rid 存储左右儿子编号
            int rt[N],tot=0;
            // rt[i] 存第 i 颗线段树的根的编号
            #define ls(x) t[x].lid
            #define rs(x) t[x].rid
            #define L  t[rt].l
            #define R  t[rt].r
            #define S  t[rt].s
            #define up(x) t[x].s=t[t[x].lid].s+t[t[x].rid].s
            void Init() {tot=0;}
            int Build(int l,int r) // 建树,这一步和普通的线段树差别不大
            {
                int rt=++tot;
                L=l,R=r,S=0;
                if (l<r)
                {
                    int mid=(l+r)>>1;
                    ls(rt)=Build(l,mid); 
                    rs(rt)=Build(mid+1,r); 
                }
                return rt;
            }
            int Insert(int pos,int rt) 
            // 在 rt 这个根的基础上,修改了 pos 位置
            // 把一路上修改的线段树上的链存一个副本,并返回这个链的顶端
            {
                int rr=++tot;
                t[rr].l=L; t[rr].r=R; t[rr].s=S+1;
                ls(rr)=ls(rt); rs(rr)=rs(rt); // 默认直接继承
    
                if (L<R)
                {
                    int mid=(L+R)>>1;
                    if (pos<=mid) ls(rr)=Insert(pos,ls(rt)); // 如果要修改左儿子,那么就左儿子开一个副本
                    else          rs(rr)=Insert(pos,rs(rt)); // 右儿子同理
                }
                return rr;
            }
            int QueryKth(int r1,int r2,int l,int r,int k) // 在 T[r2]-T[r1] 这颗权值线段树中,查询排名为 k 的位置
            {
                if (l==r) return l;
    
                int mid=(l+r)>>1;
                int lsum=t[ls(r2)].s-t[ls(r1)].s; // 左边有多少数
                if (k<=lsum) return QueryKth(ls(r1),ls(r2),l,mid,k); // k<=lsum,在左边找
                else         return QueryKth(rs(r1),rs(r2),mid+1,r,k-lsum); // 否则在右边找
            }
        }T;
    
        int n,q,a[N];
        void Input()
        {
            Rd(2,&n,&q);
            F(i,1,n) a[i]=I();
        }
    
        int d[N];
        void Soviet()
        {
            F(i,1,n) d[i]=a[i];
            sort(d+1,d+n+1);
            F(i,1,n) a[i]=lower_bound(d+1,d+n+1,a[i])-d;
    
            T.Init();
            T.rt[0]=T.Build(1,n);
            F(i,1,n)
            {
                T.rt[i]=T.Insert(a[i],T.rt[i-1]);
                // 第 i 颗线段树的根,就是在第 i-1 颗线段树的基础上,在 a[i] 的位置上加了一
            }
            F(i,1,q)
            {
                int l=I(),r=I(),k=I();
                int pos=T.QueryKth(T.rt[l-1],T.rt[r],1,n,k);
                // 查询第 r 颗树减去第 l-1 颗树的第 k 位,就相当于 a[l...r] 中的第 k 位
                printf("%d
    ",d[pos]);
            }
        }
    
        #define Flan void
        Flan IsMyWife()
        {
            Input();
            Soviet();
        }
    }
    int main()
    {
        Flandre_Scarlet::IsMyWife();
        getchar();getchar();
        return 0;
    }
    

    动态的问题

    板子见 洛谷 2617

    做好准备,这比静态的问题要困难的多。我当时调试了 (10) 个小时才过去。

    (后期:纯粹是我傻,抱歉)

    前置芝士: 树状数组

    不会这个,学个锤子主席树。

    为了方便说明,下面设 (nex(i)=i+operatorname{lowbit}(i))(pre(i)=i-operatorname{lowbit}(i))

    故 技 重 施

    动态的问题就是我们要支持对其中一个位置进行修改了。修改 了第 (i) 个位置后,([i,n]) 范围内的所有线段树都要被修改一次,时间复杂度就爆炸了。

    等等,这个问题我们是不是见过一次?

    把时间倒回大约一两年前,我们遇到过这样一个问题:“单点修改,求区间和”。当时,懵懂的我们也在想:求区间和就要维护前缀和;当时改了一个点,([i,n]) 范围内的所有前缀和都要被修改一次,时间复杂度就爆炸了。

    (对比一下这两句话)

    我们当时是用的树状数组解决的问题:树状数组 (T),第 (i) 个位置 (T_i) 维护 (pre(i)+1)(i) 的和。要查询 (i) 的前缀和,只要求 $T_i,T_{pre(i)},T_{pre(pre(i))}cdots $ 的和即可。

    那么我们可以用树状数组的思路维护主席树啊!

    总体思路

    那么我们怎么写呢?我们写一个“树状数组套主席树”:维护 (n) 个线段树 (T),其中 (T_i) 维护 ([pre(i)+1,i]) 之间的所有 (a_{x}) 组成的权值线段树即可。

    实际上这个引号可以去掉,这个就是树状数组套主席树

    初始化

    对于初始化操作,我们建树。先开 (n) 颗线段树,但是初始的时候 (n) 颗线段树 全部 和初始的线段树共用 (也就是对于每个 (i)(root[i]=root[0])

    然后对于第 (i) 个位置,它只能影响到 $i,nex(i),nex(nex(i))cdots $ 位置上的线段树。对于这些位置上的每一个线段树 (T_i),我们在它 自己 的基础上,插入 (a[i]) 位置(并加一)。

    修改操作

    对于修改第 (x) 个位置从原来的 (a[x]) 变成 (y) (修改完令 (a[x]=y)),我们要找到所有包含它的线段树,(T),在第 (a[x]) 个位置 (-1),在 (y) 的位置 (+1),这样就完成了修改操作。

    查询操作

    静态的问题中,查询 ([l,r]) 的第 (k) 大,用 (T_r) 来减掉 (T_{l-1}),然后判断答案在左边还是在右边。传参数只要传入 (T_{l-1})(T_r) 的根节点编号即可。

    然而我们现在是树状数组套线段树,两者相减,可不是两颗线段树相减了,而是

    (r,pre(r),pre(pre(r))cdots) 颗线段树的和,减去

    (l-1,pre(l-1),pre(pre(l-1)) cdots) 颗线段树的和。

    然后我们显然没法一次性传这么多参数进去(而且还是不定长度,更麻烦了)。我们的办法是,在查询之前,先把所有的 (l-1,pre(l-1),pre(pre(l-1)) cdots) 保存在一个数组 (R_1) 里,再把所有的 (r,pre(r),pre(pre(r)) cdots) 保存在一个数组 (R_2) 里(并记下这两个数组的长度)(从 (1) 开始编号的同学可以考虑用第 (0) 个位置作为长度,我就是这么写的)

    每次查询前缀 (r) 颗树减去前缀 (l-1) 颗树的时候,就遍历 (R_1,R_2),把第 (R_{2i}) 颗树的对应位置都加起来,把第 (R_{1i}) 颗树的对应位置都减掉,得到 (lsum)。然后用静态里面的做法即可。

    代码
    点击展开/折叠
    #include <bits/stdc++.h>
    using namespace std;
    namespace Flandre_Scarlet
    {
    	#define N 100005
    	#define F(i,l,r) for(int i=l;i<=r;++i)
    	#define D(i,r,l) for(int i=r;i>=l;--i)
    	#define Fs(i,l,r,c) for(int i=l;i<=r;c)
    	#define Ds(i,r,l,c) for(int i=r;i>=l;c)
    	#define MEM(x,a) memset(x,a,sizeof(x))
    	#define FK(x) MEM(x,0)
    	#define Tra(i,u) for(int i=G.Start(u),v=G.To(i);~i;i=G.Next(i),v=G.To(i))
    	#define p_b push_back
    	#define sz(a) ((int)a.size())
    	#define iter(a,p) (a.begin()+p)
    	int I()
    	{
    	    int x=0;char c=getchar();int f=1;
    	    while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar();
    	    while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
    	    return (x=(f==1)?x:-x);
    	}
    	void Rd(int cnt,...)
    	{
    	    va_list args; va_start(args,cnt);
    	    F(i,1,cnt) {int* x=va_arg(args,int*);(*x)=I();}
    	    va_end(args);
    	}
    
    	class Persistant_tree // 被卡常了的主席树...
    	{
    	public:
    		struct node{int lid,rid,s;} t[N*400];
    		// 去掉了存储左右区间的成员变量,左右区间在函数调用过程中记录
    		// 从而把空间优化成原来的 3/5,然后数组开大点
    		int rt[N],tot;
    		#define ls(x) t[x].lid
    		#define rs(x) t[x].rid
    		#define S     t[rt].s
    		void Init() {tot=0;}
    		int Build(int l,int r)
    		{
    			int rt=++tot; S=0;
    			if (l<r)
    			{
    				int mid=(l+r)>>1;
    				ls(rt)=Build(l,mid);
    				rs(rt)=Build(mid+1,r);
    			}
    			return rt;
    		} // 这个一样的
    		int Insert(int l,int r,int pos,int type,int rt)
    		// 修改操作可能会有 -1,于是加上一个 type,type=1或-1表示加还是减
    		{
    			int rr=++tot;
    			ls(rr)=ls(rt); rs(rr)=rs(rt); t[rr].s=S+type;
    			if (l<r)
    			{
    				int mid=(l+r)>>1;
    				if (pos<=mid) ls(rr)=Insert(l,mid,pos,type,ls(rt));
    				else 		  rs(rr)=Insert(mid+1,r,pos,type,rs(rt));
    				// 这些一样的
    			}
    			return rr;
    		}
    		int R2[30],R1[30];
    		// R2,R1 含义见上面
    		// 数组第 0 个位置保存数组长度
    		int QueryKth(int l,int r,int k)
    		{
    			if (l==r) return l;
    	
    			int lsum=0;
    			F(i,1,R2[0]) lsum+=t[ls(R2[i])].s;
    			F(i,1,R1[0]) lsum-=t[ls(R1[i])].s;
    			// 这个就是要遍历累加来求出 lsum 的值了
    			int mid=(l+r)>>1;
    			if (k<=lsum) 
    			{
    				F(i,1,R2[0]) R2[i]=ls(R2[i]);
    				F(i,1,R1[0]) R1[i]=ls(R1[i]);
    				// 跳左右儿子也要一块跳...这个复杂度活生生的多一个 log 啊
    				return QueryKth(l,mid,k);
    			}
    			else
    			{
    				F(i,1,R2[0]) R2[i]=rs(R2[i]);
    				F(i,1,R1[0]) R1[i]=rs(R1[i]);
    				return QueryKth(mid+1,r,k-lsum);
    			}
    		}
    	}T;
    	
    	int n,m,a[N];
    	struct node{char type; int a,b,c;} q[N];
    	// 如果是 C 操作,那么我们只用 a,b,表示将 a 位置变成了 b
    	// 如果是 Q 操作,那么我们 a,b,c 都用,表示 [a,b] 区间排名第 c 位的数
    	void Input()
    	{
    		Rd(2,&n,&m);
    		F(i,1,n) a[i]=I();
    		F(i,1,m)
    		{
    			char o[3]; scanf("%s",o);
    			if (o[0]=='Q') 
    			{
    				q[i]=(node){o[0],I(),I(),I()};
    			}
    			else
    			{
    				q[i]=(node){o[0],I(),I()};
    			}
    		}
    	}
    	
    	int d[N<<2],dcnt=0;
    	#define Find(x) (lower_bound(d+1,d+dcnt+1,x)-d)
    	void Insert(int pos,int type)
    	{
    		int x=Find(a[pos]);
    		for(int i=pos;i<=n;i+=(i&(-i))) 
    		{
    			int lastr=T.rt[i]; // 保存原来的根
    			T.rt[i]=T.Insert(1,dcnt,x,type,lastr); // 以便在原来的基础上插入
    		}
    	}
    	int Query(int l,int r,int k)
    	{
    		F(i,0,22) T.R1[i]=T.R2[i]=0;
    		for(int i=r;i>0;i-=(i&(-i))) T.R2[++T.R2[0]]=T.rt[i];
    		for(int i=l;i>0;i-=(i&(-i))) T.R1[++T.R1[0]]=T.rt[i];
    		// 先预先求好 R1,R2
    		return T.QueryKth(1,dcnt,k);
    	}
    	void Soviet()
    	{
    		F(i,1,n) d[++dcnt]=a[i];
    		F(i,1,m) if (q[i].type=='C') d[++dcnt]=q[i].b;
    		sort(d+1,d+dcnt+1);
    	
    		T.Init();
    		T.rt[0]=T.Build(1,dcnt); F(i,1,n) T.rt[i]=1;
    		F(i,1,n) Insert(i,1);
    		F(i,1,m)
    		{
    			if (q[i].type=='Q')
    			{
    				int pos=Query(q[i].a-1,q[i].b,q[i].c);
    				printf("%d
    ",d[pos]);
    			}
    			if (q[i].type=='C')
    			{
    				int x=q[i].a,y=q[i].b;
    				Insert(x,-1); a[x]=y;
    				Insert(x,1);
    			}
    		}
    	}
    	
    	#define Flan void
    	Flan IsMyWife()
    	{
    		Input();
    		Soviet();
    	}
    }
    int main()
    {
    	Flandre_Scarlet::IsMyWife();
    	getchar();getchar();
    	return 0;
    }
    
  • 相关阅读:
    mvc原理和mvc模式的优缺点
    Paxos算法详细图解
    环境搭建
    elasticsearch 安装
    redis rdb文件解析
    eclipse 远程调试
    java 解析xml
    理想化 redis
    redis 内存
    工作圈redis 使用
  • 原文地址:https://www.cnblogs.com/LightningUZ/p/14730545.html
Copyright © 2011-2022 走看看