1. 程式人生 > >【poj 2104】K-th Number【整體二分+樹狀陣列】

【poj 2104】K-th Number【整體二分+樹狀陣列】

傳送門

Description

You are working for Macrohard company in data structures department. After failing your previous task about key insertion you were asked to write a new data structure that would be able to return quickly k-th order statistics in the array segment.
That is, given an array a[1…n] of different integer numbers, your program must answer a series of questions Q(i, j, k) in the form: “What would be the k-th number in a[i…j] segment, if this segment was sorted?”
For example, consider the array a = (1, 5, 2, 6, 3, 7, 4). Let the question be Q(2, 5, 3). The segment a[2…5] is (5, 2, 6, 3). If we sort this segment, we get (2, 3, 5, 6), the third number is 5, and therefore the answer to the question is 5.

Input

The first line of the input file contains n — the size of the array, and m — the number of questions to answer (1 <= n <= 100 000, 1 <= m <= 5 000).
The second line contains n different integer numbers not exceeding 109 by their absolute values — the array for which the answers should be given.
The following m lines contain question descriptions, each description consists of three numbers: i, j, and k (1 <= i <= j <= n, 1 <= k <= j - i + 1) and represents the question Q(i, j, k).

Output

For each question output the answer to it — the k-th number in sorted a[i…j] segment.

Sample Input

7 3
1 5 2 6 3 7 4
2 5 3
4 4 1
1 7 3

Sample Output

5
6
3

整體二分:

整體二分就是將所有詢問一起二分,然後獲得每個詢問的答案。
整體二分的過程實質上是個按照數值來劃分操作序列的過程。
對每一個詢問我們都需要判定一下,以決定它被劃分到哪一個答案的區間裡。這個判定過程就是通過比較比二分的mid大的數的個數和k。
以下摘自許昊然論文-《淺談資料結構題的幾個非經典解法》

詢問的答案具有二分性顯然是前提。我們發現,因為修改判定標準的貢獻相互獨立,且貢獻的值(如果有的話)與判定標準無關,所以如果我們已經計算過某一些修改對詢問的貢獻,那麼這個貢獻將永遠不會改變,我們沒有必要當判定標準改變時再次計算這些部分修改的貢獻,只要記錄下當前的總貢獻,在進一步二分時,直接加上新的貢獻即可。 

這樣處理的複雜度可以不再與序列總長度直接相關了,而可以只與當前處理的序列的長度相關。

時間複雜度:

定義T(C,S)表示待二分割槽間長度為C,待二分序列長度為S,不妨設單次處理時間複雜度O(f(n)),則有

T(C,S)=T(C/2,S0+T(C/2,SS0))+O(f(s))

解之得
T(C,n)O(f(n)logC)

思路

首先考慮一次詢問的情況,我們可以二分答案,然後通過驗證比答案大的數有多少個來不斷地縮小答案範圍直至得到一個準確的答案。而對於多個詢問我們同樣可以這麼做,只不過對每一個詢問我們都需要判定一下,以決定它被劃分到哪一個答案的區間裡。這個判定過程就是通過比較比mid大的數的個數和k。同時如果比二分的mid大的數的個數小於k了,我們是要去尋找小的答案,那麼這些比mid大的數在以後的遞迴裡始終會對答案有貢獻,所以我們沒必要去做重複的工作,只需要把這些數的個數累積到貢獻裡,以後遞迴的時候就不用考慮這些數。我們可以把數列裡的數也和詢問一起遞迴,這樣這些數也會被分到屬於的答案區間裡,並且只對相應區間裡的詢問有影響。
整體二分的過程實質上是個按照數值來劃分操作序列的過程,於是複雜度也就和操作序列的長度線性相關,那麼我們在中間維護一些資訊的時候,就一定不能有和數列長線性相關的東西,否則會破壞其時間複雜度。
具體的複雜度證明請見2013年集訓隊XHR論文。

程式碼

#include <bits/stdc++.h>
#define lowbit(x) (x&(-x))
#define INF 0x3f3f3f3f
#define N 100005
#define M 5005
using namespace std;
int n,m,pos;
int Max=-INF,Min=INF;
int id[N],ans[N],tmp[N];
bool mark[N];
struct DATA{
    int x,v;
    bool operator < (const DATA&r)const{return v<r.v;}
}data[N];
struct Ques{
    int l,r,k;
}q[M];
int tree[N];
inline void add(int x,int num){
    while(x<=n){
        tree[x]+=num;
        x+=lowbit(x);
    }
}
inline int search(int x){
    int re=0;
    while(x){
        re+=tree[x];
        x-=lowbit(x);
    }
    return re;
}
void solve(int l,int r,int L,int R){
    if(l>r || L==R)     return;
    int mid=(L+R)>>1;
    while(data[pos+1].v<=mid && pos<n){
        add(data[pos+1].x,1);
        ++pos;
    }
    while(data[pos].v>mid){
        add(data[pos].x,-1);
        --pos;
    }
    int cnt=0;
    for(int i=l;i<=r;++i){
        if(search(q[id[i]].r)-search(q[id[i]].l-1)>q[id[i]].k-1){
            ans[id[i]]=mid;
            mark[i]=1;
            ++cnt;
        }
        else    mark[i]=0;
    }
    int l1=l,l2=l+cnt;
    for(int i=l;i<=r;++i){
        if(mark[i])     tmp[l1++]=id[i];
        else            tmp[l2++]=id[i];
    }
    for(int i=l;i<=r;++i)       id[i]=tmp[i];
    solve(l,l1-1,L,mid);
    solve(l1,l2-1,mid+1,R);
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i){
        scanf("%d",&data[i].v);
        data[i].x=i;
        Max=max(Max,data[i].v);
        Min=min(Min,data[i].v);
    }
    sort(data+1,data+n+1);
    for(int i=1;i<=m;++i)       scanf("%d%d%d",&q[i].l,&q[i].r,&q[i].k);
    for(int i=1;i<=m;++i)       id[i]=i;
    solve(1,m,Min,Max+1);
    for(int i=1;i<=m;++i)       printf("%d\n",ans[i]);
    return 0;
}