权值线段树
权值线段树
前置芝士
顾名思义,权值线段树也算是一种线段树,它的本质也是线段树。所以在学习权值线段树之前,如果对普通线段树的掌握不太熟,可以先去这里去搜索线段树进行学习。
而权值线段树的进一步本质则是用线段树维护桶。同理,如果不知道桶是什么可以到这里进行搜索。
概念
我们知道,普通线段树维护的信息是数列的区间信息,比如区间和、区间最大值、区间最小值等等。在维护序列的这些信息的时候,我们更关注的是这些数本身的信息,换句话说,我们要维护区间的最值或和,我们最关注的是这些数统共的信息。而权值线段树维护一列数中数的个数。
我们来看这样一个数列:
一棵权值线段树的叶子节点维护的是“有几个1”,“有几个2”...,他们的父亲节点维护的是“有几个1和2”。
然后我们恍然大悟:这个东西就是我们刚刚说过的“桶”。
也就是说,我们的权值线段树就是用线段树维护了一堆桶。
这就是权值线段树的概念。
和普通线段树的区别
权值线段树维护的是桶,按值域开空间,维护的是个数。
简单线段树维护的是信息,按个数可开空间,维护的是特定信息。
用途
查询第k小或第k大。(注意:这里的第k大/小是整个序列的第k大/小,而子区间的第k大/小由主席树来解决)
查询某个数排名。
查询整个数组的排序。
查询前驱和后继(比某个数小的最大值,比某个数大的最小值)。
操作及模板
建树
#define ls root<<1 //左儿子
#define rs root<<1|1 //右儿子
int tr[maxn]; //权值线段树数组
void build(int root,int l,int r)
{
if(l==r)
{
tr[root]=a[i]//a[i]表示数i有几个
return ;
}
int mid=(l+r)>>1;
build(ls,l,mid);
build(rs,mid+1,r);
tr[root]=tr[ls]+tr[rs];
}
修改(单个数出现次数+1)
void update(int root,int l,int r,int pos)//当前区间位置 l r,节点 root 位置 pos
{
if(l==r) tr[root]++;
int mid=(l+r)>>1;
if(pos<=mid) update(ls,l,mid,pos);
else update(rs,mid+1,r,pos);
tr[root]=tr[ls]+tr[rs];
}
查询
int query(int root,int l,int r,int k)//查询第k个数出现了几次
{
if(l==r) return tr[root];
int mid=(l+r)>>1;
if(k<=mid) return query(ls,l,mid,k);
else return query(rs,mid+1,r,k);
}
查询区间[x,y]中的数
int query(int root,int l,int r,int x,int y)
{
if(l==x&&r==y) return tr[root];
int mid=(l+r)>>1;
if(y<=mid) query(ls,l,mid,x,y);
else if(x>mid) return query(rs,mid+1,r,x,y);
else return query(ls,l,mid,x,mid)+query(rs,mid+1,r,mid+1,y);
}
查询所有数的第k大值
这是权值线段树的核心,思想如下:
到每个节点时,如果右子树的总和大于等于k,说明第k大值出现在右子树中,则递归进右子树;否则说明此时的第k大值在左子树中,则递归进左子树,注意:此时要将k的值减去右子树的总和。
为什么要减去?
如果我们要找的是第7大值,右子树总和为4,7−4=3 ,说明在该节点的第7大值在左子树中是第3大值。
最后一直递归到只有一个数时,那个数就是答案。
int kth(int root,int l,int r,int k)
{
if(l==r) return l;
int mid=(l+r)>>1,sum=tr[rs];//sum代表右子树的总和
if(k<=sum) return kth(rs,mid+1,r,k);
else return kth(ls,l,mid,k);
}
查询第k小值同理。请读者自行理解并写出模板。
模板题
HDU - 1394
题意
给你一个序列,你可以循环左移,问最小的逆序对是多少?
思路
逆序对其实是寻找比这个数小的数字有多少个,这个问题其实正是权值线段树所要解决的
我们把权值线段树的单点作为1-N的数中每个数出现的次数,并维护区间和,然后从1-N的数,在每个位置,查询比这个数小的数字的个数,这就是当前位置的逆序对,然后把当前位置数的出现的次数+1,就能得到答案。
然后我们考虑循环右移。我们每次循环右移,相当于把序列最左边的数字给放到最右边,而位于序列最左边的数字,它对答案的功效仅仅是这个数字大小a[i]-1,因为比这个数字小的数字全部都在它的后面,并且这个数字放到最后了,它对答案的贡献是N-a[i],因为比这个数字大数字全部都在这个数字的前面,所以每当左移一位,对答案的贡献其实就是
Ans=Ans-(a[i]-1)+n-a[i]
由于数字从0开始,我们建树从1开始,我们把所有数字+1即可
代码
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#define ls i<<1
#define rs i<<1|1
using namespace std;
const int maxn = 5005;
int tr[maxn<<2],a[maxn];
inline void update(int i,int l,int r,int pos){
if (l==r){
tr[i]++;
return;
}
int mid=(l+r)>>1;
if (pos<=mid){
update(ls,l,mid,pos);
}else {
update(rs,mid+1,r,pos);
}
tr[i]=tr[ls]+tr[rs];
}
inline int query(int i,int l,int r,int L,int R){
if (L<=l && r<=R) return tr[i];
int mid=(l+r)>>1;
if (R<=mid){
return query(ls,l,mid,L,R);
}else if (L>mid){
return query(rs,mid+1,r,L,R);
}else {
return query(ls,l,mid,L,R)+query(rs,mid+1,r,L,R);
}
}
int main(){
int n;
while(~scanf("%d",&n)){
int ans=0;
memset(tr,0,sizeof(tr));
for (int i=1;i<=n;i++){
scanf("%d",&a[i]);
a[i]++;
ans+=query(1,1,n,a[i],n);
update(1,1,n,a[i]);
}
int minn=ans;
for (int i=1;i<=n;i++){
ans=ans+(n-a[i]+1)-a[i];
minn=min(ans,minn);
}
printf("%d\n",minn);
}
return 0;
}