權值線段樹
權值線段樹
前置芝士
顧名思義,權值線段樹也算是一種線段樹,它的本質也是線段樹。所以在學習權值線段樹之前,如果對普通線段樹的掌握不太熟,可以先去這裡去搜索線段樹進行學習。
而權值線段樹的進一步本質則是用線段樹維護桶。同理,如果不知道桶是什麼可以到這裡進行搜尋。
概念
我們知道,普通線段樹維護的資訊是數列的區間資訊,比如區間和、區間最大值、區間最小值等等。在維護序列的這些資訊的時候,我們更關注的是這些數本身的資訊,換句話說,我們要維護區間的最值或和,我們最關注的是這些數統共的資訊。而權值線段樹維護一列數中數的個數。
我們來看這樣一個數列:
\[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;
}