主席树


因为近期进行了有关 主席树 的专题训练,为了巩固对 “主席树” 的了解,同时方便后期复习,在这里整理了一下有关 “主席树” 的一些要点。

模板题传送门

————————————————————

首先,我们来讲讲

什么是 “主席树” ?

主席树 ,又名 “可持久化线段树” ,顾名思义,其本质就是一棵 “进化版” 的线段树,且进化了 “可持久化” 这个功能。

那么,什么是 “可持久化”

百度百科
上的解释是 “持久化是将程序数据在持久状态和瞬时状态间转换的机制” 。

换句话说,传统意义上的线段树在修改后就无法复原,只能记录当前状态,不够“持久”,而可持久化线段树在修改后仍然可以访问以前的状态,很“持久”。

可它为什么又叫 “主席树” ?

这是一个比较有意思的问题,有一种说法是:据说发明主席树的人的姓名简写为hjt,与当时的国家主席的姓名简写一样,故叫做“主席树”。

————————————————————

了解了主席树,我们再了解一下

主席树如何 “持久” ?

传统的线段树只能维护一维,并在这一维上进行修改、询问。

就像这样:

线段树

而主席树在线段树的基础上多了一维,相当于同时存了多个线段树,就像这样:

主席树

或者有点像这样:

主席树2

但这仍然是暴力开出来的多个线段树,在实际应用中很容易MLE,可真正的主席树是一个实用且好用的算法,所以显然不是这样。

我们注意到,每次更新线段树时,只会更新一个点或者一个区间,也就是说,大部分节点是没有改变的。

只要我们把这些不变的点压成一个点,无论是时间还是内存上都会大大优化。

换句话说,在更新时,只用将修改的节点重开即可,其他节点保留。

所以真正的主席树长得有点像这样:

主席树3

看不习惯?换一个:

主席树4

也就是说,主席树通过建点不建树这种折中的办法,不仅节约了时间和空间,还实现了“持久化”。

————————————————————

到这里为止,已经对主席树有了大致的了解。那么

如何实现主席树?


模板题
为例。

事先说明,在本题中,主席树起到了前缀和的作用,主席树节点以数值(区间)为下标进行二分,主席树节点权值记录该数值(区间)出现的数量的前缀和。

先解释一下主席树最关键的几个变量:

  • rt,即root,每棵主席树的根节点。
  • ls,即left son,记录每个节点的左儿子
  • rs,即right son,记录每个节点的右儿子
  • t,即tree,记录每个节点的权值
  • tot,即total,统计节点的总个数,用于建新点

主席树的关键在于建点,所以,建点的部分是必不可少的。建新点时要注意将上一个点的状态继承过来,这样的主席树才是完整的:

void nw(int &p){
	++tot;
	t[tot]=t[p];
	ls[tot]=ls[p];
	rs[tot]=rs[p];
	p=tot;
}

更新时,大体上与传统的线段树相似,都是从\([l,r]\)区间转移到\([l,mid]\)\([mid+,r]\)区间。

不同的在于,传统线段树转移时,节点由\(p\)变为\(p<<1\)\(p<<1|1\),而主席树因为建了许多新点,编号不是吧连续的,所以转移时要变为\(ls[p]\)\(rs[p]\)

不要忘记建新点,这才是主席树的精髓!

void change(int l,int r,int x,int &p){
	nw(p);++t[p];
	if(l==r)return;
	int mid=l+r>>1;
	if(x<=mid)change(l,mid,x,ls[p]);
	else change(mid+1,r,x,rs[p]);
}

询问时,大体上也和传统线段树相似,在本题中,因为询问的区间第k小,所以需要记录\(L,R\)两个节点,通过做差求得区间某数个数,再与k比较判断向左还是向右转移:

注意:询问时不能建新点!

int query(int l,int r,int k,int L,int R){
	if(l==r)return l;
	int mid=l+r>>1;
	int s=t[ls[R]]-t[ls[L]];
	if(k<=s)return query(l,mid,k,ls[L],ls[R]);
	return query(mid+1,r,k-s,rs[L],rs[R]);
}

综上,完整的主席树成型了:

struct HJT{
	int tot,ls[MM],rs[MM],t[MM];
	void nw(int &p){
		++tot;
		t[tot]=t[p];
		ls[tot]=ls[p];
		rs[tot]=rs[p];
		p=tot;
	}
	void change(int l,int r,int x,int &p){
		nw(p);++t[p];
		if(l==r)return;
		int mid=l+r>>1;
		if(x<=mid)change(l,mid,x,ls[p]);
		else change(mid+1,r,x,rs[p]);
	}
	int query(int l,int r,int k,int L,int R){
		if(l==r)return l;
		int mid=l+r>>1;
		int s=t[ls[R]]-t[ls[L]];
		if(k<=s)return query(l,mid,k,ls[L],ls[R]);
		return query(mid+1,r,k-s,rs[L],rs[R]);
	}
}T;

再放入题目中,写得以下代码:

#include<bits/stdc++.h>
#define lb lower_bound
using namespace std;
const int M=2e5+5,MM=M*20;
int n,m,a[M],b[M],cnt,rt[M];
struct HJT{
	int tot,ls[MM],rs[MM],t[MM];
	void nw(int &p){
		++tot;
		t[tot]=t[p];
		ls[tot]=ls[p];
		rs[tot]=rs[p];
		p=tot;
	}
	void change(int l,int r,int x,int &p){
		nw(p);++t[p];
		if(l==r)return;
		int mid=l+r>>1;
		if(x<=mid)change(l,mid,x,ls[p]);
		else change(mid+1,r,x,rs[p]);
	}
	int query(int l,int r,int k,int L,int R){
		if(l==r)return l;
		int mid=l+r>>1;
		int s=t[ls[R]]-t[ls[L]];
		if(k<=s)return query(l,mid,k,ls[L],ls[R]);
		return query(mid+1,r,k-s,rs[L],rs[R]);
	}
}T;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i){scanf("%d",&a[i]);b[i]=a[i];}
	sort(b+1,b+n+1);cnt=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;++i){
		a[i]=lb(b+1,b+cnt+1,a[i])-b;
		rt[i]=rt[i-1];
		T.change(1,cnt,a[i],rt[i]);
	}
	for(int l,r,k;m;--m){
		scanf("%d%d%d",&l,&r,&k);
		printf("%d\n",b[T.query(1,cnt,k,rt[l-1],rt[r])]);
	}
	return 0;
}

————————————————————

到这里为止,已经掌握了主席树最基本也是最关键的部分。

当然,主席树的应用还有很多,它不仅可以随着数组变化,记录前缀和,还可以随着时间变化,并随时回溯(据说Word、Dev等应用中的返回功能与主席树的原理类似)。

\(\mathcal{By}\quad\mathcal{Most}\ \mathcal{Handsome}\)


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM