当前位置 博文首页 > 文章内容

    权值线段树

    作者: 栏目:未分类 时间:2020-07-19 14:02:44

    本站于2023年9月4日。收到“大连君*****咨询有限公司”通知
    说我们IIS7站长博客,有一篇博文用了他们的图片。
    要求我们给他们一张图片6000元。要不然法院告我们

    为避免不必要的麻烦,IIS7站长博客,全站内容图片下架、并积极应诉
    博文内容全部不再显示,请需要相关资讯的站长朋友到必应搜索。谢谢!

    另祝:版权碰瓷诈骗团伙,早日弃暗投明。

    相关新闻:借版权之名、行诈骗之实,周某因犯诈骗罪被判处有期徒刑十一年六个月

    叹!百花齐放的时代,渐行渐远!



    权值线段树

    前置芝士

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

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

    概念

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

    我们来看这样一个数列:

    \[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;
    }