可持久化线段树

可持久化

可持久化数据结构 (Persistent data structure) 总是可以保留每一个历史版本,并且支持操作的不可变特性 (immutable).

可持久化线段树

如何储存线段树每个版本?显然不可能将每个版本暴力存储一遍。
我们只需要:将各个版本之间的不同之处分别存储,相同部分共用节点。
如图:

单点修改的线段树每次修改只会改变一条链,直接新建一条链,剩余的连上之前版本的节点。
区间修改的线段树类似,注意懒标记的可持久化。
这样,时空复杂度就是 \(O(n+mlogn)\) 的。

主席树

主席树全称是可持久化权值线段树。
最经典的操作是维护静态区间第 \(k\) 小。

静态区间第 \(k\)

考虑对序列前缀建立主席树,每个版本的权值线段树表示对应前缀的权值情况。
对于查询 \([L,R]\) 的第 \(k\) 小,可以通过查询 版本 \(L-1\)\(R\) 的线段树,通过前缀和相减得到 \([L,R]\) 的信息,再进行线段树二分即可。

下给出实现:
【模板】可持久化线段树 2

struct Segment_Tree{
	int lch[M],rch[M],sum[M],cnt;
	int newnode(int x){
		int id=++cnt;  sum[id]=sum[x];
		lch[id]=lch[x],rch[id]=rch[x];
		return id;
	}void add(int x,int &y,int id,int L,int R){
		if(L==R){ y=newnode(x),sum[y]++; return; }
		int mid=(L+R)>>1; y=newnode(x);
		if(id<=mid)add(lch[x],lch[y],id,L,mid);
		else add(rch[x],rch[y],id,mid+1,R);
		sum[y]=sum[lch[y]]+sum[rch[y]];
	}int query(int x,int y,int k,int L,int R){
		if(L==R)return L; int mid=(L+R)>>1;
		if(k<=sum[lch[y]]-sum[lch[x]])return query(lch[x],lch[y],k,L,mid);
		else return query(rch[x],rch[y],k-sum[lch[y]]+sum[lch[x]],mid+1,R);
	}
}tr;

更多应用

考虑主席树的本质,其实就是一堆有关联的线段树。
不难想到,其实 扫描线+线段树主席树 在只需要访问一个版本的时候可以互相替换。
这样主席树其实还可以处理一些二维平面问题。

例题

[SDOI2009] HH 的项链

这道题和值域有很大的关联,故考虑从值域入手。
统计区间不同的颜色个数,就等同于 统计区间中第一次出现的颜色个数。
这些颜色有什么不一样的?它们上一次出现的位置一定在区间之外!
而我们又不希望区间之外的颜色来干扰,故考虑建立主席树,维护每一个颜色上一次出现的位置。
(当然也可以用 扫描线+树状数组)

//转化为维护该颜色上一个颜色不在此区间内的个数 
#include<iostream>
using namespace std;
constexpr int N=5e5+5,M=1e7;
int n,m,last[N],head[N],rot[N];
struct Segment_Tree{
	int lch[M],rch[M],sum[M],cnt;
	int newnode(int x){
		int id=++cnt;  sum[id]=sum[x];
		lch[id]=lch[x],rch[id]=rch[x];
		return id;
	}void add(int x,int &y,int id,int L,int R){
		if(L==R){ y=newnode(x),sum[y]++; return; }
		int mid=(L+R)>>1; y=newnode(x);
		if(id<=mid)add(lch[x],lch[y],id,L,mid);
		else add(rch[x],rch[y],id,mid+1,R);
		sum[y]=sum[lch[y]]+sum[rch[y]];
	}int query(int x,int y,int l,int r,int L,int R){
		if(L==l&&R==r)return sum[y]-sum[x];
		int mid=(L+R)>>1;
		if(r<=mid)return query(lch[x],lch[y],l,r,L,mid);
		else if(mid+1<=l)return query(rch[x],rch[y],l,r,mid+1,R);
		else return query(lch[x],lch[y],l,mid,L,mid)+query(rch[x],rch[y],mid+1,r,mid+1,R);
	}
}tr;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); 
	cin>>n>>m;
	for(int i=1,x;i<=n;i++)
		cin>>x,last[i]=head[x],head[x]=i;
	for(int i=1;i<=n;i++)
		tr.add(rot[i-1],rot[i],last[i],0,n);
	for(int i=1,ans=0,L,R;i<=m;i++){
		cin>>L>>R,L=(L+ans)%n+1,R=(R+ans)%n+1;
		if(L>R)swap(L,R);
		ans=tr.query(rot[L-1],rot[R],0,L-1,0,n);
		cout<<ans<<'\n';
	}return 0;
}

[SDOI2013] 森林

在做这道题之前,可以先考虑一个子问题:Count on a tree
很简单,对树的根链建立主席树,查询时利用树上前缀和。

那么这道题呢?树的形态会改变。
注意到只有连边操作,故考虑 启发式合并,复杂度是 \(O(nlogn)\) 的。
每次合并直接暴力增加主席树,总的复杂度就是 \(O(nlog^2n)\) 的。

(代码太长,就不放了)

[国家集训队] middle

看到中位数,想到二分。
对于区间 \([L,R]\) 中,得到 \(< mid\)\(\ge mid\) 的数的个数 分别为 \(x,y\)
令满足 \(L\in [a,b],R\in [c,d]\)\(x,y\) 中,\(a_1=max(y-x),a_2=min(y-x)\)
\(a_1\ge a_2> 0\),说明 \(mid\) 一定不是中位数,但中位数一定比 \(mid\) 大。
\(a_1\ge 0 \ge a_2\),说明 \(mid\) 是中位数,但不一定是最大的中位数。
\(0>a_1 \ge a_2\),说明 \(mid\) 一定不是中位数,但中位数一定比 \(mid\) 小。
(可以用连续变化的函数理解,\(f(x)=f(x-1)\pm 1\)
统一一下:
\(max(y-x)\ge 0\)\(mid\) 应该变大。
\(max(y-x)< 0\)\(mid\) 应该变小。

但是如何处理 \(< mid\)\(\ge mid\) 的数的个数?
考虑由于我们只需知道 \(<mid\) 的数的个数,故考虑将值域与下标互换,对于原本的值域建立主席树。
统计时对于区间求前后缀最大值 并 合并。
时间复杂度为 \(O(nlog^2n)\)

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
constexpr int N=2e4+5,M=1e7;
int n,a[N],rk[N],tt,m,q[4],rot[N];
vector<int> v[N];
struct State{
	int sum,suml,sumr;
	State operator +(const State &b)const{
		State a=*this,ans;
		ans.sum=a.sum+b.sum;
		ans.suml=max(a.suml,a.sum+b.suml);
		ans.sumr=max(a.sumr+b.sum,b.sumr);
		return ans;
	}
};
struct Segment_Tree{
	int lch[M],rch[M],cnt;
	State dp[M];
	void build(int &x,int L,int R){
		x=++cnt; int mid=(L+R)>>1;
		if(L==R){ dp[x]=State{1,1,1}; return; }
		build(lch[x],L,mid),build(rch[x],mid+1,R);
		dp[x]=dp[lch[x]]+dp[rch[x]];
	}int newnode(int x){
		lch[++cnt]=lch[x],rch[cnt]=rch[x];
		dp[cnt]=dp[x]; return cnt;
	}void add(int x,int &y,int id,int L,int R){
		if(L==R){ y=newnode(x),dp[y]=State{-1,-1,-1}; return; }
		int mid=(L+R)>>1; y=newnode(x);
		if(id<=mid)add(lch[x],lch[y],id,L,mid);
		else add(rch[x],rch[y],id,mid+1,R);
		dp[y]=dp[lch[y]]+dp[rch[y]];
	}State query(int x,int l,int r,int L,int R){
		if(L==l&&r==R)return dp[x];
		if(l>r)return State{0,0,0}; int mid=(L+R)>>1;
		if(r<=mid)return query(lch[x],l,r,L,mid);
		else if(mid+1<=l)return query(rch[x],l,r,mid+1,R);
		else return query(lch[x],l,mid,L,mid)+query(rch[x],mid+1,r,mid+1,R);
	}
}tr;
bool check(int x){
	x--;
	State xx=tr.query(rot[x],q[0],q[1],1,n);
	State yy=tr.query(rot[x],q[1]+1,q[2]-1,1,n);
	State zz=tr.query(rot[x],q[2],q[3],1,n);
	int sum=xx.sumr+yy.sum+zz.suml;
	return (sum>=0);
}signed main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n,tr.build(rot[0],1,n);
	for(int i=1;i<=n;i++)cin>>a[i],rk[i]=a[i];
	sort(rk+1,rk+n+1),tt=unique(rk+1,rk+n+1)-rk-1;
	for(int i=1;i<=n;i++)a[i]=lower_bound(rk+1,rk+tt+1,a[i])-rk;
	for(int i=1;i<=n;i++)v[a[i]].push_back(i);
	for(int i=1;i<=tt;i++){
		rot[i]=rot[i-1];
		for(int u:v[i])tr.add(rot[i],rot[i],u,1,n);
	}cin>>m;
	for(int i=1,last=0;i<=m;i++){
		for(int j=0;j<4;j++)
			cin>>q[j],q[j]=(q[j]+last)%n+1;
		sort(q,q+4);
		int L=0,R=tt+1,mid;
		while(L+1<R){
			mid=(L+R)>>1;
			if(check(mid))L=mid;
			else R=mid;
		}last=rk[L],cout<<last<<'\n';
	}return 0;
}

[FJOI2016] 神秘数

先考虑如何求 \(S\) 的神秘数。
现将 \(S\) 从小到大排序,设当前的神秘数为 \(a\)
\(S_i\le a\),则新的神秘数为 \(S_i+a\)
\(s_i>a\),则集合的神秘数就为 \(a\),因为以后的数都不会产生贡献。

我们可以得到一个充分条件:若 \(a\) 为神秘数,则 \(\le a\) 的数的和 \(< a\)
显然不是必要条件,因为无法确定 \(a\) 一定最小。

因此我们考虑一个暴力的算法:
初始时令 \(a=1\)
\(\le a\) 的数的和 \(sum<a\),得到神秘数为 \(a\)
\(\le a\) 的数的和 \(sum\ge a\),令 \(a=sum+1\)
这个算法就是对上面更暴力的方法的优化。
现在我们证明其复杂度:
由于每次产生贡献的值域为 \((a,sum+1]\),那么 \(sum'\ge sum+1+a+1=sum+a+2\)
所以有 \(sum''\ge sum'+1+a'+1\ge (sum+1+a+1)+1+(sum+1)+1=2sum+a+4=sum+sum'+2\)
也就是 \(sum\) 成斐波那契数列一样增长,又知道斐波那契数列是指数级增长。
故复杂度为 \(logW\)

由于上面的查询就是为权值线段树设计的,故考虑主席树来统计区间信息,查询区间和。
时间复杂度:\(O(mlognlogW)\)

#include<iostream>
using namespace std;
constexpr int N=1e5+5,M=6e6+6;
int n,m,rot[N];
struct Segment_Tree{
	int lch[M],rch[M],sum[M],sum2[M],cnt;
	int newnode(int x){
		lch[++cnt]=lch[x],rch[cnt]=rch[x];
		sum[cnt]=sum[x],sum2[cnt]=sum2[x]; return cnt;
	}void add(int x,int &y,int id,int L,int R){
		if(L==R){ y=newnode(x),sum[y]++,sum2[y]+=L; return; }
		int mid=(L+R)>>1; y=newnode(x);
		if(id<=mid)add(lch[x],lch[y],id,L,mid);
		else add(rch[x],rch[y],id,mid+1,R);
		sum[y]=sum[lch[y]]+sum[rch[y]];
		sum2[y]=sum2[lch[y]]+sum2[rch[y]];
	}int query(int x,int y,int l,int r,int L,int R){
		if(L==l&&r==R)return sum2[y]-sum2[x];
		if(l>r)return 0; int mid=(L+R)>>1;
		if(r<=mid)return query(lch[x],lch[y],l,r,L,mid);
		else if(mid+1<=l)return query(rch[x],rch[y],l,r,mid+1,R);
		else return query(lch[x],lch[y],l,mid,L,mid)+query(rch[x],rch[y],mid+1,r,mid+1,R);
	}
}tr;
signed main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n;
	for(int i=1,x;i<=n;i++)
		cin>>x,tr.add(rot[i-1],rot[i],x,1,1e9);
	cin>>m;
	for(int i=1,x,y,ans;i<=m;i++){
		cin>>x>>y,x--,ans=1;
		while(1){
			int s=tr.query(rot[x],rot[y],1,ans,1,1e9);
			if(!s)break;
			if(s>=ans)ans=s+1;
			else break;
		}cout<<ans<<'\n';
	}return 0;
}

动态主席树

世上哪有什么动态主席树,唯有树套树。
Dynamic Rankings

考虑树状数组套权值线段树,外层维护区间,内层维护权值。
修改时修改树状数组对应节点的线段树,
查询时现将所有用到外层节点处理好,再进行线段树二分,同样使用前缀和相减。
(线段树套线段树同理)
时间复杂度:\(O(nlogn+qlog^2n)\)

#include<iostream>
using namespace std;
constexpr int N=1e5+5,M=5e7;
int n,m,a[N];
struct Segment_Tree{
	int lowbit(int x){ return x&(-x); }
	int lch[M],rch[M],sum[M],cnt,rot[N];
	int b[25],c[25];
	void add_(int &x,int id,int val,int L,int R){
		if(!x)x=++cnt; int mid=(L+R)>>1;
		if(L==R){ sum[x]+=val; return; }
		if(id<=mid)add_(lch[x],id,val,L,mid);
		else add_(rch[x],id,val,mid+1,R);
		sum[x]=sum[lch[x]]+sum[rch[x]];
	}void add(int x,int id,int val){
		for(int i=x;i<=n;i+=lowbit(i))
			add_(rot[i],id,val,0,1e9);
	}int query(int L,int R,int k){
		int t1=0,t2=0; L--;
		for(int i=R;i;i-=lowbit(i))b[++t1]=rot[i];
		for(int i=L;i;i-=lowbit(i))c[++t2]=rot[i];
		L=0,R=1e9;//线段树左右端点 
		while(L!=R){
			int s=0,mid=(L+R)>>1;
			for(int i=1;i<=t1;i++)s+=sum[lch[b[i]]];
			for(int i=1;i<=t2;i++)s-=sum[lch[c[i]]];
			if(k<=s){
				for(int i=1;i<=t1;i++)b[i]=lch[b[i]];
				for(int i=1;i<=t2;i++)c[i]=lch[c[i]];
				R=mid;
			}else {
				for(int i=1;i<=t1;i++)b[i]=rch[b[i]];
				for(int i=1;i<=t2;i++)c[i]=rch[c[i]];
				L=mid+1,k-=s;
			}
		}return L;
	}
}tr;
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],tr.add(i,a[i],1);
	for(int i=1,x,y,z;i<=m;i++){
		char ch; cin>>ch>>x>>y;
		if(ch=='Q')cin>>z,cout<<tr.query(x,y,z)<<'\n';
		else tr.add(x,a[x],-1),a[x]=y,tr.add(x,a[x],1);
	}return 0;
}

可持久化线段树扩展

可持久化数组

单点修改,单点查询线段树。

可持久化并查集

在可持久化数组的基础上实现。
注意不能路径压缩,只能使用 启发式合并/按轶合并。

posted @ 2026-02-25 16:38  zhoumengxuan  阅读(0)  评论(0)    收藏  举报