主席树 笔记
废话
首先介绍一下主席树与其名字的来历。主席树是“可持久化线段树”(Persistant Segment Tree
)的中文民间俗称。不知道是因为有人把 Persistant
看成了 Presidant
,还是因为它的发明者是 HJT
(和某一任国家主席简称相同),被叫做“主席树”。
但是,可持久化是啥呢?
可持久化是啥
可持久化是亿点小优化,当你要开 \(n\) 个线段树,但是线段树之间只差一次修改的时候,把空间复杂度降到 \(O(n\log n)\)。
如何优化呢?考虑我们现在要对一个单点进行修改,那么我们能影响到的位置(就是这个点以及它的祖先),一共才 \(\log n\) ,个而其它位置都一样,所以我们可以 只新建 \(\log n\) 个位置,整一个副本出来,其余位置直接继承原树即可。
蒯一张图举个例子,\(n=5\),修改了第四个点:
(原作者)
(原博客)
这样就能节省下超级多的空间!
但是,有利总有弊。这样的结构破坏的原来线段树的编号的完美特性,不能再用 index<<1
和 index<<1|1
来访问左右儿子了。我们要在线段树中多维护两个元素,左儿子和右儿子的编号。
经典题型
静态/动态区间第 \(k\) 位:给定一个序列,每次给出 \(l,r,k\),求 \([l,r]\) 中的数排序后,第 \(k\) 个位置的数是谁。(当然,我们不会实际去排序,所以询问操作对原序列没有影响)
如果是动态的,那你还要支持单点修改的操作,会给定 \(x,y\),要支持把第 \(x\) 个位置上的数改成 \(y\)。
(假设我们离散化过了所有 \(a_i\),和所有询问修改后变成的 \(y\))
静态的问题
板子见 洛谷 3834
对于一个查询 \((l,r,k)\) ,(假设我们能)把 \(a[l\cdots r]\) 拿出来建一颗权值线段树 \(T\),查询就很容易了:把区间分成左半部分和右半部分,设左半部分中数字的个数为 \(lsum\)。如果 \(k\le lsum\),那么就在左半部分中查找,否则就在右半部分中查找第 \(k-lsum\) 个。
但是我们怎么把 \(a[l\cdots r]\) 拿出来建一颗权值线段树呢?这个时空复杂度都是 \(O(nlogn)\) 每次,再乘一个询问次数,肯定炸。
但是我们发现,线段树满足一种微妙的“可减性”:我们考虑建 \(n\) 颗线段树 \(T\),\(T_i\) 表示 \(a[1\cdots i]\) 组成的权值线段树。然后 \(a[l\cdots 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(n\log n)\) ,时间复杂度 \(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
做好准备,这比静态的问题要困难的多。我当时调试了 \(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\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;
}