1. 程式人生 > >歸併樹(POJ 2104 K-th Number)

歸併樹(POJ 2104 K-th Number)

        在求解區間第k個數的問題,除了劃分樹以外我們還可以使用另一種高效的方法 ------ 歸併樹。

1、演算法描述

        所謂歸併樹,就是利用線段樹的建樹過程,將歸併排序的過程儲存。(不會線段樹:here,不會歸併排序:here)。在說明歸併樹之前我們先看看這樣的一個問題:

給定一個序列A[1...n],現在對該序列能夠進行一個操作:Query(left, right, k):表示查詢區間[left, right]的第k大樹。一共進行m次的查詢,要求你儘可能快的實現該操作

        一看到這個問題,我們可以立馬得到一個樸素的演算法:每次對區間[left, right]進行從小到大排序,直接輸出第k大的值。我們稍稍分析一下這種樸素的演算法所需的時間複雜度。首先每次對區間[left, right]進行排序所需的時間為O(nlogn)。一共進行m次查詢,那麼其總的時間複雜度為O(mnlogn)。第一眼看上去感覺這個演算法貌似還不錯,仔細分析就能發現,一旦m和n同時很大,那麼這個演算法就會很慢。那麼此時,我們應該思考有麼有更快的演算法。顯然是有的,他就是今天我們要講的歸併樹(當然還有其他方法,下次再介紹)。

         歸併樹簡單來說就是線段樹加歸併排序,那麼他們是怎麼結合在一起的呢?假定現在A[1...7] = {7,6,....,1},我們先看看這個該序列歸併排序的過程把:


        我們在文章前也提到我們是利用線段樹的儲存結構來儲存歸併排序的過程,所以我們對照的看一下線段樹的結構:


        這麼一對比我們會發現線段樹的儲存結構與歸併排序的過程高度的一致,於是我們將兩者合併:


        通過上邊的分析,我們不難得出歸併樹的建樹過程,程式碼如下:

#define MAXN (1<<18)
#define DEEP 20
//sorted用來儲存歸併樹的樹體,a為輸入序列
int sorted[DEEP][MAXN], a[MAXN];
void build(int deep, int lft, int rht){
    if(lft == rht){
        sorted[deep][lft] = a[lft];
        return ;
    }
    int mid = (lft + rht) >> 1;
    build(deep+1, lft, mid);
    build(deep+1, mid+1, rht);
    //進行歸併
    int i = lft, j = mid+1, k = lft;
    while(i <= mid && j <= rht){
        if(sorted[deep+1][i] <= sorted[deep+1][j])
            sorted[deep][k++] = sorted[deep+1][i++];
        else sorted[deep][k++] = sorted[deep+1][j++];
    }
    while(i <= mid) sorted[deep][k++] = sorted[deep+1][i++];
    while(j <= rht) sorted[deep][k++] = sorted[deep+1][j++];
}

        通過程式碼我們發現,歸併樹採用層狀結構了。原因很簡單,因為要將歸併排序的整個過程完完整整的儲存下來,使用以前的樹狀結構顯然不能滿足。因為此時每一層都具有n個元素。現在我們已經完成了建樹的工作,現在還有一個大問題就是查詢。那麼我們應該怎麼查呢?一個區間的第k個數就是答案嗎?只要稍微一思考就能發現這個思路不對。其實這時候我們可以使用一個比較“笨”的方法 --- 試一下唄。那麼怎麼試呢?我們用可以把[1,n]區間內的所有數都帶入區間[left,right]中,看看那個數滿足是這個區間的第k大數。顯然我們這麼做的話,我們可能會得到很多個數,假定我們從[1,n]區間內找到了n1個數且按照從小到大的順序儲存在key[n1]陣列中。

        那麼此時我們怎麼確定這n1個數,哪一個在區間[left,right]之中呢?一眼望去好像沒什麼方法貌似。那麼作為笨的人,我們就採用一種比較笨的方法吧。假設x為區間內的第k大的數從key[n1]內任取一個數y。那麼我們分類討論:

        ①當y > x時,如果存在這種情況,那麼y為區間[left,right]內第k+1大的數,顯然y不可能出現在這n1個數。

        ②當y <= x時,這時候y都滿足情況,那麼根據①的分析,我們可以發現x是這n1個數中最大的那一個。

        綜上所述,當區間[1,n]內有多個數滿足為區間[left,right]的第k大數時,值最大的那一個為目標值,即區間的第k大數。

        經過上述分析,我們可以得到如下的程式碼:

//查詢小於key的個數
//[qlft,qrht]為查詢區間
//[lft, rht]為歸併樹上的區間
int query(int deep, int lft, int rht, int qlft, int qrht, int key){
    if(qrht < lft || qlft > rht) return 0;
    if(qlft <= lft && rht <= qrht)
        return std::lower_bound(&sorted[deep][lft], &sorted[deep][rht]+1, key) - &sorted[deep][lft];
    int mid = (lft + rht) >> 1;
    return query(deep+1, lft, mid, qlft, qrht, key) + query(deep+1, mid+1, rht, qlft, qrht, key);
}
//二分查詢在區間[qlft, qrht]上滿足是第k大的數
//換而言之,即滿足和比key小的有k個數的key
int solve(int n, int qlft, int qrht, int k){
	int low = 0, high = n;
	while(low+1 < high){
		int mid= (low + high) >> 1;
		//cnt 為小於 sorted[0][mid]的個數
		int cnt = query(0, 0, n-1, qlft, qrht, sorted[0][mid]);
		if(cnt <= k) low = mid; //[mid, high)
		else high = mid; //[low, mid)
	}
	return sorted[0][low];
}

2、時間複雜度

        通過上述程式碼我們可以發現,影響歸併樹的時間複雜度的地方只有兩處①建樹②查詢。且這兩處的時間複雜度相互獨立。我們先分析建樹過程的時間複雜度吧,每一層都對n個數進行了操作,一用logn + 1層。於是建樹的時間複雜度為O(nlogn)。我們再分析每一次查詢操作的時間複雜度,①二分的對[1,n]內的數進行試,找出滿足第k大的數②遞迴的對歸併樹上的區間進行進行查詢③在每一個區間上查詢比key小的元素個數。於是我們可以發現我們一共進行上次操作且每一次操作均為logn,故時間複雜度為O(logn*logn*logn)。一共進行m次查詢那麼總的時間複雜度為O(m*logn*logn*logn)。故歸併樹的最終時間複雜度為O(max(n*logn, m*logn*logn*logn))。

        文章的最後,附上一道模板題POJ2104K-th Number。最後的最後附上解題程式碼,如下:

#include <cstdio>
#include <algorithm>
#define MAXN (100000)
#define DEEP (20)
using namespace std;
int sorted[DEEP][MAXN], a[MAXN];
void build(int deep, int lft, int rht){
    if(lft == rht){
        sorted[deep][lft] = a[lft];
        return ;
    }
    int mid = (lft + rht) >> 1;
    build(deep+1, lft, mid);
    build(deep+1, mid+1, rht);
    int p = lft, q = mid+1, k = lft;
    while(p <= mid && q <= rht){
        if(sorted[deep+1][p] <= sorted[deep+1][q])
            sorted[deep][k++] = sorted[deep+1][p++];
        else sorted[deep][k++] = sorted[deep+1][q++];
    }
    while(p <= mid) sorted[deep][k++] = sorted[deep+1][p++];
    while(q <= rht) sorted[deep][k++] = sorted[deep+1][q++];
}
int query(int deep, int lft, int rht, int qlft, int qrht, int key){
    if(qrht < lft || qlft > rht) return 0;
    if(qlft <= lft && rht <= qrht)
        return lower_bound(&sorted[deep][lft], &sorted[deep][rht]+1, key) - &sorted[deep][lft];
    int mid = (lft + rht) >> 1;
    return query(deep+1, lft, mid, qlft, qrht, key) + query(deep+1, mid+1, rht, qlft, qrht, key);
}
int solve(int n, int qlft, int qrht, int k){
	int low = 1, high = n+1;
	while(low+1 < high){
		int mid= (low + high) >> 1;
		int cnt = query(0, 1, n, qlft, qrht, sorted[0][mid]);
		if(cnt <= k) low = mid;
		else high = mid;
	}
	return sorted[0][low];
}
int main(){
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++){
        scanf("%d", &a[i]);
    }
    build(0, 1, n);
    while(m--){
        int qlft, qrht, k;
        scanf("%d%d%d", &qlft, &qrht, &k);
        printf("%d\n", solve(n, qlft, qrht, k-1));
    }
    return 0;
}


相關推薦

歸併(POJ 2104 K-th Number)

        在求解區間第k個數的問題,除了劃分樹以外我們還可以使用另一種高效的方法 ------ 歸併樹。 1、演算法描述         所謂歸併樹,就是利用線段樹的建樹過程,將歸併排序的過程儲存。(不會線段樹:here,不會歸併排序:here)。在說明歸併樹之前我們

POJ 2104 K-th Number(區間第k大數)(平方切割,歸並,劃分

ac代碼 deb rank turn tracking line 查看 div 能夠 題目鏈接: http://poj.org/problem?id=2104 解題思路: 由於查詢的個數m非常大。樸素的求法無法在規定時間內求解。因此應該選用合理的方式維護數據來做到高效

POJ 2104 K-th Number(主席

ber sca first n) 次數 example == scan sorted K-th Number Time Limit: 20000MS Memory Limit: 65536K Total Submissions: 5742

POJ 2104 K-th Number (主席)

std +++ esp space ctype == string uniq upd 題意:給定一個序列,然後有 q 個詢問,每次詢問 l - r 區間內的第 k 大的值。 析:很明顯的主席樹,而且還是裸的主席樹,先進行離散化,然後用主席樹進行查詢就好。 代碼如下: #p

Poj 2104 K-th Number 主席模版題

OS tdi pda sig signed begin ostream air color 題意:離線詢問[l,r]區間第k大 題解:模版題,入門題 #include <iostream> #include <cstdio> #inclu

poj 2104 K-th Number (主席入門模板題)

摘抄了一段主席樹的解釋:所謂主席樹呢,就是對原來的數列[1..n]的每一個字首[1..i](1≤i≤n)建立一棵線段樹,線段樹的每一個節點存某個字首[1..i]中屬於區間[L..R]的數一共有多少個(比如根節點是[1..n],一共i個數,sum[root]

POJ 2104 K-th Number(主席,區間第K大的數)

Description You are working for Macrohard company in data structures department. After failing your previous task about key insertion you

poj 2104 K-th Number 區間第K大 二分 離散化 + (莫隊 狀陣列/平方分解/線段)

題目 題解 比較經典的題目,我用了三個方法來寫。 其中第一種第三種我覺得比較好寫,第二種出現了各種問題。 總的來說第一種速度快,但是程式碼長,第三種速度慢一些,但是程式碼比較短,第二種程式碼和第三種差不多,但是慢了很多,寫起

POJ 2104 K-th Number 主席模板題

題意:給定一個長度為n的無序陣列,然後有m組詢問l r k,求區間[l,r]中的第k大數 思路:以前用劃分樹做過,現在學習主席樹,模板題。個人對主席樹的理解:就是把線段樹更新過程中所有的歷史狀態記錄下來,例如更新m次的話,那麼就會有m種狀態,直接建m棵線段樹的話,時間和空

POJ 2104 K-th Number [主席入門]【資料結構】

題目連結:http://poj.org/problem?id=2104 ———————————————————————————————————————————————— K-th Number Time Limit: 20000MS Memory

主席(可持久化線段)講解 [POJ 2104] K-th Number

題目大意:本題包含多組資料。每組資料都會給你一個數組,包含 n 個數;一共有 m 個詢問,每次詢問輸入三個整數 L , R , k,表示求區間 [ L , R ] 以內第 k 小的數。( 1 ≤ n ≤ 100 000 , 1 ≤ m ≤ 5 000 , 陣

POJ 2104 K-th Number 主席(求區間第k大)

主席書資料 題意:給出n個數,m次詢問,[x,y]內第k小的數時多少?n<=1e5,m<=5000 主席樹:對原序列的每個字首i都建立一個線段樹 維護值域[l,r]中的每個數,在字首i的

POJ 2104 K-th Number (劃分 / 主席

Description You are working for Macrohard company in data structures department. After failing yo

poj 2104 K-th Number (主席模板)

傳送門 // by spli #include<cstring> #include<cstdio> #include<algorithm> #include<iostream> using namespace

poj 2104 K-th Number 主席+超級詳細解釋

題目大意:給出一段數列,讓你求[L,R]區間內第幾大的數字! 在這裡先介紹一下主席樹! 如果想了解什麼是主席樹,就先要知道線段樹,主席樹就是n棵線段樹,因為線段樹只能維護最大值或者最小值,要想求出第二大的數字怎麼辦呢?兩顆線段樹唄!好,那麼第n大呢,就可

POJ 2104 K-th Number (劃分,主席寫過了,這次是整體二分解法 )

還是先描述一下題意: 給出一個長度為n的數列,m次詢問區間內的第k大數 對劃分樹,主席樹和整體二分通過這題做了一下比較 劃分樹  1000ms+ 主席樹 2000ms+ 整體二分 1500ms+ 整體二分介於兩者之前,對於這題複雜度約莫是O( (n+m)log(n+m)l

[POJ 2104]K-th Number

n) 劃分樹 tput lease lap form 我們 歸並 nts Description You are working for Macrohard company in data structures department. After failing your

POJ 2104 K-th Number

poj 2104 working lan 只需要 please lin absolut input nlogn Time Limit: 20000MS Memory Limit: 65536K Total Submissions: 5948

POJ 2104 K-th Number ( 求取區間 K 大值 )

二分法 esp size 麻煩 == 平方分割 closed push_back ret 題意 : 給出一個含有 N 個數的序列,然後有 M 次問詢,每次問詢包含 ( L, R, K ) 要求你給出 L 到 R 這個區間的第 K 大是幾 分析 : 求取區間 K 大值是

POJ-2104 K-th Number CDQ分治

col 分享圖片 pri 找到 oid play out else mes 題目傳送門 題意:給你一個序列,長度為n,m次詢問,詢問一段區間的第k大。 題解:CDQ分治,對整個值域進行分治。每次取一個mid, 計算出整個區間內mid <= 的數目,如果 num >