主席树(可持久化线段树)

Posted jasony

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了主席树(可持久化线段树)相关的知识,希望对你有一定的参考价值。

主席树(可持久化线段树)

前置芝士

知识点

线段树,权值线段树(不一样),离散化,前缀和(思想)

由来

据说,是一位叫fotile主席的大大在写一道题时因为不会划分树就临时yy出一个算法,于是,这算法就这么诞生了。

作用

对区间求 (kth)

思想

思考优化策略

一列数,可以对于每个点i都建一棵权值线段树,维护1~i这些数,每个不同的数出现的个数(权值线段树以值域作为区间)

现在,n棵线段树就建出来了,第i棵线段树代表1~i这个区间

例如,一列数,n为6,数分别为1 3 2 3 6 1
首先,每棵树都是这样的:

![](C:UsersAdministratorDesktopmy things220190511122617292.png)

以第4棵线段树为例,1~4的数分别为1 3 2 3

![](C:UsersAdministratorDesktopmy things220190511123014414.png)

因为是同一个问题,n棵权值线段树的形状是一模一样的,只有节点的权值不一样
所以这样的两棵线段树之间是可以相加减的(两颗线段树相减就是每个节点对应相减)

想想,第x棵线段树减去第y棵线段树会发生什么?
第x棵线段树代表的区间是[1,x]
第y棵线段树代表的区间是[1,y]
两棵线段树一减
设x>y,[1,x]?[1,y]=[y+1,x][1,x]-[1,y]=[y+1,x][1,x]?[1,y]=[y+1,x]
所以这两棵线段树相减可以产生一个新的区间对应的线段树!

等等,这不是前缀和的思想吗
这样一来,任意一个区间的线段树,都可以由我这n个基础区间表示出来了!
因为每个区间都有一个线段树
然后询问对应区间,在区间对应的线段树中查找kth就行了

这就是主席树的一个核心思想:前缀和思想

具体做法待会儿再讲,现在还有一个严峻的问题,就是n棵线段树空间太大了!
如何优化空间,就是主席树另一个核心思想

我们发现这n棵线段树中,有很多重复的点,这些重复的点浪费了大部分的空间,所以考虑如何去掉这些冗余点

在建树中优化

假设现在有一棵线段树,序列往右移一位,建一棵新的线段树
对于一个儿子的值域区间,如果权值有变化,那么新建一个节点,否则,连到原来的那个节点上

现在举几个例子来说明
序列4 3 2 3 6 1

区间[1,1]的线段树(蓝色节点为新节点)

![](C:UsersAdministratorDesktopmy things220190511125552180.png)

区间[1,2]的线段树(橙色节点为新节点)

![](C:UsersAdministratorDesktopmy things220190511130206210.png)

区间[1,3]的线段树(紫色节点为新节点)

![](C:UsersAdministratorDesktopmy things220190511130727560.png)

这样是不是非常优秀啊?
(部分借用https://blog.csdn.net/ModestCoder_/java/article/details/90107874)

模板及代码

离散化
for(int i=1;i<=n;i++) a[i]=read(),b[i]=a[i];
sort(b+1,b+n+1);
q=unique(b+1,b+n+1)-b-1;
插入(建新树)
int update(int o,int l,int r){
    int oo=++cnt;
    ls[oo]=ls[o],rs[oo]=rs[o],sum[oo]=sum[o]+1;
    if(l==r) return oo;
    int mid=(l+r)>>1;
    if(mid>=p) ls[oo]=update(ls[oo],l,mid);	//下文有p
    else rs[oo]=update(rs[oo],mid+1,r);
}
建树
//函数
void build(int &rt,int l,int r){
    rt=++cnt,sum[rt]=0;
    if(l==r) return ;
    int mid=(l+r)>>1;
    build(ls[rt],l,mid);
    build(rs[rt],mid+1,r);
}

//主函数中的操作
build(rt[0],1,q);				//建一棵空树,虽说不建也没关系 以防万一
for(int i=1;i<=n;i++){			//1~n依次建树
    p=lower_bound(b+1,b+q+1,a[i])-b;
    rt[i]=update(rt[i-1],1,q);
}
查询
//函数
int query(int u,int v,int l,int r,int k){//u、v为两棵线段树当前节点编号,相减就是询问区间
	int mid=(l+r)>>1,x=sum[ls[v]]-sum[ls[u]];
	if(l==r) return l;
    if(x>=k) return query(ls[u],ls[v],l,mid,k);
    else return query(rs[u],rs[v],mid+1,r,k-x);
    //kth操作,排名<=左儿子的数的个数,说明在左儿子,进入左儿子;反之,目标在右儿子,排名需要减去左儿子的权值
}

//主函数中的操作
while(m--){
    int l=read(),r=read(),k=read();
    printf("%d
",b[query(rt[l-1],rt[r],1,q,k)]);
}

模板题1

区间第 (k)
代码
#include <bits/stdc++.h>
#define maxn 200010
using namespace std;
int a[maxn], b[maxn], n, m, q, p, sz;
int lc[maxn << 5], rc[maxn << 5], sum[maxn << 5], rt[maxn << 5];
//空间要注意

inline int read(){
	int s = 0, w = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == ‘-‘) w = -1;
	for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
	return s * w;
}

void build(int &rt, int l, int r){
	rt = ++sz, sum[rt] = 0;
	if (l == r) return;
	int mid = (l + r) >> 1;
	build(lc[rt], l, mid); build(rc[rt], mid + 1, r);
}

int update(int o, int l, int r){
	int oo = ++sz;
	lc[oo] = lc[o], rc[oo] = rc[o], sum[oo] = sum[o] + 1;
	if (l == r) return oo;
	int mid = (l + r) >> 1;
	if (mid >= p) lc[oo] = update(lc[oo], l, mid); else rc[oo] = update(rc[oo], mid + 1, r);
	return oo;
}

int query(int u, int v, int l, int r, int k){
	int mid = (l + r) >> 1, x = sum[lc[v]] - sum[lc[u]];
	if (l == r) return l;
	if (x >= k) return query(lc[u], lc[v], l, mid, k); else return query(rc[u], rc[v], mid + 1, r, k - x);
}

int main(){
	n = read(), m = read();
	for (int i = 1; i <= n; ++i) a[i] = read(), b[i] = a[i];
	sort(b + 1, b + 1 + n);
	q = unique(b + 1, b + 1 + n) - b - 1;
	build(rt[0], 1, q);
	for (int i = 1; i <= n; ++i){
		p = lower_bound(b + 1, b + 1 + q, a[i]) - b;
		rt[i] = update(rt[i - 1], 1, q);
	} 
	while (m--){
		int l = read(), r = read(), k = read();
		printf("%d
", b[query(rt[l - 1], rt[r], 1, q, k)]);
	}
	return 0;
}

模板题2

可持久化数组
代码
#include <bits/stdc++.h>
#define maxn 1000010
using namespace std;
struct chairman{
	int l, r, v;
}seg[maxn << 5];
int rt[maxn], sz, n, m, a[maxn];

inline int read(){
	int s = 0, w = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == ‘-‘) w = -1;
	for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
	return s * w;
}

void build(int &rt, int l, int r){
	rt = ++sz;
	if (l == r){
		seg[rt].v = a[l]; return;
	}
	int mid = (l + r) >> 1;
	build(seg[rt].l, l, mid); build(seg[rt].r, mid + 1, r);
}

int update(int o, int l, int r, int p, int k){
	int oo = ++sz;
	seg[oo] = seg[o];
	if (l == r){
		seg[oo].v = k; return oo;
	}
	int mid = (l + r) >> 1;
	if (mid >= p) seg[oo].l = update(seg[oo].l, l, mid, p, k); else
	seg[oo].r = update(seg[oo].r, mid + 1, r, p, k);
	return oo;
}

int query(int rt, int l, int r, int p){
	if (l == r) return seg[rt].v;
	int mid = (l + r) >> 1;
	if (mid >= p) return query(seg[rt].l, l, mid, p); else
	return query(seg[rt].r, mid + 1, r, p);
}

int main(){
	n = read(), m = read();
	for (int i = 1; i <= n; ++i) a[i] = read();
	build(rt[0], 1, n);
	for (int i = 1; i <= m; ++i){
		int x = read(), opt = read(), y = read();
		if (opt == 1){
			int z = read();
			rt[i] = update(rt[x], 1, n, y, z);
		} else{
			rt[i] = rt[x];
			printf("%d
", query(rt[i], 1, n, y));
		}
	}
	return 0;
}














以上是关于主席树(可持久化线段树)的主要内容,如果未能解决你的问题,请参考以下文章

可持久化专题——浅谈主席树:可持久化线段树

可持久化线段树/主席树(静态)

可持久化线段树(主席树)

重码数据结构主席树(可持久化线段树)

主席树(可持久化线段树)

LUOGU P3834 模板可持久化线段树 1(主席树)