主席树 笔记

博客观赏效果更佳

对于现在的我来说,只要有“可持久化”这个标签,一定伴随着 “毒瘤“,毕竟我刚学会…

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

但是,可持久化是啥呢?

可持久化是啥

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

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

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

原作者

原博客

(侵权删)(私信 3348064478@qq.com)

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

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

经典题型

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

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

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

静态的问题

板子见 洛谷 3834

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

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

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

考虑用上面“可持久化”的办法来优化求 TT 的过程:TiT_iTi1T_{i-1}之间,差的只是在(离散化后的) aia_i 位置上加一。那么我们就让 TiT_iTi1T_{i-1} 的基础上,复制其中的 log\log 条链即可。这样就可以空间复杂度 O(nlogn)O(n\log n) ,时间复杂度 O(nlog2n)O(n\log^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\n",d[pos]);
        }
    }

    #define Flan void
    Flan IsMyWife()
    {
        Input();
        Soviet();
    }
}
int main()
{
    Flandre_Scarlet::IsMyWife();
    getchar();getchar();
    return 0;
}

动态的问题

板子见 洛谷 2617

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

前置芝士: 树状数组

(什么?你不会这个还来学主席树?)
(算了,还是先复习一下…)

首先回顾一下树状数组的概念:

对于第 ii 个位置,我们维护从 ii 开始往前 lowbit(i)\operatorname{lowbit}(i) 个数的和。然后每次修改只需要改 log\log 个位置,

查询也只要求 log\log 个位置的和。

为了方便说明,下面设 nex(i)=i+lowbit(i)nex(i)=i+\operatorname{lowbit}(i)pre(i)=ilowbit(i)pre(i)=i-\operatorname{lowbit}(i)
(才不是因为我懒得打 lowbit\operatorname{lowbit} 呢!哼╭(╯^╰)╮)

故 技 重 施

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

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

把时间倒回大约一两年前,我们遇到过这样一个问题:“单点修改,求区间和”

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

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

总体思路

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

初始化

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

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

修改操作

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

查询操作

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

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

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

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

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

每次查询前缀 rr 颗树减去前缀 l1l-1 颗树的时候,就遍历 R1,R2R_1,R_2,把第 R2iR_{2i} 颗树的对应位置都加起来,把第 R1iR_{1i} 颗树的对应位置都减掉,得到 lsumlsum。然后用静态里面的做法即可。

代码

{% fold %}

#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\n",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;
}

{% endfold %}

发布了250 篇原创文章 · 获赞 12 · 访问量 1万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览