最基础权值线段树

整着整着主席树忽然发现(对于查询静态区间第k大的)前置知识好像没有说...

回来整权值线段树 \((QoQ)\)

权值线段树

声明:本知识点不需要一般线段树作为前置知识,可以视作与一般线段树分离开来的体系

然而精通一般线段树可以更加方便地理解权值线段树,而且一般线段树的应用也更广,

所以建议先学习一下一般线段树,当然不学...好像也可...

但是还是建议学一学...因为后面的操作跟一般线段树简直如出一辙,这样一来,倒不如先去学了应用更为广泛的一般线段树再来了解,一举两得,未尝不可.

前置知识:

  • 离散化.

离散化讲的就是只维护数的大小关系,不维护数值本身,

就是当我们不关心数值本身时,只记录每个数在(无重复元素)数列中的大小排名

例如:

数列\(1\ 100\ 3\)

将其离散化后只保留排名,成为以下数列:

\(1\ 3\ 2\)

解释一下,就是\(3\)是第二大的数,所以是\(2\),同理,\(100\)就是只能"摧眉折腰"当一个\(3\)

那么这有什么用呢?

有些题目需要根据数值大小关系开空间,然而并没有辣么大的空间给你用...

举个非常简单的例子:

  • 桶排序

我觉的桶排序都不会就不用看权值线段树了8...

就是对于值域内的每个元素开一个count记录其出现次数,没有为0,然后就完成了排序...

好像是O(N)的NB算法...然而空间大到飞起

这是我们只要维护每个数值在原数列的排名,然后基于此开数组,就好多了对不对?

那么这个东西这么好...

并不...其实应该问:

那么怎么实现呢?

下面给出比较简单的实现方法:
开一个vector动态数组,然后...

fuck我还要整动态数组...

就像这样:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
vector<int> u;
int getid(int x){
	return lower_bound(u.begin(),u.end(),x)-u.begin()+1;
}
int main(){
	for(i from 1 to n)
		u.push_back(read(ahaha));
	sort(u.begin(),u.end());
    u.erase(unique(u.begin(),u.end()),u.end());
	...
} 

其中:

\(getid\)函数是用来获取元素排名的,

\(lower\_bound\)函数是用来查询数组中大于等于x的数中最小的数的地址,做相应运算即可得到其排名,

\(sort\)排序不解释,当然如果有需求可以自己重载大于小于号或者自己写compare函数,

\(unique\)函数用来去重,用法是传入首尾地址即可,然后它返回的地址是去重之后有效数组的结尾地址的下一个地址

(头疼)

就是整理出的没用的数组的第一个地址,

然后用\(unique\)函数返回的地址作首,消掉后面的部分,剩下的就是有效数组

来模拟一下:

\(5\ 1\ 48\ 23\ 100\ 5\)

读取存入不解释...

进行排序\(\to\)

\(1\ 5\ 5\ 23\ 48\ 100\)

然后去重\(\to\)

\(1\ 5\ 23\ 48\ 100\ 5\)

顺便\(erase\)一下以真正去除无用数组,

\(1\ 5\ 23\ 48\ 100\)

对于\(getid\)操作,就是背过找到当前位置地址与首位置的差值,当然根据经验(植树问题)需要+1.

这样就方便地完成了离散化...

因为线段树啥的本身复杂度就是\(O(nlogn)\)的所以关于时间没什么好担心的

(所以我们可以借助\(O(nlogn)\)的复杂度的\(sort\)完成\(O(n)\)的排序...)

然而离散化并不是只有这种垃圾操作,而且这只是一种简单的实现而已,反正比手动模拟好写多了

前置芝士部分告一段落

下面是正式的权值线段树,

权值线段树的作用是什么呢?

其本身就是一个桶,用来统计并维护值域区间内某个数出现的次数,

所谓值域区间,就是第\(l\)小的数到第\(r\)小的数的区间,

这里究竟是第\(l\)(或\(r\))小还是大根据自己来定,本文主要是维护从小到大的区间的

说人话就是:维护的\(l\)小的数到第\(r\)小的数一共几个

先不要看这种操作有什么意义

所以说,对于权值线段树,我们可以不管数列原本的顺序,只看数字出现的次数,并根据其大小排名进行整理

具体实现原理如下:

  • 也就是说,对于树上的每个节点,最少只需维护一个信息:子树大小,就是当前区间内共有多少元素

  • 其原理就像是桶排序,对于第\(i\)个叶子节点,即区间长度为1的节点,其维护的是大小排名为i的数出现过多少次,

  • 对于非叶节点,其维护的就是该区间内元素的个数,

    对于非叶节点的合并,就是将区间内元素个数相加,作为这整个区间的元素个数,

    这样一来就完成了对于值域区间内元素个数的维护,

即:每个节点保存该区间内第\(l\)大的元素到第\(r\)大的元素的个数

那么有了这棵树,我们要实现什么,如何实现呢?

操作

因为权值线段树本身就不是什么需要修改的数据结构,所以现在所要求的就是掌握基本的查询操作

这里唯一的重要操作就是查询第\(k\)小值,

或是实现查询第\(l\)小到第\(r\)小的区间的元素个数(因为没怎么看过这样用的所以本篇博客并不整理)

这操作主要就是模拟,掌握思路还可以方便理解平衡树...

当然还有其他操作,比如最基本的插入操作,就是给第\(k\)小元素个数加1,其实思路差不多

\(insert\)插入操作

对区间进行二分然后插入,思路类似于线段树的单点修改,

具体思路就是如果当前区间大于(等于)目标排名,往左找,否则往右找,

inline void insert(ci k,ci l,ci r,ci v){
	if(l==r){
		sum[k]++;
		return ;
	}int mid=l+r>>1;
	if(v<=mid) insert(k<<1,l,mid,v);
	else insert(k<<1|1,mid+1,r,v);
	sum[k]=sum[k<<1]+sum[k<<1|1];
}

\(query\)查询第\(k\)大操作

好像也差不多吧...

inline int query(ci k,ci l,ci r,ci v){
	if(l==r) return sum[k];
	int mid=l+r>>1;
	if(v<=mid) return query(k<<1,l,mid,v);
	else return query(k<<1|1,mid+1,r,v);
}

总代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#define ci const int & //speed up!
using namespace std;
const int N=100005;
int n;
int a[N];
int sum[N];
vector<int> u;
inline int getid(ci v){
	return lower_bound(u.begin(),u.end(),v)-u.begin()+1;
}
inline void insert(ci k,ci l,ci r,ci v){
	if(l==r){
		sum[k]++;
		return ;
	}int mid=l+r>>1;
	if(v<=mid) insert(k<<1,l,mid,v);
	else insert(k<<1|1,mid+1,r,v);
	sum[k]=sum[k<<1]+sum[k<<1|1];
}
inline int query(ci k,ci l,ci r,ci v){
	if(l==r) return sum[k];
	int mid=l+r>>1;
	if(v<=mid) return query(k<<1,l,mid,v);
	else return query(k<<1|1,mid+1,r,v);
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		u.push_back(a[i]);
	}
	sort(u.begin(),u.end());
	u.erase(unique(u.begin(),u.end()),u.end());
	int m=u.size();
	for(int i=1;i<=m;i++)
		insert(1,1,m,getid(a[i]));
	... //something like luan qi ba zao de query
	printf("%d",query(1,1,m,getid(a[3]))); //query sum of number "a[3]"
	return 0;
}

权值线段树完成

对于其他操作如前驱,后继,可以考虑自己实现以下,按照思路模拟即可

然而权值线段树的较广泛应用...

还是可持久化线段树

posted @ 2019-12-23 17:26  _Alex_Mercer  阅读(210)  评论(3编辑  收藏  举报