对于现在的我来说,只要有“可持久化”这个标签,一定伴随着 “毒瘤“,毕竟我刚学会…
好的首先介绍一下主席树是啥。主席树是“可持久化线段树”(Persistant Segment Tree
)的中文民间俗称。不知道是因为有人把 Persistant
看成了 Presidant
,还是因为它的发明者是 HJT
(和某一任国家主席简称相同),被叫做“主席树”。
但是,可持久化是啥呢?
可持久化是啥
可持久化是亿点小优化,当你要开 n n n 个线段树,但是线段树之间只差一次修改的时候,把空间复杂度降到 O ( n log n ) O(n\log n) O(nlogn)。
如何优化呢?考虑我们现在要对一个单点进行修改,那么我们能影响到的位置(就是这个点以及它的祖先),一共才 log n \log n logn ,个而其它位置都一样,所以我们可以 只新建 log n \log n logn 个位置,整一个副本出来,其余位置直接继承原树即可。
蒯一张图举个例子, n = 5 n=5 n=5,修改了第四个点:
(原作者)
(原博客)
(侵权删)(私信 3348064478@qq.com)
这样就能节省下超级多的空间!
但是,有利总有弊。这样的结构破坏的原来线段树的编号的完美特性,不能再用 index<<1
和 index<<1|1
来访问左右儿子了。我们要在线段树中多维护两个元素,左儿子和右儿子的编号。
经典题型
静态/动态区间第 k k k 位:给定一个序列,每次给出 l , r , k l,r,k l,r,k,求 [ l , r ] [l,r] [l,r] 中的数排序后,第 k k k 个位置的数是谁。(当然,我们不会实际去排序,所以询问操作对原序列没有影响)
如果是动态的,那你还要支持单点修改的操作,会给定 x , y x,y x,y,要支持把第 x x x 个位置上的数改成 y y y。
(假设我们离散化过了所有 a i a_i ai,和所有询问修改后变成的 y y y)
静态的问题
板子见 洛谷 3834
对于一个查询 ( l , r , k ) (l,r,k) (l,r,k) ,(假设我们能)把 a [ l ⋯ r ] a[l\cdots r] a[l⋯r] 拿出来建一颗权值线段树 T T T,查询就很容易了:把区间分成左半部分和右半部分,设左半部分中数字的个数为 l s u m lsum lsum。如果 k ≤ l s u m k\le lsum k≤lsum,那么就在左半部分中查找,否则就在右半部分中查找第 k − l s u m k-lsum k−lsum 个。
但是我们怎么把 a [ l ⋯ r ] a[l\cdots r] a[l⋯r] 拿出来建一颗权值线段树呢?这个时空复杂度都是 O ( n l o g n ) O(nlogn) O(nlogn) 每次,再乘一个询问次数,肯定炸。
但是我们发现,线段树满足一种微妙的“可减性”:我们考虑建 n n n 颗线段树 T T T, T i T_i Ti 表示 a [ 1 ⋯ i ] a[1\cdots i] a[1⋯i] 组成的权值线段树。然后 a [ l ⋯ r ] a[l\cdots r] a[l⋯r] 组成的权值线段树的对应位置就是 T r T_r Tr 的对应位置减去 T l − 1 T_{l-1} Tl−1的对应位置。但是把 T T T 建出来,光空间就够炸的了,是 O ( n 2 ) O(n^2) O(n2) 的
考虑用上面“可持久化”的办法来优化求 T T T 的过程: T i T_i Ti 和 T i − 1 T_{i-1} Ti−1之间,差的只是在(离散化后的) a i a_i ai 位置上加一。那么我们就让 T i T_i Ti 在 T i − 1 T_{i-1} Ti−1 的基础上,复制其中的 log \log log 条链即可。这样就可以空间复杂度 O ( n log n ) O(n\log n) O(nlogn) ,时间复杂度 O ( n log 2 n ) O(n\log^2 n) O(nlog2n) 的过去了。
一个伏笔
请把它理解成“前缀和套线段树”。
那么恭喜您,现在您已经会了一个嵌套型的数据结构了
关于为什么要把它理解成这样…看到后面就知道啦,现在保密哦,不能告诉你(ฅ>ω<*ฅ)
静态问题(板子)的代码
#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
做好准备,这比静态的问题要困难的多。我当时调试了 10 10 10 个小时才过去。
前置芝士: 树状数组
(什么?你不会这个还来学主席树?)
(算了,还是先复习一下…)
首先回顾一下树状数组的概念:
对于第 i i i 个位置,我们维护从 i i i 开始往前 lowbit ( i ) \operatorname{lowbit}(i) lowbit(i) 个数的和。然后每次修改只需要改 log \log log 个位置,
查询也只要求 log \log log 个位置的和。
为了方便说明,下面设
n
e
x
(
i
)
=
i
+
lowbit
(
i
)
nex(i)=i+\operatorname{lowbit}(i)
nex(i)=i+lowbit(i),
p
r
e
(
i
)
=
i
−
lowbit
(
i
)
pre(i)=i-\operatorname{lowbit}(i)
pre(i)=i−lowbit(i)
(才不是因为我懒得打
lowbit
\operatorname{lowbit}
lowbit 呢!哼╭(╯^╰)╮)
故 技 重 施
动态的问题就是我们要支持对其中一个位置进行修改了。修改 了第 i i i 个位置后, [ i , n ] [i,n] [i,n] 范围内的所有线段树都要被修改一次,时间复杂度就爆炸了。
等等,这个问题我们是不是见过一次?
把时间倒回大约一两年前,我们遇到过这样一个问题:“单点修改,求区间和”
我们当时是用的树状数组解决的问题:树状数组 T T T,第 i i i 个位置 T i T_i Ti 维护 p r e ( i ) + 1 pre(i)+1 pre(i)+1 到 i i i 的和。要查询 i i i 的前缀和,只要求 T i , T p r e ( i ) , T p r e ( p r e ( i ) ) ⋯ T_i,T_{pre(i)},T_{pre(pre(i))}\cdots Ti,Tpre(i),Tpre(pre(i))⋯ 的和即可。
那么我们可以用树状数组的思路维护主席树啊!
总体思路
那么我们怎么写呢?我们写一个“树状数组套线段树”:维护 n n n 个线段树 T T T,其中 T i T_i Ti 维护 [ p r e ( i ) + 1 , i ] [pre(i)+1,i] [pre(i)+1,i] 之间的所有 a x a_{x} ax 组成的权值线段树即可。
初始化
对于初始化操作,我们建树。先开 n n n 颗线段树,但是初始的时候 n n n 颗线段树 全部 和初始的线段树共用 (也就是对于每个 i i i, r o o t [ i ] = r o o t [ 0 ] root[i]=root[0] root[i]=root[0])
然后对于第 i i i 个位置,它只能影响到 i , n e x ( i ) , n e x ( n e x ( i ) ) ⋯ i,nex(i),nex(nex(i))\cdots i,nex(i),nex(nex(i))⋯ 位置上的线段树。对于这些位置上的每一个线段树 T i T_i Ti,我们在它 自己 的基础上,插入 a [ i ] a[i] a[i] 位置(并加一)。
修改操作
对于修改第 x x x 个位置从原来的 a [ x ] a[x] a[x] 变成 y y y (修改完令 a [ x ] = y a[x]=y a[x]=y),我们要找到所有包含它的线段树, T T T,在第 a [ x ] a[x] a[x] 个位置 − 1 -1 −1,在 y y y 的位置 + 1 +1 +1,这样就完成了修改操作。
查询操作
静态的问题中,查询 [ l , r ] [l,r] [l,r] 的第 k k k 大,用 T r T_r Tr 来减掉 T l − 1 T_{l-1} Tl−1,然后判断答案在左边还是在右边。传参数只要传入 T l − 1 T_{l-1} Tl−1 和 T r T_r Tr 的根节点编号即可。
然而我们现在是树状数组套线段树,两者相减,可不是两颗线段树相减了,而是
第 r , p r e ( r ) , p r e ( p r e ( r ) ) ⋯ r,pre(r),pre(pre(r))\cdots r,pre(r),pre(pre(r))⋯ 颗线段树的和,减去
第 l − 1 , p r e ( l − 1 ) , p r e ( p r e ( l − 1 ) ) ⋯ l-1,pre(l-1),pre(pre(l-1)) \cdots l−1,pre(l−1),pre(pre(l−1))⋯ 颗线段树的和。
然后我们显然没法一次性传这么多参数进去(而且还是不定长度,更麻烦了)。我们的办法是,在查询之前,先把所有的 l − 1 , p r e ( l − 1 ) , p r e ( p r e ( l − 1 ) ) ⋯ l-1,pre(l-1),pre(pre(l-1)) \cdots l−1,pre(l−1),pre(pre(l−1))⋯ 保存在一个数组 R 1 R_1 R1 里,再把所有的 r , p r e ( r ) , p r e ( p r e ( r ) ) ⋯ r,pre(r),pre(pre(r)) \cdots r,pre(r),pre(pre(r))⋯ 保存在一个数组 R 2 R_2 R2 里(并记下这两个数组的长度)(从 1 1 1 开始编号的同学可以考虑用第 0 0 0 个位置作为长度,我就是这么写的)
每次查询前缀 r r r 颗树减去前缀 l − 1 l-1 l−1 颗树的时候,就遍历 R 1 , R 2 R_1,R_2 R1,R2,把第 R 2 i R_{2i} R2i 颗树的对应位置都加起来,把第 R 1 i R_{1i} R1i 颗树的对应位置都减掉,得到 l s u m lsum lsum。然后用静态里面的做法即可。
代码
{% 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 %}