权值线段树

权值线段树

前置芝士

​ 顾名思义,权值线段树也算是一种线段树,它的本质也是线段树。所以在学习权值线段树之前,如果对普通线段树的掌握不太熟,可以先去这里去搜索线段树进行学习。

​ 而权值线段树的进一步本质则是用线段树维护桶。同理,如果不知道桶是什么可以到这里进行搜索。

概念

​ 我们知道,普通线段树维护的信息是数列的区间信息,比如区间和、区间最大值、区间最小值等等。在维护序列的这些信息的时候,我们更关注的是这些数本身的信息,换句话说,我们要维护区间的最值或和,我们最关注的是这些数统共的信息。而权值线段树维护一列数中数的个数

我们来看这样一个数列:

\[1~1~1~2~3~3~4~5~5~6~6~7 \]

一棵权值线段树的叶子节点维护的是“有几个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;
}
posted @ 2020-07-19 13:53  jasony_sam  阅读(202)  评论(3编辑  收藏  举报