划分樹講解


  划分樹,類似線段樹,主要用於求解某個區間的第k 大元素(時間復雜度log(n)),快排本也可以快速找出,但快排會改變原序列,所以每求一次都得恢復序列。

  下面就以 POJ 2104 進行解說:

  題目意思就是,給你n 個數的原序列,有m 次詢問,每次詢問給出l、r、k,求原序列l 到r 之間第k 大的數。n范圍10萬,m范圍5千,這道題用快排也可以過,快排過的時間復雜度n*m,而划分樹是m*logn(實際上應該是nlogn才對,因為建圖時間是nlogn,n又比m大),分別AC后,時間相差很明顯。

  划分樹,顧名思義是將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[]的值就可以了。

 1 int a[N];       //原數組
 2 int sorted[N];  //排序好的數組
 3 //是一棵樹,但把同一層的放在一個數組里。
 4 int num[20][N];   //num[i] 表示i前面有多少個點進入左孩子
 5 int val[20][N];   //20層,每一層元素排放,0層就是原數組
 6 void build(int l,int r,int ceng)
 7 {
 8   if(l==r) return ;
 9   int mid=(l+r)/2,isame=mid-l+1;  //isame保存有多少和sorted[mid]一樣大的數進入左孩子
10   for(int i=l;i<=r;i++) if(val[ceng][i]<sorted[mid]) isame--;
11   int ln=l,rn=mid+1;   //本結點兩個孩子結點的開頭,ln左
12   for(int i=l;i<=r;i++)
13   {
14     if(i==l) num[ceng][i]=0;
15     else num[ceng][i]=num[ceng][i-1];
16     if(val[ceng][i]<sorted[mid] || val[ceng][i]==sorted[mid]&&isame>0)
17     {
18       val[ceng+1][ln++]=val[ceng][i];
19       num[ceng][i]++;
20       if(val[ceng][i]==sorted[mid]) isame--;
21     }
22     else
23     {
24       val[ceng+1][rn++]=val[ceng][i];
25 }
26   }
27   build(l,mid,ceng+1);
28   build(mid+1,r,ceng+1);
29 }

  查詢時,比如要查找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 的元素。在下一層的查找步驟和第一層一樣,也就是不斷遞歸,當跑到葉子結點時就可以返回正確的值了。

 

 1 int look(int ceng,int sl,int sr,int l,int r,int k)
 2 {
 3   if(sl==sr) return val[ceng][sl];
 4   int ly;  //ly 表示l 前面有多少元素進入左孩子
 5   if(l==sl) ly=0;  //和左端點重合時
 6   else ly=num[ceng][l-1];
 7   int tolef=num[ceng][r]-ly;  //這一層l到r之間進入左子樹的有tolef個
 8   if(tolef>=k)
 9   {
10     return look(ceng+1,sl,(sl+sr)/2,sl+ly,sl+num[ceng][r]-1,k);
11   }
12   else
13   {
14     // l-sl 表示l前面有多少數,再減ly 表示這些數中去右子樹的有多少個
15     int lr = (sl+sr)/2 + 1 + (l-sl-ly);  //l-r 去右邊的開頭位置
16     // r-l+1 表示l到r有多少數,減去去左邊的,剩下是去右邊的,去右邊1個,下標就是lr,所以減1
17     return look(ceng+1,(sl+sr)/2+1,sr,lr,lr+r-l+1-tolef-1,k-tolef);
18   }
19 }

  上述的查詢采取了二分的思路,所以時間復雜度logn。建圖時,每一層n 個元素,共有logn 層,所以時間復雜度是nlogn。

  上題AC代碼:

  

 1 #include<stdio.h>
 2 #include<string.h>
 3 #include<algorithm>
 4 #define N 100010
 5 using namespace std;
 6 typedef long long LL;
 7 int a[N];       //原數組
 8 int sorted[N];  //排序好的數組
 9 //是一棵樹,但把同一層的放在一個數組里。
10 int num[20][N];   //num[i] 表示i前面有多少個點進入左孩子
11 int val[20][N];   //20層,每一層元素排放,0層就是原數組
12 void build(int l,int r,int ceng)
13 {
14   if(l==r) return ;
15   int mid=(l+r)/2,isame=mid-l+1;  //isame保存有多少和sorted[mid]一樣大的數進入左孩子
16   for(int i=l;i<=r;i++) if(val[ceng][i]<sorted[mid]) isame--;
17   int ln=l,rn=mid+1;   //本結點兩個孩子結點的開頭,ln左
18   for(int i=l;i<=r;i++)
19   {
20     if(i==l) num[ceng][i]=0;
21     else num[ceng][i]=num[ceng][i-1];
22     if(val[ceng][i]<sorted[mid] || val[ceng][i]==sorted[mid]&&isame>0)
23     {
24       val[ceng+1][ln++]=val[ceng][i];
25       num[ceng][i]++;
26       if(val[ceng][i]==sorted[mid]) isame--;
27     }
28     else
29     {
30       val[ceng+1][rn++]=val[ceng][i];
31 }
32   }
33   build(l,mid,ceng+1);
34   build(mid+1,r,ceng+1);
35 }
36 
37 
38 int look(int ceng,int sl,int sr,int l,int r,int k)
39 {
40   if(sl==sr) return val[ceng][sl];
41   int ly;  //ly 表示l 前面有多少元素進入左孩子
42   if(l==sl) ly=0;  //和左端點重合時
43   else ly=num[ceng][l-1];
44   int tolef=num[ceng][r]-ly;  //這一層l到r之間進入左子樹的有tolef個
45   if(tolef>=k)
46   {
47     return look(ceng+1,sl,(sl+sr)/2,sl+ly,sl+num[ceng][r]-1,k);
48   }
49   else
50   {
51     // l-sl 表示l前面有多少數,再減ly 表示這些數中去右子樹的有多少個
52     int lr = (sl+sr)/2 + 1 + (l-sl-ly);  //l-r 去右邊的開頭位置
53     // r-l+1 表示l到r有多少數,減去去左邊的,剩下是去右邊的,去右邊1個,下標就是lr,所以減1
54     return look(ceng+1,(sl+sr)/2+1,sr,lr,lr+r-l+1-tolef-1,k-tolef);
55   }
56 }
57 
58 int main()
59 {
60   int n,m,l,r,k;
61   while(scanf("%d%d",&n,&m)!=EOF)
62   {
63 
64     for(int i=1;i<=n;i++)
65     {
66       scanf("%d",&val[0][i]);
67       sorted[i]=val[0][i];
68     }
69     sort(sorted+1,sorted+n+1);
70     build(1,n,0);
71     while(m--)
72     {
73       scanf("%d%d%d",&l,&r,&k);
74       printf("%d\n",look(0,1,n,l,r,k));
75     }
76   }
77   return 0;
78 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM