线段树之主席树(POJ-2104)

线段树之主席树(POJ-2104)

本文主要介绍静态不带修改的主席树,动态修改的主席树以后有机会补上(还是因为自己太菜了…)

一、主席树入门分析

首先主席树的最经典应用,就是求动态区间的第k小元素/第k大元素

这类问题又分为两种,一是不允许修改,即本文将要讨论的,一是允许修改的。

前者只需要用到静态的主席树就足够解决,而后者则需要配合树状数组形成动态的主席树来解决。

先来看看如果使用普通的线段树来解决动态区间的第k小元素该如何解决:

如果这个问题的区间是静态的,那么使用一颗普通的权值线段树就可以解决,但是是动态区间的话,我们就需要对要查询的区间 [left, right] 重新建树,然后再进行查询,是不是觉得很麻烦?而且很浪费空间!

而使用主席树的优点在于:

主席树可以保存线段树的历史版本,或者说可以回到之前的操作,而且主席树不需要每次都重新创建一颗全新的线段树,而可以利用之前实现的线段树,在其基础上保留不变的结点,新增改变的结点

二、图示分析

假设我们需要求序列 {5, 1, 3, 4, 2} 的区间第 k 小元素

那么先建立一棵只有根节点的线段树,如下:

在这里插入图片描述

扫描二维码关注公众号,回复: 9255076 查看本文章

表示数字在 [1, 5] 之中的数量为 0 。

接下来加入第一个元素 5,

在这里插入图片描述

可以看到前面这棵只有根节点的树对于这棵树的建立没有任何贡献,继续加入 1,

在这里插入图片描述

这里就需要注意了,新的线段树左孩子需要重新创建,但是右孩子没有发生改变,因此就没有必要重新创建了,可以利用上一个线段树,如下:

在这里插入图片描述

这里看上去很扭曲,但其实可以认为和普通的线段树的结构是一样的,像这样:

在这里插入图片描述

我们再走一步,加入 3,

在这里插入图片描述

这样相当于只新建了 3 个结点就创建了一个新的线段树如下:

在这里插入图片描述

处理完我们就可以得到 5 棵“不完整的线段树”(第一棵只有根节点的线段树忽略),分别统计数组 [1, i] 中多少元素处于 [1, 5] 这个区间中(注意理解这句话)。

现在我们来考虑区间第 k 小的问题,假设我们要求区间 [2, 4] 中的 k = 2 的元素,区间 [2, 4] 表示的序列为 {1, 3, 4},显而易见第 2 小的元素为 3,可是我们利用刚刚求的这么多线段树呢?

可以联系前缀和的概念,这里的权值线段树也是满足相减的性质的。

第 4 棵与第 1 棵线段树如下:

在这里插入图片描述

首先需要判断这个区间中是否存在第 k 小的元素,两个根节点的值相减 4-1 = 3 >= 2,因此存在第 k 小的元素。

然后我们需要判断第 k 小的元素存在于哪个孩子中,当前的结点是根节点,因此 check 根节点的左孩子,分别为 2 和 0,相减 2-0 = 2 >= 2,因此左区间中一定存在两个元素,那么第 k 小的元素一定存在于左孩子中,重新把左孩子当做根节点递归 check。

此时左孩子 1-0 = 1 < 2,因此右孩子一定存在于右孩子中,将右孩子当做根节点递归 check。

这时候发现我们已经走到了叶子节点,说明已经找到了第 k 小元素,即 3 。

至此,主席树查找一次区间第 k 小元素的过程已经结束,不知道我讲清楚没有…

三、编码实现

要完成上面的工作其实有一点很重要,但是没有说的,就是编码,我们要给每个节点进行编码,这样我们才能完成节点的复用!除此以外,还要另起数组保存每棵线段树的根节点,这样我们才能方便调用每个线段树。

这里给出一道模板题目:

K-th Number POJ - 2104

题目大意:给定长度为 n 的整型序列,以及 m 个询问查询区间第 k 小元素。

sample input:

7 3
1 5 2 6 3 7 4
2 5 3
4 4 1
1 7 3

sample output:

5
6
3

hint:

大数据量,推荐用 scanf 和 printf 。

就是一道裸题,思路跟上面讲得是一样的,主要理解代码是如何组织的。

参考代码:

// 不带修改的主席树模板
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
using namespace std;

const int maxn = 1e5+10;
// arr 保存整型序列,afterSortArr 保存离散化排序过后的整型序列
int arr[maxn], afterSortArr[maxn];
// root 保存每棵线段树根节点的编码
int root[maxn];
// cnt 用于编码
int cnt;

struct Node {
	int left, right;
	int sum;
    // ls 和 rs 分别保存左右孩子的编码
	int ls, rs;
} tree[20*maxn];  // 这道题目粗略计算大概 20 倍就可以解决,其他的题目自行估计一下...

// 建树同时编号
void build(int &rt, int l, int r) {
	rt = cnt++;
	tree[rt].left = l;
	tree[rt].right = r;
	if(l == r) {
		return;
	}
	int mid = (l+r)/2;
	build(tree[rt].ls, l, mid);
	build(tree[rt].rs, mid+1, r);
}

// 插入同时编号
void insert(int pre, int &rt, int x) {
	rt = cnt++;
    // 这里就是复用上一棵线段树 pre 的过程
	tree[rt].left = tree[pre].left;
	tree[rt].right = tree[pre].right;
	tree[rt].ls = tree[pre].ls;
	tree[rt].rs = tree[pre].rs;
	tree[rt].sum = tree[pre].sum+1;
	if(tree[rt].left == tree[rt].right) {
		return;
	}
	int mid = (tree[rt].left+tree[rt].right)/2;
	if(x <= mid) insert(tree[pre].ls, tree[rt].ls, x);
	else insert(tree[pre].rs, tree[rt].rs, x);
}

// 查询区间第 k 小元素
int query(int pre, int rt, int k) {
	if(tree[rt].left == tree[rt].right) {
        // 找到了叶子节点说明已经找到了第 k 小元素
		return afterSortArr[tree[rt].left];
	}
    // 判断左孩子的差值与 k 的关系,进而选择递归查询哪个区间
	int diff = tree[tree[rt].ls].sum - tree[tree[pre].ls].sum;
	if(diff >= k) return query(tree[pre].ls, tree[rt].ls, k);
	else return query(tree[pre].rs, tree[rt].rs, k-diff);
}

int main() {
	int n, m;
	while(scanf("%d%d", &n, &m) == 2) {
		for(int i = 1; i <= n; i++) {
			scanf("%d", &arr[i]);
			afterSortArr[i] = arr[i];
		}
		sort(afterSortArr+1, afterSortArr+1+n);
		cnt = 0;
        // 先建一棵空树
		build(root[0], 1, n);
		for(int i = 1; i <= n; i++) {
            // 查询离散化过后该数字 a[i] 的位置
			int pos = lower_bound(afterSortArr+1, afterSortArr+1+n, arr[i])-afterSortArr;
            // 需要使用上一棵线段树与当前需要建立的线段树的编码
			insert(root[i-1], root[i], pos);
		}
		for(int i = 0; i < m; i++) {
			int l, r, k;
			scanf("%d%d%d", &l, &r, &k);
			// 注意是 l-1
			int ans = query(root[l-1], root[r], k);
			printf("%d\n", ans);
		}
	}
	return 0;
}

【END】感谢观看!

发布了44 篇原创文章 · 获赞 17 · 访问量 9099

猜你喜欢

转载自blog.csdn.net/qq_41765114/article/details/90181320