最基础权值线段树
整着整着主席树忽然发现(对于查询静态区间第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;
}
权值线段树完成
对于其他操作如前驱,后继,可以考虑自己实现以下,按照思路模拟即可
然而权值线段树的较广泛应用...
还是可持久化线段树吧