劃分樹 詳解(轉)
轉自: https://blog.csdn.net/Akatsuki__Itachi/article/details/80030929
有這樣一類題目,求的是區間內的第k大數。
劃分樹的定義就是對整體的區間進行劃分,把相對於原來序列中較小的值放在左子樹,較大的放在右子樹,最後按照它的性質進行查詢以此找到要查詢的區間裡的第k大數。
例圖(圖是偷的~~~)
1.建樹
建樹是一個不停遞迴的過程
第一步:首先我們要根據排序後的陣列找到當前層數的中值(中值即中位數。注意,是中位數,不是中間的數),將沒有排序的序列(即輸入的原序列)裡面的數這樣安排:小於中位數的放進左子樹,大於等於中位數的放進右子樹。當然了,這是針對中值只有唯一一個時候的做法,一會再說多箇中值應該怎麼處理。
第二步:對於每一個子區間,我們都採用第一步的方法去劃分,直到左右區間相等的時候,即為終止遞迴的條件。
第三步:在我們向左子樹裡放數的時候,我們還要統計出區間 [left,right ] 裡有多少個數進入了左子樹(這個主要用於查詢操作)。
在劃分樹的時候,有幾點需要注意:
1.建樹是分層的,所以我們要用二維陣列去儲存,第一維只需要20就夠了,因為100000的資料量的話,它的層數為logN。
2.劃分的標準是中值,在第一步裡已經特別強調過。
3.劃分的數永遠存放在它的下一層,為什麼呢?下面舉個例子模擬一下過程就知道了。
那麼下面先列出我們要用到的陣列:
const int MAXL(1e5);
int tree[20 ][MAXL+50];//第一維代表當前的樹的層數,
//第二維代表這一層經過劃分後的序列值
int toLeft[20][MAXL+50];//第一維代表當前的樹的層數,
//第二維代表當前層區間[left,right]進入左子樹的數目
int sorted[MAXL+50];//將初始序列排序後的陣列
- 1
- 2
- 3
- 4
- 5
- 6
按照圖中給出的原始序列為
4 2 5 7 1 8 3 6
- 1
排序後的序列為
1 2 3 4 5 6 7 8
- 1
那麼我們tree [ 0 ]儲存的應該是原始序列
並且得到toLeft [ 0 ] 的序列
tree[0] = 42571836
toLeft[0]= 12223344
- 1
- 2
- 3
再次強調一遍
toLeft [ i ] [ j ] 存的是 第 i 層,當前劃分區間【 left , right 】裡進入左子樹的個數
至於為什麼要這麼存,一會說查詢的時候就知道了。
模擬一下劃分過程
首先是第一層,找到中值4 ( sorted[ ( left + mid) / 2 ] )
那麼tree [ 1 ] 和toLeft [ 1 ] 應該是
tree[1]= 42135786
toLeft[1]= 01221112
- 1
- 2
可能這裡有人注意到問題了,為什麼把4劃分到了左區間?上面不是說大於等於中值的劃分到右區間嗎? 別急-
第二層,分別對左子樹和右子樹按照上述的方法劃分
tree[2]= 21435678
toLeft[2]= 01011111
- 1
- 2
在這裡再囉嗦地解釋一下這一組的toLeft陣列
很明顯這一組的 2 1 4 3 5 6 7 8
分別在左 右 左 右 子樹
那麼對於左子樹裡的 2 1這個小區間,進入下一層左子樹的數分別為 0 1
對於右子樹 4 3 這個小區間,進入下一層左子樹的數分別為 0 1
…
…
第三層
tree[3]= 12345678
toLeft[3]= 00000000
- 1
- 2
下面開始說另外一個要注意的問題:有多箇中值怎麼辦?
因為我們要使得左右區間的數量儘可能的均等
所以在這裡,我們用一種特殊的處理方法。
在還沒有進行劃分之前,我們先假設中值左邊的資料都小於中值。
即 設定一個suppose = mid - left + 1。
如果當前的數小於中值,就使suppose減一,即
if(tree[level][i]<sorted[mid]
suppose--;
- 1
- 2
如果結果如我們假設的那樣,那麼suppose最後一定等於1,否則,就說明中值的數量不唯一。那麼在下面進行的時候,如果還剩suppose>1,就先把中值放在左子樹,直到suppose為0,如果仍還有中值,就把剩下的放進右子樹。
通過這樣操作,就能均分左右子樹了。
再舉個例子增深理解:
3 3 4 4 4 5 7
中值為4,左子樹要放4個((1+7)/2),右子樹放3個
處理後的suppose為2
那麼遇到第一個4,放進左子樹,suppose=1;
遇到第二個4,放進左子樹,suppose=0;
遇到第三個4,這時suppose已經等於0,所以放進右子樹。
終於可以上建樹的程式碼了
void Build_tree(int level,int left,int right)//level為當前層
{
if(left==right)//左右區間相等為終止條件
return ;
int mid=(left+right)>>1;
int suppose=mid-left+1;//設定suppose的初值
for(int i=left; i<=right; i++)
if(tree[level][i]<sorted[mid])//處理suppose
suppose--;
int subLeft=left,subRight=mid+1;//進入下層左右子樹的下標
for(int i=left; i<=right; i++)
{
if(i==left)//初始化
toLeft[level][i]=0;
else//初始化
toLeft[level][i]=toLeft[level][i-1];
if(tree[level][i]<sorted[mid]||tree[level][i]==sorted[mid]&&suppose>0)
{//這就是上面說的處理多箇中值的情況,放在一起了
tree[level+1][subLeft++]=tree[level][i];//將數放在下一層
toLeft[level][i]++;//進入左子樹的數目+1
if(tree[level][i]==sorted[mid])
suppose--;//繼續處理suppose
}
else//進入右子樹
tree[level+1][subRight++]=tree[level][i];
}
Build_tree(level+1,left,mid);//遞迴
Build_tree(level+1,mid+1,right);//遞迴
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
在建好樹之後,接下來就是查詢的問題。
假設初始大區間為【left , right】,要查詢的區間為【qLeft , qRight】
現在要查詢區間【qLeft , qRight】的第k大數
我們的想法是,先判斷【qLeft , qRight】在【left , right】的哪個子樹中,然後找出對應的小區間和k,然後遞迴查詢,直到小區間qLeft==qRight時為止。
那如何解決這個問題呢?這時候前面記錄的進入左子樹的元素個數就派上用場了。通過之前的記錄可以知道,在區間【left , qLeft】中有toLeft [ level ] [ qLeft - 1 ] 個元素進入了左子樹,記它為lef,同理,在區間【left , qRight】中有toLeft [ level ] [ qRight ] 個元素進入了左子樹,記它為rig , 所以在區間【qLeft , qRight】之間就有 rig - lef 個元素進入了左子樹,記為 toLef。 如果 toLef>= k ,說明 第k大元素肯定進入了左子樹,那麼就進入左子樹查詢,否則進入右子樹查詢。
那麼接下來要解決確定小區間的問題:
如果進入的是左子樹,那麼小區間就應該是
【 left +( [ left,qLeft-1] )進入左子樹的數目,left +( [ left,qRight ] )進入左子樹的數目-1】
即:【 left + lef , left + lef + tolef-1 】,並且,這時候k的值不用變化。
如果進入的是右子樹,那麼小區間就應該是
【 mid +( [ left,qLeft-1] )進入右子樹的數目+1,mid +( [ left,qRight ] )進入右子樹的數目】
即:【 mid + qLeft - left -lef + 1 , mid + qRight - left - toLef - lef + 1 】
同時,這裡的k要發生變化,變為k-(【qLeft , qRight】進入左子樹的元素個數)
即 k-toLef
其中mid = ( left + right ) / 2
這裡的區間式子很長,需要仔細思考。
下面舉個例子(又是偷的圖~~~)
獻上查詢的程式碼
//[qLeft,qRight]為查詢的區間,[left,right]為原始區間
int Query(int level,int qLeft,int qRight,int left,int right,int k)
{
int mid=(left+right)>>1;
if(qLeft==qRight)//終止條件
return tree[level][qLeft];
int lef;//lef 代表[left,qLeft]進入左子樹的個數
int toLef;//toLeft代表[qLeft,qRight]進入左子樹的個數
if(qLeft==left)//如果和原始區間重合
lef=0,toLef=toLeft[level][qRight];
else
lef=toLeft[level][qLeft-1],toLef=toLeft[level][qRight]-lef;
if(k<=toLef)//進入左子樹
{
int newLeft=left+lef;
int newRight=left+lef+toLef-1;
return Query(level+1,newLeft,newRight,left,mid,k);
}
else//進入右子樹
{
int newLeft=mid+qLeft-left-lef+1;
int newRight=mid+qRight-left-toLef-lef+1;
return Query(level+1,newLeft,newRight,mid+1,right,k-toLef);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
好了,說的也差不多了。
接下來就是一個模板題
poj2104
poj2104 AC程式碼
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<cmath>
#include<iomanip>
#include<map>
#include<stack>
#include<vector>
#include<queue>
#include<set>
#include<utility>
#include<list>
#include<algorithm>
#define max(a,b) (a>b?a:b)
#define min(a,b) (a<b?a:b)
#define swap(a,b) (a=a+b,b=a-b,a=a-b)
#define memset(a,v) memset(a,v,sizeof(a))
#define X (sqrt(5)+1)/2.0 //Wythoff
#define Pi acos(-1)
#define e 2.718281828459045
#define eps 1.0e-8
using namespace std;
typedef long long int LL;
typedef pair<int,int>pa;
const int MAXL(1e5);
const int INF(0x3f3f3f3f);
const int mod(1e9+7);
int dir[4][2]= {{-1,0},{1,0},{0,1},{0,-1}};
int tree[20][MAXL+50];
int toLeft[20][MAXL+50];
int sorted[MAXL+50];
void Build_tree(int level,int left,int right)
{
if(left==right)
return ;
int mid=(left+right)>>1;
int suppose=mid-left+1;
for(int i=left; i<=right; i++)
if(tree[level][i]<sorted[mid])
suppose--;
int subLeft=left,subRight=mid+1;
for(int i=left; i<=right; i++)
{
if(i==left)
toLeft[level][i]=0;
else
toLeft[level][i]=toLeft[level][i-1];
if(tree[level][i]<sorted[mid]||tree[level][i]==sorted[mid]&&suppose>0)
{
tree[level+1][subLeft++]=tree[level][i];
toLeft[level][i]++;
if(tree[level][i]==sorted[mid])
suppose--;
}
else
tree[level+1][subRight++]=tree[level][i];
}
Build_tree(level+1,left,mid);
Build_tree(level+1,mid+1,right);
}
int Query(int level,int qLeft,int qRight,int left,int right,int k)
{
int mid=(left+right)>>1;
if(qLeft==qRight)
return tree[level][qLeft];
int lef;
int toLef;
if(qLeft==left)
lef=0,toLef=toLeft[level][qRight];
else
lef=toLeft[level][qLeft-1],toLef=toLeft[level][qRight]-lef;
if(k<=toLef)
{
int newLeft=left+lef;
int newRight=left+lef+toLef-1;
return Query(level+1,newLeft,newRight,left,mid,k);
}
else
{
int newLeft=mid+qLeft-left-lef+1;
int newRight=mid+qRight-left-toLef-lef+1;
return Query(level+1,newLeft,newRight,mid+1,right,k-toLef);
}
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
{
scanf("%d",&tree[0][i]);
sorted[i]=tree[0][i];
}
sort(sorted+1,sorted+n+1);
Build_tree(0,1,n);
while(m--)
{
int ql,qr,k;
scanf("%d%d%d",&ql,&qr,&k);
int ans=Query(0,ql,qr,1,n,k);
cout<<ans<<endl;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
做題的過程中發現了toLeft陣列的另一種存法
下面的模板程式碼對於toLeft【i】【j】存的是第 i 層 1到 j 進入左子樹的元素個數
copy下別人的模板
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define MAX_SIZE 100005
int sorted[MAX_SIZE];//已經排好序的資料
int toleft[25][MAX_SIZE];
int tree[25][MAX_SIZE];
void build_tree(int left, int right, int deep)
{
int i;
if (left == right) return ;
int mid = (left + right) >> 1;
int same = mid - left + 1; //位於左子樹的資料
for (i = left; i <= right; ++i) {//計算放於左子樹中與中位數相等的數字個數
if (tree[deep][i] < sorted[mid]) {
--same;
}
}
int ls = left;
int rs = mid + 1;
for (i = left; i <= right; ++i) {
int flag = 0;
if ((tree[deep][i] < sorted[mid]) || (tree[deep][i] == sorted[mid] && same > 0)) {
flag = 1;
tree[deep + 1][ls++] = tree[deep][i];
if (tree[deep][i] == sorted[mid])
same--;
} else {
tree[deep + 1][rs++] = tree[deep][i];
}
toleft[deep][i] = toleft[deep][i - 1]+flag;
}
build_tree(left, mid, deep + 1);
build_tree(mid + 1, right, deep + 1);
}
int query(int left, int right, int k, int L, int R, int deep)
{
if (left == right)
return tree[deep][left];
int mid = (L + R) >> 1;
int x = toleft[deep][left - 1] - toleft[deep][L - 1];//位於left左邊的放於左子樹中的數字個數
int y = toleft[deep][right] - toleft[deep][L - 1];//到right為止位於左子樹的個數
int ry = right - L - y;//到right右邊為止位於右子樹的數字個數
int cnt = y - x;//[left,right]區間內放到左子樹中的個數
int rx = left - L - x;//left左邊放在右子樹中的數字個數
if (cnt >= k) {
//printf("sss %d %d %d\n", xx++, x, y);
return query(L + x, L + y - 1, k, L, mid, deep + 1);
// 因為x不在區間內 所以沒關係 所以先除去,從L+x開始,然後確定範圍
}
else {
//printf("qqq %d %d %d\n", xx++, x, y);
return query(mid + rx + 1, mid + 1 + ry, k - cnt, mid + 1, R, deep + 1);
//同理 把不在區間內的 分到右子樹的元素數目排除,確定範圍
}
}
int main()
{
int m, n;
int a, b, k;
int i;
while (scanf("%d%d", &m, &n) == 2) {
for (i = 1; i <= m; ++i) {
scanf("%d", &sorted[i]);
tree[0][i] = sorted[i];
}
sort(sorted + 1, sorted + 1 + m);
build_tree(1, m, 0);
for (i = 0; i < n; ++i) {
scanf("%d%d%d", &a, &b, &k);
printf("%d\n", query(a, b, k, 1, m, 0));
}
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77