1. 程式人生 > 實用技巧 >權值線段樹

權值線段樹

權值線段樹

前置芝士

​ 顧名思義,權值線段樹也算是一種線段樹,它的本質也是線段樹。所以在學習權值線段樹之前,如果對普通線段樹的掌握不太熟,可以先去這裡去搜索線段樹進行學習。

​ 而權值線段樹的進一步本質則是用線段樹維護桶。同理,如果不知道桶是什麼可以到這裡進行搜尋。

概念

​ 我們知道,普通線段樹維護的資訊是數列的區間資訊,比如區間和、區間最大值、區間最小值等等。在維護序列的這些資訊的時候,我們更關注的是這些數本身的資訊,換句話說,我們要維護區間的最值或和,我們最關注的是這些數統共的資訊。而權值線段樹維護一列數中數的個數

我們來看這樣一個數列:

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