可持久化 & 学习笔记

可持久化是一种重要的思想,具体怎么重要?

什么是可持久化

通俗点讲就是对于一种数据结构,我们保留它在每个历史版本。
也就是说,对于一个可持久化的数据结构,你不但可以进行正常的查询、修改操作,还可以查询它的某个历史时刻的值,或是在某个历史时刻对其进行修改。
听上去非常厉害,但是我知道它的历史版本有什么用?
以我现在的想象力好像也只能解决一些特意为可持久化出的板子题。
但真正学了之后发现并不是这样,它可以通过记录数据结构的不同版本来实现一些之前做不到的事情。

主席树

介绍

主席树的全称是「可持久化权值线段树」。

  • 为什么叫主席树?

这就要提到主席树的提出者——黄嘉泰。
由于黄嘉泰名字的字母首拼(hjt)与当时(2010)中华人民共和国的主席一样,所以就叫主席树了。
所以主席树并不是跟主席没有关系,但是主席树里确实没有主席。

作用

  • 那么主席树到底是干什么的?

看下面一个问题:

对于可重数列 \(a_1,a_2,a_3,a_4,\cdots a_n\) 对于每一次询问回答 \(a_i\) 的值有多少个?或者 排名为 \(k\) 的值?

大家脑子里一定会想到平衡树权值线段树,当然这里肯定是选权值线段树,我觉得平衡树码量太大。

那如果我将问题换一下:

对于可重数列 \(a_1,a_2,a_3,a_4,\cdots a_n\) 对于每一次询问回答在区间 \([l,r]\)\(a_i\) 出现了多少次?

挠挠头好像并不能想到非暴力的做法,你想用权值线段树但这并不是全部区间,你只能对于每次询问都重新建一棵树,然后时间复杂度直接飞起来。

如果对每一次询问都建一棵新树,那么为什么不考虑考虑直接建一堆树呢?
想一下如果我对 \([1,l-1]\) 建了一颗权值线段树,然后又对 \([1,r]\) 建了一颗权值线段树,我分别在两棵树上去查 \(a_i\) 的个数,我们就会得到区间 \([1,l-1]\)\(a_i\) 的个数 \(ans_{l-1}\)、以及区间 \([1,r]\)\(a_i\) 的个数 \(ans_r\),然后即就会发现我们要的答案正是 \(ans_r-ans_{l-1}\),这借助的是差分的思想。
再往大了想,我们对于每一个区间 \([1,i],i\in[1,n]\) 建立一颗权值线段树,利用上述差分的思想,我们可以快速的求出区间 \(a_i\) 个数的问题。类似地,我们也可以解决区间排名问题。

以上就是主席树的基本思想,即通过差分维护区间问题。

实现

你发现当我们对于每一个区间 \([1,i],i\in[1,n]\) 都建一颗权值线段树,时间复杂度成功降到了 \(log\) 级别,但一看空间……不忍直视
其实我们并没有必要对于每一个区间都完整地建一棵树。
下面我们考虑对于维护区间 \([1,i-1]\)\([1,i]\) 的关系,为了方便,下面简称第 \(i-1\) 与第 \(i\) 颗树。
你发现,这两棵树有相当一部分是一样的,于是就可以直接将相同部分合并,这样空间就下去了。

事实上,我们并不是执行合并操作,而是在插入时,传入两个节点,一个是第 \(i\) 颗树的根,另一个是第 \(i+1\) 颗树的根,先让 \(i+1\) 的节点赋成 \(i\) 的值,若要向左走,就让 \(i+1\) 的左子节点指向新建的节点,向右同理,这就是可持久化的具体实现。

这样就可以保证,每次插入一个新节点时,只新建了一条链,这样空间问题就解决了。

上述就是主席树插入的实现过程。

下面就要考虑查询了。
\(l-1\)\(r\) 这两颗树上直接查不就行了吗?
若对于查区间某个值的个数,这种方法当然可以,那要是查区间某个排名的值呢?
好像这种方法就不适用了,原因是你并不知道左右子树在要查询区间的元素个数,也就不知道要往左找还是往右找。
那怎么办呢?
你看我们插入的时候传了两个根,同样的思想为什么不用在查询上?
我们将 \(l-1\)\(r\) 同时传入查询函数,就可以直接差分出当前节点的左子节点在某个区间中元素的个数,这样按照权值线段树的基本操作向左查查向右查查就可以找到答案了。

以上就是主席树的基本操作,发现可持久化还是非常有用的。

例题

【模板】可持久化线段树 1(可持久化数组)\(^{luoguP3919}\)

这是可持久化线段树的题,可以帮助理解可持久化操作。

  • 代码
#include<bits/stdc++.h>
using namespace std;
const int NUM=5e7+10;
#define lid T[id].l
#define rid T[id].r

struct node{
	int l,r;
	int val;
}T[NUM];
int tot,n,m;
int A[NUM];
int root[NUM],totr;
void pushup(int id){
	T[id].val=T[lid].val+T[rid].val;
}
void build(int &id,int l,int r){
	if(!id) id=++tot;
	if(l==r){
		T[id].val=A[l];
		return;
	}
	int mid=(l+r)>>1;
	build(lid,l,mid);
	build(rid,mid+1,r);
	pushup(id);
}
void update(int oid,int &nid,int l,int r,int pos,int val){
	if(!nid) nid=++tot;
	T[nid]=T[oid];
	if(l==r){
		T[nid].val=val;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid) update(T[oid].l,T[nid].l=++tot,l,mid,pos,val);
	else update(T[oid].r,T[nid].r=++tot,mid+1,r,pos,val);
	pushup(nid); 
}
int query(int id,int l,int r,int pos){
	if(!id) return 0;
	if(l==r){
		return T[id].val;
	}
	int mid=(l+r)>>1;
	if(pos<=mid) return query(lid,l,mid,pos);
	else return query(rid,mid+1,r,pos);
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i){
		cin>>A[i];
	}
	build(root[totr],1,n);
	for(int i=1,op,x,y,z;i<=m;++i){
		cin>>x>>op;totr++;
		if(op==1){
			cin>>y>>z;
			update(root[x],root[totr],1,n,y,z);
		}else{
			cin>>y;
			root[totr]=root[x];
			cout<<query(root[x],1,n,y)<<'\n';
		}
	}
	return 0;
}

【模板】可持久化线段树 2\(^{luoguP3834}\)

这道题是一道静态查询区间第 \(k\) 大的板题,用上述方法就可以过去。
注意题目给了你 \(1GB\) 就不要吝啬开数组。

  • 代码
#include<bits/stdc++.h>
using namespace std;
const int NUM=5e7+10;
#define lid T[id].l
#define rid T[id].r

struct tree{
	int l,r;
	int cnt;
}T[NUM];
int root[NUM],tot;
void pushup(int id){
	T[id].cnt=T[lid].cnt+T[rid].cnt;
}
void update(int oid,int &nid,int l,int r,int pos,int val){
	if(!nid) nid=++tot;
	T[nid]=T[oid];
	if(l==r){
		T[nid].cnt+=val;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid) update(T[oid].l,T[nid].l=++tot,l,mid,pos,val);
	else update(T[oid].r,T[nid].r=++tot,mid+1,r,pos,val);
	pushup(nid); 
}
int query(int oid,int nid,int l,int r,int k){
	if(l==r) return l;
	int mid=(l+r)>>1,x=T[T[nid].l].cnt-T[T[oid].l].cnt;
	if(k<=x) return query(T[oid].l,T[nid].l,l,mid,k);
	else return query(T[oid].r,T[nid].r,mid+1,r,k-x);
}
int n,m;
int A[NUM]; 

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;++i){
		cin>>A[i];
		update(root[i-1],root[i],0,1e9,A[i],1);
	}
	for(int i=1,x,y,z;i<=m;++i){
		cin>>x>>y>>z;
		cout<<query(root[x-1],root[y],0,1e9,z)<<'\n';
	}
	return 0;
}

Count on a tree\(^{luoguP2633}\)

也是区间查询第 \(k\) 大的题,唯一不同的是将操作换到了树上,于是我们采用树上差分的思想,即:

\[cnt_x+cnt_y-cnt_{lca}-cnt_{fa_{lca}} \]

来作为对比排名的依据,插入的时候也不是按顺序插入,而是按照 \(fa \rightarrow son\) 的遍历顺序,即要在一个节点的父亲版本插入这个值,这
样才能保证差分是对的。

  • 代码
#include<bits/stdc++.h>
using namespace std;
const int NUM=5e6+10;
const int NUMM=2e5+10;
#define lid T[id].l
#define rid T[id].r

struct tree{
	int l,r;
	int cnt;
}T[NUM];
int root[NUMM],tot;
void pushup(int id){
	T[id].cnt=T[lid].cnt+T[rid].cnt;
}
void update(int oid,int &nid,int l,int r,int pos,int val){
	if(!nid) nid=++tot;
	T[nid]=T[oid];
	if(l==r){
		T[nid].cnt+=val;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid) update(T[oid].l,T[nid].l=++tot,l,mid,pos,val);
	else update(T[oid].r,T[nid].r=++tot,mid+1,r,pos,val);
	pushup(nid); 
}
int query(int x,int y,int lca,int fa,int l,int r,int k){
	if(l==r) return l;
	int lx=T[T[x].l].cnt+T[T[y].l].cnt-T[T[lca].l].cnt-T[T[fa].l].cnt;
	int mid=(l+r)>>1;
	if(lx>=k) return query(T[x].l,T[y].l,T[lca].l,T[fa].l,l,mid,k);
	else return query(T[x].r,T[y].r,T[lca].r,T[fa].r,mid+1,r,k-lx);
} 
int n,m;
long long A[NUMM],B[NUMM];
int h[NUMM],nxt[NUMM],to[NUMM],tote;
int fa[NUMM][26],dep[NUMM];
long long last;
void addedge(int x,int y){
	to[++tote]=y;
	nxt[tote]=h[x];
	h[x]=tote;
}
int len;
void dfs(int x,int f){
	dep[x]=dep[f]+1;
	fa[x][0]=f;
	for(int i=1;i<=25;++i){
		fa[x][i]=fa[fa[x][i-1]][i-1];
	}
	update(root[f],root[x],1,n,A[x],1);
	for(int i=h[x];i;i=nxt[i]){
		int y=to[i];
		if(y!=f) dfs(y,x);
	}
}
int lca(int x,int y){
	if(dep[x]<=dep[y]) swap(x,y);
	for(int i=25;i>=0;--i){
		if(dep[fa[x][i]]>=dep[y]) x=fa[x][i];
	}
	if(x==y) return x;
	for(int i=25;i>=0;--i){
		if(fa[x][i]!=fa[y][i]){
			x=fa[x][i];
			y=fa[y][i];
		}
	}
	return fa[x][0];
}

int lb(int x){
	return lower_bound(B+1,B+1+len,x)-B;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0); 
	cin>>n>>m;
	for(int i=1;i<=n;++i){
		cin>>A[i];B[i]=A[i];
	}
	sort(B+1,B+1+n);
	len=unique(B+1,B+1+n)-B-1;
	for(int i=1;i<=n;++i){
		A[i]=lb(A[i]);
	}
	for(int i=1,x,y;i<n;++i){
		cin>>x>>y;
		addedge(x,y);
		addedge(y,x);
	}
	dfs(1,0);
	for(int i=1,x,y,z;i<=m;++i){
		cin>>x>>y>>z;
		x^=last;
		int lc=lca(x,y);
		cout<<(last=B[query(root[x],root[y],root[lc],root[fa[lc][0]],1,n,z)])<<'\n';
	}
	return 0;
}

[POI2014] KUR-Couriers\(^{luoguP3567}\)

题目.zip:

  • 给一个数列,每次询问一个区间内有没有一个数出现次数超过一半。

如果这个区间中有一个数的数量超过了一半 \(cnt_{a_i} \ge(r-l+1)/2\)(其中 \(cnt\) 表示线段树上所维护的区间元素数量) 那么当这个数对应的父节点在pushup操作时,其父节点的 \(cnt\) 也一定大于 \((r-l+1)/2\) 那么我们在查询的时候,向子节点的 \(cnt\ge(r-l+1)/2\)的节点去找即可。

#include<bits/stdc++.h>
using namespace std;
const int NUM=5e7+10;
#define lid T[id].l
#define rid T[id].r

struct tree{
	int l,r;
	int cnt;
}T[NUM];
int root[NUM],tot;
void pushup(int id){
	T[id].cnt=T[lid].cnt+T[rid].cnt;
}
void update(int oid,int &nid,int l,int r,int pos,int val){
	if(!nid) nid=++tot;
	T[nid]=T[oid];
	if(l==r){
		T[nid].cnt+=val;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid) update(T[oid].l,T[nid].l=++tot,l,mid,pos,val);
	else update(T[oid].r,T[nid].r=++tot,mid+1,r,pos,val);
	pushup(nid); 
}
int query(int oid,int nid,int l,int r,int len){
	if(l==r) return l;
	int mid=(l+r)>>1;
	if((T[T[nid].l].cnt-T[T[oid].l].cnt)*2>len)
		return query(T[oid].l,T[nid].l,l,mid,len);
	else if((T[T[nid].r].cnt-T[T[oid].r].cnt)*2>len)
		return query(T[oid].r,T[nid].r,mid+1,r,len);
	else return 0;
}
int n,m;
int A[NUM];
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i){
		cin>>A[i];
		update(root[i-1],root[i],0,1e9,A[i],1);
	}
	for(int i=1,x,y;i<=m;++i){
		cin>>x>>y;
		cout<<query(root[x-1],root[y],0,1e9,y-x+1)<<'\n';
	}
	return 0;
}

[国家集训队] middle\(^{luoguP2839}\)

题目给你左右端点的范围,让你求最大的中位数。
中位数的范围不就在 \([\frac{a+c}{2},\frac{b+d}{2}]\) 之间吗?
如果你跟我一样也是这么想的,恭喜你错了,因为原数列是乱序的,二中位数需要排序,所以你并不知道中位数在哪。
于是我们考虑二分答案
我们先建立一颗完整的权值线段树,每一个点的权值都为 \(1\),然后按从小到大的顺序依次修改其权值为 \(-1\)
也就是说第 \(k\) 个版本的权值线段树,其权值大于第 \(k\) 个值的都为 \(1\),小的都为 \(-1\)
当二分出来一个 \(mid\) 就只需要检查其对应的 第 \(mid\) 个版本的线段树所对应的区间和是否大于 \(0\),若大于 \(0\) 则就说明答案还可以更大,继续向右找。
然后就是怎么查所对应的区间,其所对应的区间是左端点在 \([a,b]\),右端点在 \([c,d]\) 的区间最大值。
我们可以在线段树上维护三个信息:

  • 区间和 \(sum\)
  • 区间前缀最大和 \(pre\)
  • 区间后缀最大和 \(nxt\)

这样我们就可以解决上述问题,通过查询 \([a,b]\) 的最大后缀,\([b,c]\) 的区间和,\([c,d]\) 的最大前缀,再加起来就是要的最大值,拿这个值和 \(0\) 比较二分即可。

#include<bits/stdc++.h>
using namespace std;
const int NUM=5e7+10;
const int NUMM=4e4+10;
const int inf=0x3f3f3f3f;
#define lid T[id].l
#define rid T[id].r

struct node{
	int l,r;
	int pre,sum,nxt;
}T[NUM];
int root[NUM],tot;
void pushup(int id){
	T[id].sum=T[lid].sum+T[rid].sum;
	T[id].pre=max(T[lid].pre,T[rid].pre+T[lid].sum);
	T[id].nxt=max(T[rid].nxt,T[lid].nxt+T[rid].sum);
}
void build(int &id,int l,int r){
	if(!id) id=++tot;
	if(l==r){
		T[id].pre=T[id].nxt=T[id].sum=1;
		return;
	}
	int mid=(l+r)>>1;
	build(lid,l,mid),build(rid,mid+1,r);
	pushup(id);
}
void update(int oid,int &nid,int l,int r,int pos,int val){
	if(!nid) nid=++tot;
	T[nid]=T[oid];
	if(l==r){
		T[nid].pre=T[nid].nxt=T[nid].sum=val;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid) update(T[oid].l,T[nid].l=++tot,l,mid,pos,val);
	else update(T[oid].r,T[nid].r=++tot,mid+1,r,pos,val);
	pushup(nid);
}
int query_sum(int id,int l,int r,int L,int R){
	if(!id) return 0;
	if(L<=l&&r<=R){
		return T[id].sum;
	}
	int mid=(l+r)>>1,ans=0;
	if(L<=mid) ans+=query_sum(lid,l,mid,L,R);
	if(R>mid) ans+=query_sum(rid,mid+1,r,L,R);
	return ans;
}
int query_pre(int id,int l,int r,int L,int R){
	if(!id) return -inf;
	if(L<=l&&r<=R){
		return T[id].pre;
	}
	int mid=(l+r)>>1;
	if(R<=mid) return query_pre(lid,l,mid,L,R);
	else if(L>mid) return query_pre(rid,mid+1,r,L,R);
	else
		return max(query_pre(lid,l,mid,L,mid),query_pre(rid,mid+1,r,mid+1,R)+query_sum(lid,l,mid,L,mid));
}
int query_nxt(int id,int l,int r,int L,int R){
	if(!id) return inf;
	if(L<=l&&r<=R){
		return T[id].nxt;
	}
	int mid=(l+r)>>1;
	if(R<=mid) return query_nxt(lid,l,mid,L,R);
	else if(L>mid) return query_nxt(rid,mid+1,r,L,R);
	else
		return max(query_nxt(lid,l,mid,L,mid)+query_sum(rid,mid+1,r,mid+1,R),query_nxt(rid,mid+1,r,mid+1,R));
} 
int n,m;
int last;
int q[5];
struct tmp{
	int val,id;
}A[NUM];

bool check(int x){
	int sum=0;
	if(q[3]-1>=q[2]+1) sum+=query_sum(root[x-1],1,n,q[2]+1,q[3]-1);
	sum+=query_nxt(root[x-1],1,n,q[1],q[2]);
	sum+=query_pre(root[x-1],1,n,q[3],q[4]);
	return sum>=0;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i){
		cin>>A[i].val;A[i].id=i;
	}
	sort(A+1,A+1+n,[](tmp _1,tmp _2){return _1.val<_2.val;});
	build(root[0],1,n);
	for(int i=1;i<=n;++i){
		update(root[i-1],root[i],1,n,A[i].id,-1);
	}
	cin>>m;
	for(int i=1;i<=m;++i){
		for(int i=1;i<=4;++i){
			cin>>q[i];q[i]=(q[i]+last)%n+1;
		}
		sort(q+1,q+5);
		int l=1,r=n,ans=l;
		while(l<=r){
			int mid=(l+r)>>1;
			if(check(mid)) l=mid+1,ans=mid;
			else r=mid-1;
		}
		cout<<(last=A[ans].val)<<'\n';
	}
	return 0;
}
posted @ 2026-07-22 22:08  LZYXT  阅读(10)  评论(0)    收藏  举报