1. 程式人生 > >劃分樹 圖文講解讓你一次就懂 hdu2665為例

劃分樹 圖文講解讓你一次就懂 hdu2665為例

學主席樹的時候看到一篇部落格說是因為一個大牛當時不會劃分樹而想出的另一種解決區間第k小的問題,所以在學了主席樹之後我就學了下劃分樹。
劃分樹解決靜態區間第k小問題比主席樹的時空消耗都要少,不過好像不能解決動態區間第k小問題。

這篇部落格寫的非常好,所以我直接轉載了。

劃分樹

劃分樹,類似線段樹,主要用於求解某個區間的第k 大元素(時間複雜度log(n))。

下面以hdu2665為例進行講解,給你n 個數的原序列,有m 次詢問,每次詢問給出l、r、k,求原序列l 到r 之間第k小的數。(n,m<=100000)

劃分樹,顧名思義是將n 個數的序列不斷劃分,根結點就是原序列,左孩子儲存父結點所有元素排序後的一半,右孩子也存一半,也就是說排名1 -> mid的存在左邊,排名(mid+1) -> r 的存在右邊,同一結點上每個元素保持原序列中相對的順序。見下圖:

紅點標記的就是進入左孩子的元素。

  當然,一般不會說每個結點開個陣列存數,經觀察,每一層都包含原本的n 個數,只是順序不同而已,所以我們可以開val[20][N]來儲存,也就是說共20層,每一層N個數。

  我們還需要一個輔助陣列num,num[i]表示i 前面有多少數進入左孩子(i 和i 前面可以弄成本結點內也可以是所有,兩種風格不同而已,下面採取的是本結點內),和val一樣,num也開成num[20][N],來表示每一層,i 和i 前面(本結點)有多少進入左孩子。

  第一層:1 進入左孩子,num[1]=1,5 進入右孩子,num[2]=1,…,num[8]=4。

  第二層:5 進入左孩子,num[5]=1,6 進入右孩子,num[6]=1,…,num[8]=2。

  建圖時就是維護每一層val[]和num[]的值就可以了。

int h[maxn];  //排序後陣列
int val[20][maxn]; //20層,每一層元素排放,0層就是原陣列
int num[20][maxn]; //num[i] 表示i前面有多少個點進入左孩子
void build(int l,int r,int deep)
{
    if(l==r) return;
    //cnt儲存有多少和h[mid]一樣大的數進入左孩子,用於保證最多隻有一半的數進入左孩子
    int mid = (l+r)>>1,cnt = mid-l+1;
    for(int i=l;i<=r;i++) if
(val[deep][i]<h[mid]) cnt--; int ln=l,rn=mid+1; //本結點兩個孩子結點的開頭,ln左 for(int i=l;i<=r;i++) { if(i==l) num[deep][i] = 0; else num[deep][i] = num[deep][i-1]; if(val[deep][i]<h[mid] || val[deep][i]==h[mid]&&cnt>0) { val[deep+1][ln++] = val[deep][i]; num[deep][i]++; if(val[deep][i]==h[mid]) cnt--; } else { val[deep+1][rn++] = val[deep][i]; } } build(l,mid,deep+1); build(mid+1,r,deep+1); }

查詢時,比如要查詢2 到6 之間第3 大的數,那麼先判斷2 到6 之間有多少元素進入左子樹,(在此忽略細節)num[6]-num[2-1]=2,就說明2 到6 有兩個數進入左子樹,又因為我們要找的是第3 大的數,所以一定在右子樹中。可以算出,下標2 前面有0 個數進入右子樹,所以2 到6 之間進入右子樹的元素在下一層一定是從5 開始排的,2 到6 的區間進入右子樹3 個,所以下一層從5 排到7 都是原本2 到6 之間的。現在,因為去左子樹兩個,所以現在要在右子樹找5 到7 之間排名第1 的元素。在下一層的查詢步驟和第一層一樣,也就是不斷遞迴,當跑到葉子結點時就可以返回正確的值了。

int query(int deep,int s,int e,int l,int r,int k)
{
    if(l==r) return val[deep][l];
    int pre; //pre表示s前面有多少元素進入左孩子
    if(l==s) pre=0; //這裡要特判一下和左端點重合時
    else pre = num[deep][s-1];
    //這一層s到e之間進入左子樹的有cnt個
    int mid = (l+r)>>1,cnt = num[deep][e]-pre;
    if(k<=cnt)
    {
        return query(deep+1,l+pre,l+num[deep][e]-1,l,mid,k);
    }
    else
    {
        // s-l 表示s前面有多少數,再減pre 表示這些數中去右子樹的有多少個
        int rn = mid+1+s-l-pre;
        // e-s+1 表示s到e有多少數,減去去左邊的,剩下是去右邊的,去右邊1個,下標就是rn,所以減1
        return query(deep+1,rn,rn+e-s+1-cnt-1,mid+1,r,k-cnt);
    }
}

hdu2665

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int maxn = 1e5+5;
int n,m;
int h[maxn],val[20][maxn],num[20][maxn];

void build(int l,int r,int deep)
{
    if(l==r) return;
    //cnt儲存有多少和h[mid]一樣大的數進入左孩子,用於保證最多隻有一半的數進入左孩子
    int mid = (l+r)>>1,cnt = mid-l+1;
    for(int i=l;i<=r;i++) if(val[deep][i]<h[mid]) cnt--;
    int ln=l,rn=mid+1; //本結點兩個孩子結點的開頭,ln左
    for(int i=l;i<=r;i++)
    {
        if(i==l) num[deep][i] = 0;
        else num[deep][i] = num[deep][i-1];
        if(val[deep][i]<h[mid] || val[deep][i]==h[mid]&&cnt>0)
        {
            val[deep+1][ln++] = val[deep][i];
            num[deep][i]++;
            if(val[deep][i]==h[mid]) cnt--;
        }
        else
        {
            val[deep+1][rn++] = val[deep][i];
        }
    }
    build(l,mid,deep+1);
    build(mid+1,r,deep+1);
}

int query(int deep,int s,int e,int l,int r,int k)
{
    if(l==r) return val[deep][l];
    int pre; //pre表示s前面有多少元素進入左孩子
    if(l==s) pre=0; //這裡要特判一下和左端點重合時
    else pre = num[deep][s-1];
    //這一層s到e之間進入左子樹的有cnt個
    int mid = (l+r)>>1,cnt = num[deep][e]-pre;
    if(k<=cnt)
    {
        return query(deep+1,l+pre,l+num[deep][e]-1,l,mid,k);
    }
    else
    {
        // s-l 表示s前面有多少數,再減pre 表示這些數中去右子樹的有多少個
        int rn = mid+1+s-l-pre;
        // e-s+1 表示s到e有多少數,減去去左邊的,剩下是去右邊的,去右邊1個,下標就是rn,所以減1
        return query(deep+1,rn,rn+e-s+1-cnt-1,mid+1,r,k-cnt);
    }
}

int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++) scanf("%d",&val[0][i]),h[i]=val[0][i];
        sort(h+1,h+1+n);
        build(1,n,0);
        int l,r,k;
        while(m--)
        {
            scanf("%d%d%d",&l,&r,&k);
            printf("%d\n",query(0,l,r,1,n,k));
        }
    }
    return 0;
}

看一下和主席樹的對比
劃分樹AC

主席樹AC