1. 程式人生 > >數據結構之線段樹

數據結構之線段樹

級別 初始 標記 tree clas 表示 概述 左右 傳遞

1、概述

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

2、線段樹基本操作

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

(1) 線段樹構造

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

技術分享

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //構造求解區間最小值的線段樹 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]範圍的最小值。同理,對於其他詢問的區間,也都可以找到若幹個相連的區間,合並後可以得到詢問的區間。

區間查詢的偽代碼如下:

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 // 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 的標記。代碼框架為:

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 // 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/

數據結構之線段樹