整着整着主席树忽然发现(对于查询静态区间第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)
读取存入不解释...
进行排序( o)
(1 5 5 23 48 100)
然后去重( o)
(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;
}
权值线段树完成
对于其他操作如前驱,后继,可以考虑自己实现以下,按照思路模拟即可
然而权值线段树的较广泛应用...
还是可持久化线段树吧