樹狀結構之主席樹

主席樹搞了一個多星期TAT,,,,,,也只是大體領悟而已!!!php

主席樹又稱函數式線段樹,顧名思義,也就是經過函數來實現的線段樹,至於爲何叫主席樹,那是由於是fotile主席建立出來的這個數據結構(其實貌似是當初主席不會劃分樹而本身想出來的另外一個處理方式。。。。是否是很吊呢? ORZ...)不扯了,切入正題。node

主席樹就是利用函數式編程的思想來使線段樹支持詢問歷史版本、同時充分利用它們之間的共同數據來減小時間和空間消耗的加強版的線段樹。編程

     不少問題若是用線段樹處理的話須要採用離線思想,若用主席樹則可直接在線處理。故不少時候離線線段樹求解能夠轉化爲在線主席樹求解。注意,主席樹本質就是線段樹,變化就在其實現可持久化,後一刻能夠參考前一刻的狀態,兩者共同部分不少。一顆線段樹的節點維護的是當前節點對應區間的信息,假若每次區間都不同,就會給處理帶來一些困難。有時能夠直接細分區間而後合併,此種狀況線段樹能夠直接搞定;但有時沒法經過直接劃分區間來求解,如頻繁詢問區間第k小元素,固然,此問題有比較特殊的數據結構-劃分樹。其實還有一個叫作歸併樹,是根據歸併排序實現的,每一個節點保存的是該區間歸併排序後的序列,所以,時間、空間複雜度都及其高, 因此通常不推薦去用。固然,主席樹也是能夠解決的。數據結構

附上歸併樹代碼:ide

 1 #include <cstdio>
 2 #include <cstring>
 3 #include <vector>
 4 #include <algorithm>
 5 using namespace std;
 6 const int N = 100000 + 5;
 7 
 8 vector<int>node[N << 2];
 9 
10 int T, n, q, ql, qr, ans, k, sz;
11 
12 int a[N], b[N];
13 
14 inline int read(){//快速讀入是邪教
15     char c;
16     int ret = 0;
17     int sgn = 1;
18     do{c = getchar();}while((c < '0' || c > '9') && c != '-');
19     if(c == '-') sgn = -1; else ret = c - '0';
20     while((c = getchar()) >= '0' && c <= '9') ret = ret * 10 + (c - '0');
21     return sgn * ret;
22 }
23 
24 void Build(int o, int l, int r){
25     node[o].clear();
26     if(l == r){
27         node[o].push_back(a[l]);
28         return ;
29     }
30     int m = (l + r) >> 1;
31     Build(o << 1, l, m);
32     Build(o << 1|1, m + 1, r);
33     node[o].resize(r - l + 1);
34     merge(node[o<<1].begin(), node[o<<1].end(), node[o<<1|1].begin(), node[o<<1|1].end(), node[o].begin());
35 }
36 
37 int query(int o, int l, int r, int x){
38     //if(ql > r || qr < l) return 0;
39     if(ql <= l && qr >= r)  return upper_bound(node[o].begin(), node[o].end(), x) - node[o].begin();
40     int m = (l + r) >> 1;
41     int ret = 0;
42     if(ql <= m)ret += query(o << 1, l, m, x);
43     if(qr > m)ret += query(o << 1|1, m + 1, r, x);
44     return ret;
45 }
46 
47 void work(){
48     //ql = read();
49     //qr = read();
50     //k = read();
51     scanf("%d%d%d", &ql, &qr, &k);
52     int lt = 1, rt = sz;
53    while(lt <= rt){
54        int md = (lt + rt) >> 1;
55        if(query(1, 1, n, b[md]) >= k)rt = md - 1;
56        else lt = md + 1;
57    } 
58    printf("%d\n", b[rt+1]);
59 }
60 
61 int main(){
62     scanf("%d", &T);
63     while(T--){
64         scanf("%d%d", &n, &q);
65         //n = read();
66         //q = read();
67         //for(int i = 1; i <= n; i ++) a[i] = b[i] = read();
68         for(int i = 1; i <= n; i ++)scanf("%d", a + i), b[i] = a[i];
69         Build(1, 1, n);
70         sort(b + 1, b + n + 1);
71         sz = unique(b + 1, b + n + 1) - (b + 1);
72         while(q --)work();
73     }
74     return 0;
75 }
View Code

 

赤果果的嫌棄,時間竟然那麼費,,,,,,,(不過挺好理解的)函數式編程

     主席樹的每一個節點對應一顆線段樹,此處有點抽象。在咱們的印象中,每一個線段樹的節點維護的樹左右子樹下標以及當前節點對應區間的信息(信息視具體問題定)。對於一個待處理的序列a[1]、a[2]…a[n],有n個前綴。每一個前綴能夠看作一棵線段樹,共有n棵線段樹;若不採用可持久化結構,帶來的嚴重後果就是會MLE,即對內存來講很難承受。根據可持久化數據結構的定義,因爲相鄰線段樹即前綴的公共部分不少,能夠充分利用,達到優化目的,同時每棵線段樹仍是保留全部的葉節點只是較以前共用了不少共用節點。主席樹很重要的操做就是如何尋找公用的節點信息,這些可能可能出如今根節點也可能出如今葉節點。函數

下面是某大牛的理解:所謂主席樹呢,就是對原來的數列[1..n]的每個前綴[1..i](1≤i≤n)創建一棵線段樹,線段樹的每個節點存某個前綴[1..i]中屬於區間[L..R]的數一共有多少個(好比根節點是[1..n],一共i個數,sum[root] = i;根節點的左兒子是[1..(L+R)/2],若不大於(L+R)/2的數有x個,那麼sum[root.left] = x)。若要查找[i..j]中第k大數時,設某結點x,那麼x.sum[j] - x.sum[i - 1]就是[i..j]中在結點x內的數字總數。而對每個前綴都建一棵樹,會MLE,觀察到每一個[1..i]和[1..i-1]只有一條路是不同的,那麼其餘的結點只要用回前一棵樹的結點便可,時空複雜度爲O(nlogn)。優化


我本身對主席樹的理解,是一個線段樹在修改一個值的時候,它只要修改logn個節點就能夠了,那麼咱們只要每次增長logn個節點就能夠記錄它原來的狀態了, 即你在更新一個值的時候僅僅只是更新了一條鏈,其餘的節點都相同,即達到共用。因爲主席樹每棵節點保存的是一顆線段樹,維護的區間相同,結構相同,保存的信息不一樣,所以具備了加減性。(這是主席樹關鍵所在,當除筆者理解了好久好久,才相通的),因此在求區間的時候,若要處區間[l, r], 只須要處理rt[r] - rt[l-1]就能夠了,(rt[l-1]處理的是[1,l-1]的數,rt[r]處理的是[1,r]的數,相減即爲[l, r]這個區間裏面的數。ui

好比說(以區間第k大爲例hdu2665題目戳這裏http://acm.hdu.edu.cn/showproblem.php?pid=2665):spa

設n = 4,q= 1;

4個數分別爲4, 1, 3 ,2;

ql = 1, qr = 3, k = 2;

1.建樹

首先須要創建一棵空的線段樹,也是最原始的主席樹,此時主席樹只含一個空節點,此時設根節點爲rt[0],表示剛開始的初值狀態,而後依次對原序列按某種順序更新,即將原序列加入到對應位置。此過程與線段樹同樣,時間複雜度爲O(nlogn),空間複雜度O(nlog(n))(筆者目前沒有徹底搞清到底是多少, 不過保守狀況下,線段樹不會超過4*n)

2.更新

 咱們知道,更新一個葉節點只會影響根節點到該葉節點的一條路徑,故只需修改該路徑上的信息便可。每一個主席樹的節點即每棵線段樹的結構徹底相同,只是對應信息(能夠理解爲線段樹的結構徹底同樣,只是對應葉子節點取值不一樣,從而有些節點的信息不一樣,本質是節點不一樣),此時能夠利用歷史狀態,即利用相鄰的上一棵線段樹的信息。相鄰兩顆線段樹只有當前待處理的元素不一樣,其他位置徹底同樣。所以,若是待處理的元素進入線段樹的左子樹的話,右子樹是徹底同樣的,能夠共用,即直接讓當前線段樹節點的右子樹指向相鄰的上一棵線段樹的右子樹;若進入右子樹,狀況能夠類比。此過程容易推出時間複雜度爲O(logn),空間複雜度爲 O(logn)。如圖:

 

3.查詢

先附上處理好以後的主席樹, 如圖:

是否是看着很暈。。。。。。筆者其實也暈了,咱們把共用的節點拆開來,看下圖:

啊, 這下清爽多了,一眼看下去就知道每一個節點維護的是哪棵線段樹了,TAT,若是早就這樣寫估計很快就明白了,rt[i]表示處理完前i個數以後所造成的線段樹,即具備了前綴和的性質,那麼rt[r] - rt[l-1]即表示處理的[l, r]區間嘍。當要查詢區間[1,3]的時候,咱們只要將rt[3] 和 rt[0]節點相減便可獲得。如圖:

這樣咱們獲得區間[l, r]的數要查詢第k大便很容易了,設左節點中存的個數爲cnt,當k<=cnt時,咱們直接查詢左兒子中第k小的數便可,若是k>cnt,咱們只要去查右兒子中第k-cnt小的數便可,這邊是一道很簡單的線段樹了。就如查找[1, 3]的第2小數(圖上爲了方便,從新給節點標號),從根節點1向下搜,發現左兒子2的個數爲1,1<2,全部去右兒子3中搜第2-1級第1小的數,而後再往下搜,發現左兒子6即可以了,此時已經搜到底端,因此直接返回節點6維護的值3便可就能夠了。

附上代碼:

 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 const int N = 100000 + 5;
 6 
 7 int a[N], b[N], rt[N * 20], ls[N * 20], rs[N * 20], sum[N * 20];
 8 
 9 int n, k, tot, sz, ql, qr, x, q, T;
10 
11 void Build(int& o, int l, int r){
12     o = ++ tot;
13     sum[o] = 0;
14     if(l == r) return;
15     int m = (l + r) >> 1;
16     Build(ls[o], l, m);
17     Build(rs[o], m + 1, r);
18 }
19 
20 void update(int& o, int l, int r, int last, int p){
21     o = ++ tot;
22     ls[o] = ls[last];
23     rs[o] = rs[last];
24     sum[o] = sum[last] + 1;
25     if(l == r) return;
26     int m = (l + r) >> 1;
27     if(p <= m)  update(ls[o], l, m, ls[last], p);
28     else update(rs[o], m + 1, r, rs[last], p);
29 }
30 
31 int query(int ss, int tt, int l, int r, int k){
32     if(l == r) return l;
33     int m = (l + r) >> 1;
34     int cnt = sum[ls[tt]] - sum[ls[ss]];
35     if(k <= cnt) return query(ls[ss], ls[tt], l, m, k);
36     else return query(rs[ss], rs[tt], m + 1, r, k - cnt);
37 }
38 
39 void work(){
40     scanf("%d%d%d", &ql, &qr, &x);
41     int ans = query(rt[ql - 1], rt[qr], 1, sz, x);
42     printf("%d\n", b[ans]);
43 }
44 
45 int main(){
46     scanf("%d", &T);
47     while(T--){
48         scanf("%d%d", &n, &q);
49         for(int i = 1; i <= n; i ++) scanf("%d", a + i), b[i] = a[i];
50         sort(b + 1, b + n + 1);
51         sz = unique(b + 1, b + n + 1) - (b + 1);
52         tot = 0;
53         Build(rt[0],1, sz);
54         //for(int i = 0; i <= 4 * n; i ++)printf("%d,rt =  %d,ls =  %d, rs = %d, sum = %d\n", i, rt[i], ls[i], rs[i], sum[i]);
55         for(int i = 1; i <= n; i ++)a[i] = lower_bound(b + 1, b + sz + 1, a[i]) - b;
56         for(int i = 1; i <= n; i ++)update(rt[i], 1, sz, rt[i - 1], a[i]);
57         for(int i = 0; i <= 5 * n; i ++)printf("%d,rt =  %d,ls =  %d, rs = %d, sum = %d\n", i, rt[i], ls[i], rs[i], sum[i]);
58         while(q --)work();
59     }
60     return 0;
61 }
View Code

看着這個時間的複雜度和歸併樹一比,今後對歸併樹無愛了,估計不會再用了。。。。ORZ~~

4.總結

由以上可知,主席樹是一種特殊的線段樹集,他幾乎具備全部線段樹的全部優點,而且能夠保存歷史狀態,以便之後加以利用,主席樹查找和更新時時間空間複雜度均爲O(logn), 且空間複雜度約爲O(nlogn + nlogn)前者爲空樹的空間複雜度,後者爲更新n次的空間複雜度,主席樹的缺點就是空間耗損巨大,但仍是能夠接受的。固然主席樹不止這點應用,他能夠處理許多區間問題,例如求區間[l, r]中的值介於[x,y]的值。總之應用多多。

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息