详解权值线段树
详解权值线段树
本篇随笔详细讲解一下算法竞赛中的一种数据结构——权值线段树。
前置知识
在讲解权值线段树之前,我们首先要明确:权值线段树属于一种线段树,它的本质仍然是线段树。所以在学习权值线段树之前,如果还对普通线段树并没有一个深刻的了解的话,请先移步这篇博客来学习简单线段树。
以及,权值线段树的本质是线段树维护桶。这个桶到底是什么呢?如果读者对桶的概念和应用比较模糊的话,请移步这篇博客来学习桶的基本概念和应用:
权值线段树的概念
那么,在了解了所有的前置知识之后,理解权值线段树便变得容易许多了。
我们知道,普通线段树维护的信息是数列的区间信息,比如区间和、区间最大值、区间最小值等等。在维护序列的这些信息的时候,我们更关注的是这些数本身的信息,换句话说,我们要维护区间的最值或和,我们最关注的是这些数统共的信息。而权值线段树维护一列数中数的个数。
我们来看这样一个数列:
一棵权值线段树的叶子节点维护的是“有几个1”,“有几个2”...,他们的父亲节点维护的是“有几个1和2”。
然后我们恍然大悟:这个东西就是我们刚刚说过的“桶”。
也就是说,我们的权值线段树就是用线段树维护了一堆桶。
这就是权值线段树的概念。
权值线段树和简单线段树的区别
权值线段树维护的是桶,按值域开空间,维护的是个数。
简单线段树维护的是信息,按个数可开空间,维护的是特定信息。
权值线段树的用途
权值线段树可以解决数列第k大/小的问题。
这里要注意!我们只能对给定数列解决整个数列的第k大/小,并不能解决数列的子区间的第k大/小。
(剧透一下,解决数列子区间的第k大/小需要主席树(可持久化线段树),因为蒟蒻还不会,以后会的时候可能会补讲解,希望大家海涵)
一些直觉强的读者可能已经隐约体会到权值线段树是如何维护数列第k大/小了。没错,因为我们的权值线段树维护的是一堆桶,每个节点储存的是节点维护区间(就是值域)的数出现的次数(再次强调定义),那么,根据这个定义,整棵线段树的根节点就表示整个值域有几个数。对于整列数中第k大/小的值,我们从根节点开始判断(这里按第k大为例),如果k比右儿子大,就说明第k大的数在左儿子所表示的值域中。然后,k要减去右儿子。(这是因为继续递归下去的时候,正确答案,整个区间的第k大已经不再是左儿子表示区间的第k大了,很好理解)。
依次递归下去,到了叶子节点的时候,叶子节点就是我们要找的正确答案。
权值线段树的相关操作及权值线段树模板
建树
#define lson pos<<1
#define rson pos<<1|1
void build(int pos,int l,int r)
{
int mid=(l+r)>>1;
if(l==r)
{
tree[pos]=a[l];//a[l]表示数l有多少个
return;
}
build(lson,l,mid);
build(rson,mid+1,r);
tree[pos]=tree[lson]+tree[rson];
}
修改(插入)
void update(int pos,int l,int r,int k,int cnt)//表示数k的个数多cnt个
{
int mid=(l+r)>>1;
if(l==r)
{
tree[pos]+=cnt;
return;
}
if(k<=mid)
update(lson,l,mid,k,cnt);
else
update(rson,mid+1,r,k,cnt);
tree[pos]=tree[lson]+tree[rson];
}
查询
int query(int pos,int l,int r,int k)//查询数k有多少个
{
int mid=(l+r)>>1;
if(l==r)
return tree[pos];
if(k<=mid)
return query(lson,l,mid,k);
else
return query(rson,mid+1,r,k);
}
查询第K大/小值
int kth(int pos,int l,int r,int k)//查询第k大值是多少
{
int mid=(l+r)>>1;
if(l==r)
return l;
if(k<mid)
return kth(rson,mid+1,r,k);
else
return kth(lson,l,mid,k-mid);
}
查询k小值同理。请读者根据自行理解再写出查询K小值的模板,就当作本次学习的练习吧!
权值线段树的相关内容大约就是这样。感谢大家的观看和支持。