权值线段树

定义:

权值线段树,基于普通线段树,但是不同。

举个栗子:对于一个给定的数组,普通线段树可以维护某个子数组中数的和,而权值线段树可以维护某个区间内数组元素出现的次数。

在实现上,由于值域范围通常较大,权值线段树会采用离散化或动态开点的策略优化空间。单次操作时间复杂度o(logn

权值线段树的节点用来表示一个区间的数出现的次数  例如: 数 1和2 分别出现3次和5次,则节点1记录 3,节点2 记录5, 1和2的父节点记录它们的和8 .

存储结构

堆式存储:rt ,l, r,      rt<<! , l, m     rt<<1|1  ,m+1, r 

结点式存储 struct Node { int sum ,l , r :};

基本作用:

查询第k小或第k大。

查询某个数排名。

查询整干数组的排序。

查询前驱和后继(比某个数小的最大值,比某个数大的最小值)

基本操作:

单点修改 (单个数出现的次数+1)

    void update(int l,int r,int rt,int pos) // 当前区间范围 l r 节点 rt    位置 pos 
    {
        if(l==r) t[rt]++;
        else
        {
            int mid=(l+r)/2;
            if(pos<=mid) add(l,mid,rt*2,pos); else add(mid+1,r,rt*2+1,pos);
            t[rt]=t[rt*2]+t[rt*2+1];
        {
    }

查询一个数出现的次数

    int query(int l,int r,int rt,int pos)
    {
        if(l==r) return t[rt];
        else
        {
            int mid=(l+r)/2;
            if(pos<=mid) return find(l,mid,rt*2,pos); else return find(mid+1,r,rt*2+1,pos);
        }
    }

查询一段区间数出现的次数 查询区间 【x,y]

递归+二分

    int query(int l,int r,int rt,int x,int y)
    {
        if(l==x&&r==y) return t[rt];
        else
        {
            int mid=(l+r)/2;
            if(y<=mid) return find(l,mid,rt*2,x,y);
            else if(x>mid) return find(mid+1,r,rt*2+1,x,y);
            else return find(l,mid,rt*2,x,mid)+find(mid+1,r,rt*2+1,mid+1,y);
        }
    }

查询所有数的第k大值
这是权值线段树的核心,思想如下:
到每个节点时,如果右子树的总和大于等于k kk,说明第k kk大值出现在右子树中,则递归进右子树;否则说明此时的第k kk大值在左子树中,则递归进左子树,注意:此时要将k kk的值减去右子树的总和。
为什么要减去?
如果我们要找的是第7 77大值,右子树总和为4 44,7−4=3 7-4=37−4=3,说明在该节点的第7 77大值在左子树中是第3 33大值。
最后一直递归到只有一个数时,那个数就是答案。

    int kth(int l,int r,int rt,int k)
    {
        if(l==r) return l;
        else
        {
            int mid=(l+r)/2,s1=f[rt*2],s2=f[rt*2+1];
            if(k<=s2) return kth(mid+1,r,rt*2+1,k); else return kth(l,mid,rt*2,k-s2);
        }
    }

 

模板题:

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<string.h>
#include<algorithm>
#include<stdio.h>
using namespace std;
const int maxx = 5005;
int tree[maxx<<2];
inline int L(int root){return root<<1;};
inline int R(int root){return root<<1|1;};
inline int MID(int l,int r){return (l+r)>>1;};
int a[maxx];
void update(int root,int l,int r,int pos){
   if (l==r){
     tree[root]++;
     return;
   }
   int mid=MID(l,r);
   if (pos<=mid){
      update(L(root),l,mid,pos);
   }else {
      update(R(root),mid+1,r,pos);
   }
   tree[root]=tree[L(root)]+tree[R(root)];
}
int query(int root,int l,int r,int ql,int qr){
     if (ql<=l && r<=qr){
        return tree[root];
     }
     int mid=MID(l,r);
     if (qr<=mid){
        return query(L(root),l,mid,ql,qr);
     }else if (ql>mid){
        return query(R(root),mid+1,r,ql,qr);
     }else {
        return query(L(root),l,mid,ql,qr)+query(R(root),mid+1,r,ql,qr);
     }
}
int main(){
  int n;
  while(~scanf("%d",&n)){
    int ans=0;
    memset(tree,0,sizeof(tree));
    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;
}
View Code

 

进阶知识 主席树:https://www.cnblogs.com/young-children/p/11787490.html

参考博客:https://blog.csdn.net/qq_39565901/article/details/81782611

posted @ 2019-11-03 15:51  Young-children  阅读(7964)  评论(3编辑  收藏  举报