線段樹

1、概述

線段樹,也叫區間樹,是一個完全二叉樹,它在各個節點保存一條線段(即“子數組”),因而常用於解決數列維護問題,它基本能保證每個操作的複雜度爲O(lgN)。

2、線段樹基本操作

線段樹的基本操作主要包括構造線段樹,區間查詢和區間修改。

(1)    線段樹構造

首先介紹構造線段樹的方法:讓根節點表示區間[0,N-1],即所有N個數所組成的一個區間,然後,把區間分成兩半,分別由左右子樹表示。不難證明,這樣的線段樹的節點數只有2N-1個,是O(N)級別的,如圖:


顯然,構造線段樹是一個遞歸的過程,僞代碼如下:

//構造求解區間最小值的線段樹
 
function 構造以v爲根的子樹
 
  if v所表示的區間內只有一個元素
 
     v區間的最小值就是這個元素, 構造過程結束
 
  end if
 
  把v所屬的區間一分爲二,用w和x兩個節點表示。
 
  標記v的左兒子是w,右兒子是x
 
  分別構造以w和以x爲根的子樹(遞歸)
 
  v區間的最小值 <- min(w區間的最小值,x區間的最小值)
 
end function

線段樹除了最後一層外,前面每一層的結點都是滿的,因此線段樹的深度

h =ceil(log(2n -1))=O(log n)。

(2)    區間查詢

區間查詢指用戶輸入一個區間,獲取該區間的有關信息,如區間中最大值,最小值,第N大的值等。

比如前面一個圖中所示的樹,如果詢問區間是[0,2],或者詢問的區間是[3,3],不難直接找到對應的節點回答這一問題。但並不是所有的提問都這麼容易回答,比如[0,3],就沒有哪一個節點記錄了這個區間的最小值。當然,解決方法也不難找到:把[0,2]和[3,3]兩個區間(它們在整數意義上是相連的兩個區間)的最小值“合併”起來,也就是求這兩個最小值的最小值,就能求出[0,3]範圍的最小值。同理,對於其他詢問的區間,也都可以找到若干個相連的區間,合併後可以得到詢問的區間。

區間查詢的僞代碼如下:

// node 爲線段樹的結點類型,其中Left 和Right 分別表示區間左右端點
 
// Lch 和Rch 分別表示指向左右孩子的指針
 
void Query(node *p, int a, int b) // 當前考察結點爲p,查詢區間爲(a,b]
 
{
 
  if (a <= p->Left && p->Right <= b)
 
  // 如果當前結點的區間包含在查詢區間內
 
  {
 
     ...... // 更新結果
 
     return;
 
  }
 
  Push_Down(p); // 等到下面的修改操作再解釋這句
 
  int mid = (p->Left + p->Right) / 2; // 計算左右子結點的分隔點
 
  if (a < mid) Query(p->Lch, a, b); // 和左孩子有交集,考察左子結點
 
  if (b > mid) Query(p->Rch, a, b); // 和右孩子有交集,考察右子結點
 
}

可見,這樣的過程一定選出了儘量少的區間,它們相連後正好涵蓋了整個[l,r],沒有重複也沒有遺漏。同時,考慮到線段樹上每層的節點最多會被選取2個,一共選取的節點數也是O(log n)的,因此查詢的時間複雜度也是O(log n)。

線段樹並不適合所有區間查詢情況,它的使用條件是“相鄰的區間的信息可以被合併成兩個區間的並區間的信息”。即問題是可以被分解解決的。

(3)    區間修改

當用戶修改一個區間的值時,如果連同其子孫全部修改,則改動的節點數必定會遠遠超過O(log n)個。因而,如果要想把區間修改操作也控制在O(log n)的時間內,只修改O(log n)個節點的信息就成爲必要。

借鑑前一節區間查詢用到的思路:區間修改時如果修改了一個節點所表示的區間,也不用去修改它的兒子節點。然而,對於被修改節點的祖先節點,也必須更新它所記錄的值,否則查詢操作就肯定會出問題(正如修改單個節點的情況一樣)。

這些選出的節點的祖先節點直接更新值即可,而選出的節點的子孫卻顯然不能這麼簡單地處理:每個節點的值必須能由兩個兒子節點的值得到,如這幅圖中的例子:


這裏,節點[0,1]的值應該是4,但是兩個兒子的值又分別是3和5。如果查詢[0,0]區間的RMQ,算出來的結果會是3,而正確答案顯然是4。

問題顯然在於,儘管修改了一個節點以後,不用修改它的兒子節點,但是它的兒子節點的信息事實上已經被改變了。這就需要我們在節點裏增設一個域:標記。把對節點的修改情況儲存在標記裏面,這樣,當我們自上而下地訪問某節點時,就能把一路上所遇到的所有標記都考慮進去。

但是,在一個節點帶上標記時,會給更新這個節點的值帶來一些麻煩。繼續上面的例子,如果我把位置0的數字從4改成了3,區間[0,0]的值應該變回3,但實際上,由於區間[0,1]有一個“添加了1”的標記,如果直接把值修改爲3,則查詢區間[0,0]的時候我們會得到3+1=4這個錯誤結果。但是,把這個3改成2,雖然正確,卻並不直觀,更不利於推廣(參見下面的一個例子)。

爲此我們引入延遲標記的一些概念。每個結點新增加一個標記,記錄這個結點是否被進行了某種修改操作(這種修改操作會影響其子結點)。還是像上面的一樣,對於任意區間的修改,我們先按照查詢的方式將其劃分成線段樹中的結點,然後修改這些結點的信息,並給這些結點標上代表這種修改操作的標記。在修改和查詢的時候,如果我們到了一個結點p ,並且決定考慮其子結點,那麼我們就要看看結點p 有沒有標記,如果有,就要按照標記修改其子結點的信息,並且給子結點都標上相同的標記,同時消掉p 的標記。代碼框架爲:

// node 爲線段樹的結點類型,其中Left 和Right 分別表示區間左右端點
 
// Lch 和Rch 分別表示指向左右孩子的指針
 
void Change(node *p, int a, int b) // 當前考察結點爲p,修改區間爲(a,b]
 
{
 
  if (a <= p->Left && p->Right <= b)
 
  // 如果當前結點的區間包含在修改區間內
 
  {
 
     ...... // 修改當前結點的信息,並標上標記
 
     return;
 
  }
 
  Push_Down(p); // 把當前結點的標記向下傳遞
 
  int mid = (p->Left + p->Right) / 2; // 計算左右子結點的分隔點
 
  if (a < mid) Change(p->Lch, a, b); // 和左孩子有交集,考察左子結點
 
  if (b > mid) Change(p->Rch, a, b); // 和右孩子有交集,考察右子結點
 
  Update(p); // 維護當前結點的信息(因爲其子結點的信息可能有更改)
 
}

3、應用

下面給出線段樹的幾個應用:

(1)有一列數,初始值全部爲0。每次可以進行以下三種操作中的一種:

a. 給指定區間的每個數加上一個特定值;

b.將指定區間的所有數置成一個統一的值;

c.詢問一個區間上的最小值、最大值、所有數的和。

給出一系列a.b.操作後,輸出c的結果。

[問題分析]

這個是典型的線段樹的應用。在每個節點上維護一下幾個變量:delta(區間增加值),same(區間被置爲某個值),min(區間最小值),max(區間最大值),sum(區間和),其中delta和same屬於“延遲標記”。

(2)在所有不大於30000的自然數範圍內討論一個問題:已知n條線段,把端點依次輸入給你,然後有m(≤30000)個詢問,每個詢問輸入一個點,要求這個點在多少條線段上出現過。

[問題分析]

在這個問題中,我們可以直接對問題處理的區間建立線段樹,在線段樹上維護區間被覆蓋的次數。將n條線段插入線段樹,然後對於詢問的每個點,直接查詢被覆蓋的次數即可。

但是我們在這裏用這道題目,更希望能夠說明一個問題,那就是這道題目完全可以不用線段樹。我們將每個線段拆成(L,+1),(R+1,-1)的兩個事件點,每個詢問點也在對應座標處加上一個詢問的事件點,排序之後掃描就可以完成題目的詢問。我們這裏討論的問題是一個離線的問題,因此我們也設計出了一個很簡單的離線算法。線段樹在處理在線問題的時候會更加有效,因爲它維護了一個實時的信息。

這個題目也告訴我們,有的題目儘管可以使用線段樹處理,但是如果我們能夠抓住題目的特點,就可能獲得更加優秀的算法。

(3)某次列車途經C個城市,城市編號依次爲1到C,列車上共有S個座位,鐵路局規定售出的車票只能是坐票,即車上所有的旅客都有座,售票系統是由計算機執行的,每一個售票申請包含三個參數,分別用O、D、N表示,O爲起始站,D爲目的地站,N爲車票張數,售票系統對該售票申請作出受理或不受理的決定,只有在從O到D的區段內列車上都有N個或N個以上的空座位時該售票申請才被受理,請你寫一個程序,實現這個自動售票系統。

[問題分析]

這裏我們可以把所有的車站順次放在一個數軸上,在數軸上建立線段樹,在線段樹上維護區間的delta與max。每次判斷一個售票申請是否可行就是查詢區間上的最大值;每個插入一個售票請求,就是給一個區間上所有的元素加上購票數。

這道題目在線段樹上維護的信息既包括自下至上的遞推,也包括了自上至下的傳遞,能夠比較全面地對線段樹的基本操作進行訓練。

(4)給一個n*n的方格棋盤,初始時每個格子都是白色。現在要刷M次黑色或白色的油漆。每次刷漆的區域都是一個平行棋盤邊緣的矩形區域。

輸入n,M,以及每次刷漆的區域和顏色,輸出刷了M次之後棋盤上還有多少個棋格是白色。

[問題分析]

首先我們從簡單入手,考慮一維的問題。即對於一個長度爲n的白色線段,對它進行M次修改(每次更新某一子區域的顏色)。問最後還剩下的白色區域有多長。

對於這個問題,很容易想到建立一棵線段樹的模型。複雜度爲O(Mlgn)。

擴展到二維,需要把線段樹進行調整,即首先在橫座標上建立線段樹,它的每個節點是一棵建立在縱座標上的線段樹(即樹中有樹。稱爲二維線段樹)。複雜度爲O(M(logn)^2)。


4、總結

利用線段樹,我們可以高效地詢問和修改一個數列中某個區間的信息,並且代碼也不算特別複雜。

但是線段樹也是有一定的侷限性的,其中最明顯的就是數列中數的個數必須固定,即不能添加或刪除數列中的數。

5、參考資料

(1)    楊弋文章:《線段樹》:

http://download.csdn.net/source/2255479

(2)    林濤文章《線段樹的應用》:

http://wenku.baidu.com/view/d65cf31fb7360b4c2e3f64ac.html

(3)    朱全民文章《線段樹及其應用》:

http://wenku.baidu.com/view/437ad3bec77da26925c5b0ba.html

(4)    線段樹:

http://wenku.baidu.com/view/32652a2d7375a417866f8f51.html

原創文章,轉載請註明: 轉載自董的博客

本文鏈接地址: http://dongxicheng.org/structure/segment-tree/




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章