主席树

前置知识

主席树:用于处理 历史版本问题:需要以优秀复杂度来解决可持久化问题的数据结构。
可持久化问题(数据结构):

1.部分可持久化:所有版本都可以访问,但是只有最新版本可以修改。
2.完全可持久化:所有版本都既可以访问又可以修改。


初步介绍

主席树:全称可持久化权值线段树,用函数式手法实现可持久化,函数式线段树 是指使用函数式编程思想的线段树。在函数式编程思想中,将计算机运算视为数学函数,并避免可改变的状态或变量。不难发现,函数式线段树是完全可持久化的。

引入:考虑一个例题:区间第k小--给定一个1到n的区间和区间内的值,求出[l,r]内第k小数。

解决方案

为什么主席树可以解决和个问题呢?主席树的主要思想就是:保存每次插入操作时的历史版本,以便查询区间第k小。见例子与图:

假设我们的数列为1 5 2 3 7 6 4

先将其离散化(原因:为保证不超出空间限制,并且:主席树只在乎数字的大小,后文会详细介绍)。用离散的数组建立一棵空树:
空树

然后将原数组中的树一个个插入树中,并将包含此数字的节点的sum都++:

注意:主席树每个节点保存的信息是:从离散化之后排在l的数字到离散化后排在r的数字一共出现的次数。

离散化的意义:将原数组巨大的数字,以映射的思想,将其按大小转化为1,2,3......这样可以节省空间,当然,如果你愿意用链表那也是完全OK的。<_<

1

52

......中间步骤省略4

那现在的问题就是:我们如何进行可持久化操作?假设我们要查询[2,5]中第2大的数,考虑:如果你有一个函数,你要求出f(5)-f(2)的值,你会怎么做?主席树是一样的,我们拿出第5次和第2此的版本,把每个节点的sum相减(没错,这就是前缀和思想,每个系欸但存储信息有单调性,并且:详见后恰好就是中间插入带来的贡献,可以自己脑补一下,相信你已经理解了。),剩下的树就是中间3次插入带来的贡献。

考虑查询操作:从根,向下查询,如果当前左子树的大小比要查询的k小,那么向右,反之向左,原因:左子树存的是1到mid这些大小的数字出现次数,如果次数大于k,说明我们的答案在这之中(很好理解),于是,我们就可以用\(log_2n\)的复杂度解决查询问题,但是问题来了:我们还没有实现可持久化。

持久化操作

请在此观察之前修改的图:我们发现,每次插入,之后\(log_2n\)的节点会被修改,所以:我们新建根之后,只用再次修改与插入值相关的点就行了,不用每次都建立线段树。

1.建立一棵空树
2.进行原数组插入时,从空树最后一个节点的节点编号tot+1,建立一个新的节点,然后遍历需要修改的节点,不用修改的直接连起来,就可以解决持久化问题。同时,注意:我们需要建立一个时间轴(数组),保存修改节点根的顺序。

相关代码:

inline int update(int pre, int l, int r, int x){
	int rt = ++ tot;
	L[rt] = L[pre]; R[rt] = R[pre]; sum[rt] = sum[pre] + 1; 
	if (l < r){
		if (x <= mid) L[rt] = update(L[pre], l, mid, x);
		else R[rt] = update(R[pre], mid+1, r, x);
	}
	return rt;

}
int main(){
    ······
	for (int i=1;i<=n;i++){
			a[i] = lower_bound(b+1, b+1+m, a[i]) - b;
			//找到插入值在离散化后的位置
			T[i] = update(T[i-1], 1, m, a[i]);
			//T数组保存的就是每次修改的根的编号
		}
}

完整代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cctype>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long LL;
 
const int N = 200010, LOG = 20;
//一般我们用20倍空间来保存(防止溢出)
int n, m, q, tot = 0;
//tot就是时间戳(每个节点的编号)
int a[N], b[N];
int T[N], sum[N*LOG], L[N*LOG], R[N*LOG];
 
//建树
inline int build(int l, int r){
	int rt = ++ tot;
	int mid=l+r>>1;
	if (l < r){
		L[rt] = build(l, mid);
		R[rt] = build(mid+1, r);
	}
	return rt;
}
 
//更新
inline int update(int pre, int l, int r, int x){
	int rt = ++ tot;
	L[rt] = L[pre]; R[rt] = R[pre]; sum[rt] = sum[pre] + 1; 
	if (l < r){
		int mid=l+r>>1;
		//按离散化有的编号进行更新
        if (x <= mid) L[rt] = update(L[pre], l, mid, x);
		else R[rt] = update(R[pre], mid+1, r, x);
	}
	return rt;
}
 
//查询操作
inline int query(int u, int v, int l, int r, int k){
	if (l == r) return l;
	int x = sum[L[v]] - sum[L[u]];
	//x就是左子树的大小
    int mid=l+r>>1;
	if (x >= k) return query(L[u], L[v], l, mid, k);
	else return query(R[u], R[v], mid+1, r, k-x);
}
 
int main(){
	int t; 
	tot=0;
	memset(T, 0, sizeof T);
	memset(sum, 0, sizeof sum);
	memset(L, 0, sizeof L); 
	memset(R, 0, sizeof R);
	scanf("%d%d",&n,&q);
		for (int i=1;i<=n;i++) 
		scanf("%d", &a[i]), b[i] = a[i];
	sort(b+1, b+1+n);
	m = unique(b+1, b+1+n)-b-1;
	T[0] = build(1, m);
	for (int i=1;i<=n;i++){
		a[i] = lower_bound(b+1, b+1+m, a[i]) - b;
		//查找离散后的下标
        T[i] = update(T[i-1], 1, m, a[i]);
		//增加时间,用来查询历史
        //printf ("a[%d]=%d,T[%d]=%d\n",i,a[i],i,T[i]);
	}
	while (q --){
		int x, y, z; 
		scanf("%d%d%d", &x, &y, &z);
		int p = query(T[x-1], T[y], 1, m, z);
		printf("%d\n", b[p]);
	}
	return 0;
}
posted @ 2021-01-27 09:36  fallingdust  阅读(688)  评论(2编辑  收藏  举报