線段樹入門


線段樹原理    

    線段樹是一顆二叉樹,他的每個節點對應的都是一個區間,主要是通過對區間的分割和合並來修改節點的值, 然后再得到答案。

      現在給你一個 目的為求區間和 所造出來的線段樹 線段樹。如下圖所示。

仔細觀察,第一二三行方框內的值是他的下面2個子區間的和, 第四行的方框內的數字代表的是自身的值, 藍色代表的是這個方框他包含的區間, 紅色代表的是這個元素在數組中所儲存的位置。(在絕大多數博客中,我們默認區間的左兒子他的下標是當前區間下標的2倍,右兒子的下標是當前區間的2倍再加上1,這個下標是認為定義的,你也可以將對應關系修改)。

為什么說用線段樹可以節省求和時間呢, 假設我們需要查找區間 [1,8] 的和, 對於這個不用多說, 我們可以直接將最上面的那個46輸出,因為最上面的那個矩形代表的就是區間 [1,8] 的和。

然后假設我們要查找區間 [3,7] 的和, 剛開始我們出現在區間 [1,8]的位置, 但是對於目標區間來說 [1,8] 太大了, 所以我們要繼續往下走, 走到 [1,4] 和 [5,8] 的區間, 但是對於這2個區間來說, 還有一部分區域是落在查詢區間之外的, 所以我們需要繼續往下走,我們先分析區間 [1,4] , 對於他左兒子的區間[1,2]來說,沒有任意一個點是落在查詢區間內的, 所以我們不需要走到他的左兒子處, 然后走到右兒子[3,4],可以發現 [3,4] 倍查詢區間覆蓋了, 所以我們就不需要往下走了, 因為整個區間都倍覆蓋了, 直接將這個點的值返回就好了, 因為這個點就是他下面節點的和。 然后我們再看區間[5,8], 先往左走, 走到左兒子區間 [5,6] ,也可以發現該區間倍查詢區間覆蓋了,就不需要往下走了, 返回該節點的值,對於右兒子節點 [7,8] 來說,只有一部分區域倍查詢區間覆蓋, 所以我們還需要往下走,繼續往左邊走, 發現 [7,7] 是合法區域, 返回該值, {8,8]不是合法區域,所以不對這快里的數據進行處理。 所以最后的結果就是 [3,4] + [5,6] + [7,7] 這3個區間的和。 可能你會說就5個點而已, 我直接加過去時間也就這樣, 的確, 當點數小的時候線段樹的優勢並不會很明顯,但是如果查詢的區間長度會到達 1e5的話, 那么線段樹就可以省下很多時間了。

 

線段樹的某段區間內的值是可以修改的。

假設我們修改了區間[2,2]的值

我們就需要更新一下所有區間內含2的區間, 也就是順着[2,2]一直往上走 按次序更新 [2,2]  -> [1,2] -> [1,4] -> [1,8] 這四個區間的值, 更新完了之后就可以繼續愉快的去查詢區間和了。 

可以發現, 每一次對於一個點更新之后, 她執行的點的數目就是logn個, 如果你使用的是前綴和去寫的話, 就需要約更新n 個節點。 

代碼實現

  

1,建樹,對於一顆樹需要先建樹。這里用到的是遞歸建樹。 

 1 void Build(int l, int r, int rt){ // l,r 代表的是這個區間內的左端點 和 右端點, rt代表的是 [l,r] 這個區間內的值是存在哪一個位置的。
 2     if(l == r){
 3         scanf("%d", tree[rt]); /// tree[rt] = a[l]; 
 4         return ;
 5     }
 6     int m = (l+r) / 2;
 7     Build(l,m,rt*2); // 對於區間區分,我們一般將m點划入左半邊區間
 8     Build(m+1,r,rt*2+1);
 9     PushUp(rt); // PushUp 函數是通過2個子節點來更新現在這個節點的狀態, 對於不同的要求需要不同的寫法。
10 }
建樹(有注釋)
 1 void Build(int l, int r, int rt){
 2     if(l == r){
 3         scanf("%d", tree[rt]); /// tree[rt] = a[l];
 4         return ;
 5     }
 6     int m = (l+r) / 2;
 7     Build(l,m,rt*2);
 8     Build(m+1,r,rt*2+1);
 9     PushUp(rt);
10 }
建樹(無注釋)

  

2,通過子節點來更新目前節點。  

1 void PushUp(int rt){
2     tree[rt] = tree[rt*2] + tree[rt*2+1]; ///區間和的更新操作
3 }
4 void PushUp(int rt){
5     tree[rt] = max(tree[rt*2], tree[rt*2+1]);///求區域最大值的更新操作
6 }
PushUp(有注釋)
1 void PushUp(int rt){
2     tree[rt] = tree[rt*2] + tree[rt*2+1];
3 }
4 void PushUp(int rt){
5     tree[rt] = max(tree[rt*2], tree[rt*2+1]);
6 }
PushUp(無注釋)

 

 

3,更新某個節點。

 1 void Update(int l, int r, int rt, int L, int C){ // l,r,rt 與前面的定義一樣, L代表的是要更新區間的位置,C代表的是修改后的值
 2     if(l == r){              /// 這里不能寫成 if(l == L) 因為有可能左端點恰好是要更新的位置, 但是還有右端點, 我們直接更新的只有區間 [L,L]。
 3         tree[rt] = C;
 4         return ;
 5     }
 6     int m = (l+r) / 2;
 7     if(L <= m) Update( l, m, rt*2, L, C); //要更新的區間在左邊部分,所以往左邊走,更新左邊
 8     else Update(m+1, r, rt*2+1, L, C); //要更新的區間在右邊部分, 往右邊走,更新右邊
 9     PushUp(rt); //更新完子節點之后需要更新現在的位置, 需要保證線段樹的性質。
10 }
單點更新(有注釋)
 1 void Update(int l, int r, int rt, int L, int C){
 2     if(l == r){              
 3         tree[rt] = C;
 4         return ;
 5     }
 6     int m = (l+r) / 2;
 7     if(L <= m) Update( l, m, rt*2, L, C); 
 8     else Update(m+1, r, rt*2+1, L, C); 
 9     PushUp(rt);
10 }
單點更新(無注釋)

 

4, 查詢區間和

查詢的規則前面已經解釋過一次了。

 1 int Query(int l, int r, int rt, int L, int R){// [L,R]為查詢區間
 2     if(L <= l && r <= R){  // 如果成立則滿足查詢區間覆蓋了當前區間, 直接返回當前區間的值
 3         return tree[rt];
 4     }
 5     int m = (l+r) / 2;
 6     int ret = 0;
 7     if(L <= m) ret += Query(l, m, rt*2, L, R); //左邊有一部分需要查詢的區域。
 8     if(m < R) ret += Query(m+1, r, rt*2+1, L, R);//右邊有一部分。
 9     return ret;
10 }
區間查詢(有注釋)
 1 int Query(int l, int r, int rt, int L, int R){
 2     if(L <= l && r <= R){
 3         return tree[rt];
 4     }
 5     int m = (l+r) / 2;
 6     int ret = 0;
 7     if(L <= m) ret += Query(l, m, rt*2, L, R);
 8     if(m < R) ret += Query(m+1, r, rt*2+1, L, R);
 9     return ret;
10 }
區間查詢(無注釋)

 

總結

1。首先對於大多數線段樹題目來說, 第一步就是建樹。 建樹用法 Build(1,n,1), [1,n]就是第一個節點所代表的區間長度。

2。在每次更新了點之后,為了保證線段樹性質, 一定要去執行PushUP操作,保證線段樹的性質不丟失。

3。線段樹的精華就是,每一個節點代表着一段區間,這個節點的值,就是他所代表的區間內的值。

4。當底層節點只有5個點的時候, 我們處理線段樹時, 需要將他變成8個節點, 如果給9個節點, 那么底層節點必須要有16個節點, 所以為了保證空間足夠用,所以需要將空間開大2倍,然后由於每一層的上方都還有 m/2個點(m為該層節點的數目)。

所以空間需要再大兩倍, 最終合起來就是4倍。 所以我們需要開 4n 的空間。

 

HDU-1166 線段樹求區間和

 1 #include<cstdio>
 2 #include<cstring>
 3 const int N = 50000+5;
 4 int tree[N<<2], a[N];
 5 void PushUp(int rt) {
 6     tree[rt] = tree[rt<<1]+tree[rt<<1|1];
 7 }
 8 void Build(int l, int r, int rt){
 9     if(l == r) {
10         tree[rt] = a[l];
11         return ;
12     }
13     int m = l+r >> 1;
14     Build(l, m, rt*2);
15     Build(m+1, r, rt*2+1);
16     PushUp(rt);
17 }
18 void Update(int l, int r, int rt, int L, int C){
19     if(l == r){
20         tree[rt] += C;
21         return ;
22     }
23     int m = l+r >> 1;
24     if(L <= m) Update(l, m, rt*2, L, C);
25     else Update(m+1, r, rt*2+1, L, C);
26     PushUp(rt);
27 }
28 int Query(int l, int r, int rt, int L, int R){
29     if(L <= l && r <= R) return tree[rt];
30     int ans = 0;
31     int m = l+r >> 1;
32     if(L <= m) ans += Query(l, m, rt*2, L, R);
33     if(m < R)  ans += Query(m+1, r, rt*2+1, L, R);
34     return ans;
35 }
36 int main()
37 {
38     int t, n, x, y;
39     char str[100];
40     scanf("%d", &t);
41     for(int i = 1; i <= t; i++) {
42         printf("Case %d:\n", i);
43         int n;
44         scanf("%d", &n);
45         for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
46         Build(1,n,1);
47         while(~scanf("%s", str) && strcmp(str,"End") != 0) {
48             scanf("%d%d", &x, &y);
49             if(str[0] == 'Q') printf("%d\n", Query(1, n, 1, x, y));
50             else if(str[0] == 'A') Update(1, n, 1, x, y);
51             else if(str[0] == 'S') Update(1, n, 1, x, -y);
52         }
53     }
54     return 0;
55 }
View Code

HDU-1754 線段樹求區間最大值

 1 #include<cstdio>
 2 #include<algorithm>
 3 using namespace std;
 4 const int N = 200005;
 5 int tree[N<<2], a[N];
 6 void PushUp(int rt) {
 7     tree[rt] = max(tree[rt<<1], tree[rt<<1|1]);
 8 }
 9 void Build(int l, int r, int rt) {
10     if(l == r) {
11         tree[rt] = a[l];
12         return ;
13     }
14     int m = l+r >>1;
15     Build(l, m, rt*2);
16     Build(m+1, r, rt*2+1);
17     PushUp(rt);
18 }
19 void Update(int l, int r, int rt, int L, int C) {
20     if(l == r) {
21         tree[rt] = C;
22         return;
23     }
24     int m = l+r >> 1;
25     if(L <= m) Update(l, m, rt*2, L, C);
26     else Update(m+1, r, rt*2+1, L, C);
27     PushUp(rt);
28 }
29 int Query(int l, int r, int rt, int L, int R) {
30     if(L <= l && r <= R)  {
31         return tree[rt];
32     }
33     int ret = -N, m = l+r >> 1;
34     if(L <= m) ret = max(ret, Query(l, m, rt*2, L, R));
35     if(m < R) ret = max(ret, Query(m+1, r, rt*2+1, L, R));
36     return ret;
37 }
38 int main()
39 {
40     int n, m;
41     char str[N];
42     while(~scanf("%d%d", &n, &m)) {
43         for(int i = 1; i <= n; i++)
44             scanf("%d", &a[i]);
45         Build(1,n,1);
46         int i, j;
47         while(m--) {
48             scanf("%s%d%d", str, &i, &j);
49             if(str[0] == 'Q'){
50                 if(i > j) swap(i, j);
51                 printf("%d\n", Query(1,n,1,i,j));
52             }
53             else if(str[0] == 'U')
54                 Update(1,n,1,i,j);
55         }
56     }
57     return 0;
58 
59 }
View Code

 

區間更新 lazy標記

現在我們突然遇到這樣一個題目

POJ-3468

這個題目和上面題目不同的地方是更新, 在這個題目中, 他更新數據是成段更新, 上面的題目都是一個點一個點更新, 並且更新的次數不是很少, 我們不可能去像點更新一樣, 將這些區域內的點都一個個更新過去。

前面提到過,線段樹的每一個節點都代表着一段區間的性質, 所以假如我們需要對於區間 [5,8] 里面的數都加上 10。(基於更新[2,2]后的那個線段樹)。

如果我們將一個個點覆蓋過去之后, 現在的這課樹是這樣的。

我們可以發現對節點3來說, 他所管轄的區間[5,8]都是要被更新的區間,並且他增加的指為40,即 區間長度(4) * 修改的值(10)。 我們可以發現,在區域更新的時候, 對於一個節點來說, 如果他所管理的區間 被 要更新的區間 覆蓋了, 那我們就提早了知道這一個節點的值。

然后我們引入一個概念, lazy標記, 還是對於開頭的情況來說, 如果我們使用了lazy標記之后, 這一課線段樹是這樣的

在這一顆樹上, 我們只修改了2個節點, 同時在節點3處增加了一個 lazy標記, 在這個標記中 lazy = 10。(即整段區間內每一個點都要加上的值)。接下來, 我們如果詢問區間[1,8]的和, 我們直接返回節點1就好了。  如果我們詢問區間 [3,8] 的和 那么只需要返回 [3,4] + [5,8] 的值。 我們可以看見如果不訪問[5,8]的子區間的時候, 我並不會用到里面的值。 在這些時候, 我們並不需要更新里面的值, 更不更新都一樣, 不會被訪問到。

如果我們現在需要查詢 [1,5]的和, 我們只需要將 lazy 標記下推,然后再更新對應的區間就好了。

然后我們返回 [1,4] + [5,5] 的值就好了。 

總結就是:

lazy標記的含義就是延遲更新,在我們不需要訪問區間內部時就保留lazy標記的值,如果需要訪問內部的時候,我們要先將lazy標記下推, 因為可能lazy標記還需要繼續往下走。

在區域更新的時候,如果 當前區間 被 更新的區間完全覆蓋了, 就直接在這個節點加上 區間長度*修改的值, 並且更新這個點的lazy標記。

操作代碼

PushDown --- 將lazy標記下推

1 void PushDown(int rt, int llen, int rlen){
2     if(lazy[rt]){
3         lazy[rt*2] += lazy[rt];
4         lazy[rt*2+1] += lazy[rt];
5         tree[rt*2] += lazy[rt] * llen;
6         tree[rt*2+1] += lazy[rt] * rlen;
7         lazy[rt] = 0;
8     }
9 }
PushDown
void Update(int l, int r, int rt, int L, int R, int C){
    if(L <= l && r <= R){
        tree[rt] += (LL)C*(r-l+1);
        lazy[rt] += C;
        return;
    }
    int m = (l+r) / 2;
    PushDown(rt, m-l+1, r-m);
    if(L <= m) Update(l, m, rt*2, L, R, C);
    if(m < R) Update(m+1, r, rt*2+1, L, R, C);
    PushUp(rt);
}
區域更新
1 LL Query(int l, int r, int rt, int L, int R){
2     if(L <= l && r <= R) return tree[rt];
3     LL ans = 0;
4     int m = (l+r) / 2;
5     PushDown(rt, m-l+1, r-m);
6     if(L <= m) ans += Query(l, m, rt*2, L, R);
7     if(m < R) ans += Query(m+1, r, rt*2+1, L, R);
8     return ans;
9 }
查詢

注意的就是每次對子區間進行修改的時候,我們都需要提前先把lazy標記下推。

 

 

所以一開始的那個題目我們就可以做了。

 1 #include<cstdio>
 2 #define LL long long
 3 const int N = 1e5+10;
 4 LL tree[N<<2];
 5 LL lazy[N<<2];
 6 int a[N];
 7 void PushUp(int rt){
 8     tree[rt] = tree[rt*2] + tree[rt*2+1];
 9 }
10 void PushDown(int rt, int llen, int rlen){
11     if(lazy[rt]){
12         lazy[rt*2] += lazy[rt];
13         lazy[rt*2+1] += lazy[rt];
14         tree[rt*2] += lazy[rt] * llen;
15         tree[rt*2+1] += lazy[rt] * rlen;
16         lazy[rt] = 0;
17     }
18 }
19 void Build(int l, int r, int rt){
20     lazy[rt] = 0;
21     if(l == r){
22         tree[rt] = a[l];
23         return ;
24     }
25     int m = (l+r) / 2;
26     Build(l, m, rt*2);
27     Build(m+1, r, rt*2+1);
28     PushUp(rt);
29 }
30 void Update(int l, int r, int rt, int L, int R, int C){
31     if(L <= l && r <= R){
32         tree[rt] += (LL)C*(r-l+1);
33         lazy[rt] += C;
34         return;
35     }
36     int m = (l+r) / 2;
37     PushDown(rt, m-l+1, r-m);
38     if(L <= m) Update(l, m, rt*2, L, R, C);
39     if(m < R) Update(m+1, r, rt*2+1, L, R, C);
40     PushUp(rt);
41 }
42 LL Query(int l, int r, int rt, int L, int R){
43     if(L <= l && r <= R) return tree[rt];
44     LL ans = 0;
45     int m = (l+r) / 2;
46     PushDown(rt, m-l+1, r-m);
47     if(L <= m) ans += Query(l, m, rt*2, L, R);
48     if(m < R) ans += Query(m+1, r, rt*2+1, L, R);
49     return ans;
50 }
51 int main(){
52     int n, m, i, j, c;
53     char str[N];
54     while(~scanf("%d%d", &n, &m)){
55         for(int i = 1; i <= n; i++)
56             scanf("%d", &a[i]);
57         Build(1, n, 1);
58         while(m--){
59             scanf("%s", str);
60             if(str[0] == 'Q'){
61                 scanf("%d%d", &i, &j);
62                 printf("%lld\n", Query(1,n,1,i,j));
63             }
64             else if(str[0] == 'C'){
65                 scanf("%d%d%d", &i, &j, &c);
66                 Update(1,n,1,i,j,c);
67             }
68         }
69     }
70     return 0;
71 }
POJ-3468

再來一道

HDU-1698 

 1 #include<cstdio>
 2 #define LL long long
 3 const int N = 1e5+10;
 4 int tree[N<<2];
 5 int lazy[N<<2];
 6 int a[N];
 7 
 8 void PushUp(int rt){
 9     tree[rt] = tree[rt*2] + tree[rt*2+1];
10 }
11 void Build(int l, int r, int rt){
12     lazy[rt] = 0;
13     if(l == r){
14         tree[rt] = 1;
15         return ;
16     }
17     int m = (l+r) / 2;
18     Build(l, m, rt*2);
19     Build(m+1, r, rt*2+1);
20     PushUp(rt);
21 }
22 void PushDown(int rt, int llen, int rlen){
23     if(lazy[rt]){
24         lazy[rt*2] = lazy[rt];
25         lazy[rt*2+1] = lazy[rt];
26         tree[rt*2] = lazy[rt] * llen;
27         tree[rt*2+1] = lazy[rt] * rlen;
28         lazy[rt] = 0;
29     }
30 }
31 void Update(int l, int r, int rt, int L, int R, int C){
32     if(L <= l && r <= R){
33         tree[rt] = C*(r-l+1);
34         lazy[rt] = C;
35         return;
36     }
37     int m = (l+r) / 2;
38     PushDown(rt, m-l+1, r-m);
39     if(L <= m) Update(l, m, rt*2, L, R, C);
40     if(m < R) Update(m+1, r, rt*2+1, L, R, C);
41     PushUp(rt);
42 }
43 int main(){
44     int t, n, m, i, j, c;
45     scanf("%d", &t);
46     for(int cas = 1; cas <= t; cas++){
47         scanf("%d%d", &n, &m);
48         Build(1, n, 1);
49         while(m--){
50             scanf("%d%d%d", &i, &j, &c);
51             Update(1, n, 1, i, j, c);
52         }
53         printf("Case %d: The total value of the hook is %d.\n", cas, tree[1]);
54     }
55     return 0;
56 }
HDU-1698

 

到此關於線段樹的查詢 單點更新 區域更新都介紹完了。

還有一種特殊的思想是: 線段樹求逆序對

 


免責聲明!

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



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