欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

主席树详解——区间的权值线段树

程序员文章站 2024-02-13 23:26:58
...

为啥叫主席树?

很多人一看到这名字觉得这肯定是个很厉害的数据结构,从而望而却步。
其实为啥这个数据结构叫主席树呢,emmmm…
主席树详解——区间的权值线段树
这个数据结构是这位同学在考场上想出来的,而他的名字与某位主席缩写有一致的相似性QAQ


引入题(静态区间第k小)

题目描述

如题,给定 nn 个整数构成的序列,将对于指定的闭区间查询其区间内的第 kk 小值。

输入格式

第一行包含两个正整数 n,mn,m,分别表示序列的长度和查询的个数。

第二行包含 nn 个整数,表示这个序列各项的数字。

接下来 mm 行每行包含三个整数 l,r,kl, r, k , 表示查询区间 [l,r][l, r] 内的第 kk 小值。

输出格式

输出包含 kk 行,每行一个整数,依次表示每一次查询的结果

输入输出样例
输入 #1

5 5
25957 6405 15770 26287 26465
2 2 1
3 4 1
4 5 1
1 2 2
4 4 1

输出 #1

6405
15770
26287
25957
26287


想想怎么解决这题

唔好像只会暴力

如果简化一下

先假设每个数值域都在 [1,n][1,n]
假如每次询问都是问 [1,n][1,n] 的第 kk 大,那怎么做呢?(雾似乎直接排序然后输出就好了
但如果我们硬要用数据结构来动态查询呢?我们学过啥?
权值线段树!!!
权值线段树的每个叶子节点代表的是一个数,节点的值是这个数出现的次数。
那么我们每次查找 [1,n][1,n] 中的第 kk 大,查询的时候判断一下 kksum[lson]sum[lson] 的大小,如果 kk 小,说明 [l,m][l,m] 范围内已经有了不止 kk 个数,说明第 kk 大肯定在左子树内,于是就往左子树查询第 kk 大数,否则就往右子树查询第 ksum[lson]k - sum[lson] 大的数。

问题来了

现在求的不是区间 [1,n][1,n],而是区间 [l,r][l,r],这怎么破呢?
要是我们能够在 [l,r][l,r] 之间建一棵权值线段树就好了QAQ
没错这就是主席树的思想!
为了得到区间 [l,r][l,r] 的权值线段树!
如果我们得到了 [1,r][1,r][1,l1][1,l-1] 的权值线段树,那么求 [l,r][l,r] 每个节点的 sumsum 值不就是 sum[rt]sum[las]sum[rt] - sum[las] 了吗!!!
rtrt[1,r][1,r] 的权值线段树的当前节点,laslas[1,l1][1,l-1] 的权值线段树的当前节点


进入正题(敲黑板

考虑建立 [1,i][1,i] 的权值线段树

如果我们每次都直接建树,时空复杂度还是爆炸
问题在于我们每次都要重新建立 [1,i1][1,i-1] 的信息
如果这部分信息能直接利用起来就好了!
事实上真的可以直接用!!
接着往下看!

建树

主席树详解——区间的权值线段树
这个图是 [1,i][1,i] 的权值线段树,可以看到,每颗权值线段树,都和前一棵有很强的相似性!!!其实,每一棵和前一棵的区别仅仅在于一条链上!
每次对 [1,i][1,i] 建树,插入 a[i]a[i] 这个节点时,最多只会影响到 lognlogn 个节点的 sumsum 值 (单点插入的复杂度是 lognlogn),我们其实只要建这 lognlogn 个节点就好了!其他节点直接用 i1i-1 那棵树的,就可以避免重复建树!
先看看 updateupdate 部分的代码

void update(int l, int r, int &rt, int p, int las){
	rt = ++cnt;
	sum[rt] = sum[las] + 1;
	lch[rt] = lch[las];
	rch[rt] = rch[las];
	if(l == r) return;
	int m = l + r >> 1;
	if(p <= m) update(lson, p, lch[las]);
	else update(rson, p, rch[las]);
}

这里我们用的是动态开点,也就是对于每一个和前一棵树不同的点,都对它单独开一个新的空间。
很容易发现,我们每次 updateupdate,都会开 lognlogn 个空间,所以主席树的空间复杂度是 nlognnlogn

查询

查询部分的原理就是权值线段树!上面已经说过了!
直接看代码!

int find(int l, int r, int rt, int k, int las){
	if(l == r) return l;
	int s = sum[lch[rt]] - sum[lch[las]], m = l + r >> 1;
	if(k <= s) return find(lson, k, lch[las]);
	return find(rson, k - s, rch[las]);
}

sum[rt]sum[las]sum[rt] - sum[las] 就是 [l,r][l,r] 的权值线段树的节点的 sumsum 值!
如果你理解了上面的部分,这里应该很好理解!

最后补充一下主席树一般用到的一些东西

离散化

在一般的题目中,数据范围一般不是 [1,n][1,n],而是 intint 范围,于是我们要离散化,把数据映射到 [1,n][1,n] 范围。其实离散化就是用每个数的排名来代替这个数,要访问这个数只要在原来排序后的数组中访问下标就可以了。
也就是说,离散化后,我们用这个数的排名来代替这个数了!

离散化代码如下

	for(i = 1; i <= n; i++) scanf("%d", &a[i]), b[i] = a[i];
	sort(b + 1, b + i);
	m = unique(b + 1, b + n + 1) - b - 1;
	for(i = 1; i <= n; i++) a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;

uniqueunique 的作用是去重,mm 是去重后的数组有多少个数。

总代码如下

#include <bits/stdc++.h>
#define N 200005
#define lson l, m, lch[rt]
#define rson m + 1, r, rch[rt]
using namespace std;
int cnt, root[N], lch[N * 32], rch[N * 32], sum[N * 32], a[N], b[N];
void build(int l, int r, int &rt){
	rt = ++cnt;
	if(l == r) return;
	int m = l + r >> 1;
	build(lson);
	build(rson);
}
void update(int l, int r, int &rt, int p, int las){
	rt = ++cnt;
	sum[rt] = sum[las] + 1;
	lch[rt] = lch[las];
	rch[rt] = rch[las];
	if(l == r) return;
	int m = l + r >> 1;
	if(p <= m) update(lson, p, lch[las]);
	else update(rson, p, rch[las]);
}
int find(int l, int r, int rt, int k, int las){
	if(l == r) return l;
	int s = sum[lch[rt]] - sum[lch[las]], m = l + r >> 1;
	if(k <= s) return find(lson, k, lch[las]);
	return find(rson, k - s, rch[las]);
}
int main(){
	int i, j, n, m, l, r, k, T, q;
	//scanf("%d", &T);
	T = 1;
	while(T--){
		scanf("%d%d", &n, &q);
		memset(root, 0, sizeof(root));
		memset(lch, 0, sizeof(lch));
		memset(rch, 0, sizeof(rch));
		memset(sum, 0, sizeof(sum));
		cnt = 0;
		for(i = 1; i <= n; i++) scanf("%d", &a[i]), b[i] = a[i];
		sort(b + 1, b + i);
		m = unique(b + 1, b + n + 1) - b - 1;
		for(i = 1; i <= n; i++) a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
		build(1, m, root[0]);
		for(i = 1; i <= n; i++) update(1, m, root[i], a[i], root[i - 1]);
		for(i = 1; i <= q; i++){
			scanf("%d%d%d", &l, &r, &k);
			j = find(1, m, root[r], k, root[l - 1]);
			printf("%d\n", b[j]);
		}
	}
	return 0;
}

总结

主席树其实就是区间的权值线段树。
任何权值线段树能支持的查询操作主席树都能有
主席树时空复杂度都是 O(nlogn)O(nlogn)

完结撒花!!!