主席树学习笔记

主席树

主席树,又称可持久化线段树,主要思想在线段树上加上一些历史节点去维护一些历史数据,实现对过去数据的查询。
去盗了一张图
主席树学习笔记
途中橙色的节点维护的就是历史数据。

主席树的用处

引用大佬的一段话:
主席树就是利用函数式编程的思想来使线段树支持询问历史版本、同时充分利用它们之间的共同数据来减少时间和空间消耗的增强版的线段树。
其实它是一堆线段树的集合体。

因为对于一个点的修改,它对整棵树的影响就是根节点到叶节点的这条长度为\(log~n\)的路径上的每一个节点,所以它每次修改就在该路径的节点旁新开\(log~n\)个节点,初始时他们的左右节点的信息和原来路径上的点一样,维护的信息则变成了需要修改的值,优化了时空的复杂度。

一般我们见到什么静态的区间第 k 小(大) 数就可以使用主席树。

例题: 【洛谷 P3834 【模板】可持久化线段树 2(主席树)】

解法&思路:

这是一个很明显的例题,静态区间第 k 大数。
还记得树状数组怎么求的逆序对吗?它利用的是值域去维护的前缀和
我们考虑使用 线段树去维护区间\([1,1],[1,2],[1,3],·····[1,n-1],[1,n]\)的信息。
那么我们每次询问\([l,r]\)时就可以用\([1,r]-[1,l-1]\)搞定

但是这样就要搞 n 棵线段树,空间爆炸,时间复杂度将变成\(O(m*n~log~n)\)
但是线段树的很多节点是重复的。
所以我们就要用到主席树了。

首先我们要建一棵空的线段树。
注意主席树的节点并不能像线段树那样直接求出来,需要去记录节点的信息,因为它不再是一棵完全二叉树了,仅仅只是一颗普普通通的二叉树。
注意我们以每个点的值域来建树,然后我们要保证值域的单调递增,所以要先排序去重,就是离散化。

        scanf("%lld%lld",&n,&m);
	rep(i,1,n) 
	{
		scanf("%lld",&a[i]);
		b[i]=a[i];
	}
	sort(b+1,b+n+1);
	top=unique(b+1,b+n+1)-b-1; //离散化

建立初始线段树:

void build(ll &now,ll x,ll y) //初始时建一棵线段树
{
	now=++cnt; //开新节点
	ll mid=x+y>>1;
	if(x==y) return ;//如果维护的是前缀和就要像线段树那样去搞
	build(lc[now],x,mid); //创建左子节点
	build(rc[now],mid+1,y); //创建右子节点
}

然后我们要像树状数组求解逆序对那样対值域进行单点修改。
单点修改:

ll add(ll now,ll x,ll y)
{
	ll nxt=++cnt; //开一个新节点
	lc[nxt]=lc[now],rc[nxt]=rc[now]; //左右子节点的信息相同
	tree[nxt]=tree[now]+1; //修改维护信息
	if(x==y) return nxt; //返回现在的节点编号
	ll mid=x+y>>1;
	if(p<=mid) lc[nxt]=add(lc[nxt],x,mid); //维护左子树
	else rc[nxt]=add(rc[nxt],mid+1,y); //维护右子树
	return nxt;
}

区间查询:返回的时答案在离散化数组中的编号
注意在查询右子树时要注意减去当前查询到的比它大的数的个数

ll ask(ll x,ll y,ll le,ll ri,ll k)
{
	ll ans,val=tree[lc[y]]-tree[lc[x]]; //左子树两个区间相减
	if(le==ri) return le; //返回位置
	ll mid=le+ri>>1;
	if(k<=val) ans=ask(lc[x],lc[y],le,mid,k); //比当前值要小,查询左子树
	else ans=ask(rc[x],rc[y],mid+1,ri,k-val); 
	//已经有 val 个数比它大了,所以需要减去,在右区间不需要找满 k 个
	return ans;
}

完整代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
#include<vector>
#define r register
#define rep(i,x,y) for(r ll i=x;i<=y;++i)
#define per(i,x,y) for(r ll i=x;i>=y;--i)
using namespace std;
typedef long long ll;
const ll V=2*1e5+10;
ll tree[V<<5],a[V<<5],b[V<<5];
ll lc[V<<5],rc[V<<5],x,y,k;
ll n,m,top,cnt,f[V<<5],ans,p;
void build(ll &now,ll x,ll y)
{
	now=++cnt;
	ll mid=x+y>>1;
	if(x==y) return ;
	build(lc[now],x,mid);
	build(rc[now],mid+1,y);
}
ll add(ll now,ll x,ll y)
{
	ll nxt=++cnt;
	lc[nxt]=lc[now],rc[nxt]=rc[now];
	tree[nxt]=tree[now]+1;
	if(x==y) return nxt;
	ll mid=x+y>>1;
	if(p<=mid) lc[nxt]=add(lc[nxt],x,mid);
	else rc[nxt]=add(rc[nxt],mid+1,y);
	return nxt;
}
ll ask(ll x,ll y,ll le,ll ri,ll k)
{
	ll ans,val=tree[lc[y]]-tree[lc[x]];
	if(le==ri) return le;
	ll mid=le+ri>>1;
	if(k<=val) ans=ask(lc[x],lc[y],le,mid,k);
	else ans=ask(rc[x],rc[y],mid+1,ri,k-val);
	return ans;
}
int main()
{
	scanf("%lld%lld",&n,&m);
	rep(i,1,n) 
	{
		scanf("%lld",&a[i]);
		b[i]=a[i];
	}
	sort(b+1,b+n+1);
	top=unique(b+1,b+n+1)-b-1;
	build(f[0],1,top);
	rep(i,1,n)
	{
		p=lower_bound(b+1,b+top+1,a[i])-b;
		f[i]=add(f[i-1],1,top);
	}
	rep(i,1,m)
	{
		scanf("%lld%lld%lld",&x,&y,&k);
		ans=ask(f[x-1],f[y],1,top,k);
		printf("%lld\n",b[ans]);
	}
	return 0;
}
上一篇:LC 打乱数组


下一篇:LC 有效的字母异位词