給定一段區間和若干查詢關於子區間性質(比如元素總和,最大最小值等等)的請求,要求高效返回結果是很常見的要求。接下來幾篇文章我詳細說一說應對這些需求所用的資料結構。
從求子區間元素和問題說起。給定序列a[1...n],要求從sum(i,j) = a[i]+a[i+1]+...+a[j]很簡單,一個迴圈就可以了。但如果很多個查詢過來,每次都迴圈一遍就太低效了。我們希望能對結果進行緩衝,如果有相同的查詢可以直接得到結果,至少能利用之前已經算出的值減少計算量(比如求sum(1,10),之前已經算出sum(1,5)和sum(6,10)就可以直接把它們加起來返回),這就要求把區間分割分成大小不一的子區間,並形成某種層次關係,每個子區間記錄自己的總和,查詢時把目標區間分成已知子區間的並;為了查詢的高效,分解的層數要儘可能少,達到lg(n)級,這實際就是動態規劃的一種應用。不同的分解方式就產生了不同的資料結構,這一節先說線段樹。
線段樹的分法是把區間[a,b]不斷二分,直到長度為1,[1,5]的分解結果如下。
易知,線段樹必是一棵完全二叉樹(即非葉結點必有兩孩子),葉結點個數為區間長度L,總結點為2L-1個,高度O(lgL),可以把它高效地存在數組裡面。再有,給定一個區間[a,b],可以把它分解為不超過2lg(b-a)個子區間的並(用歸納法不難證),比如[2,5]可分解為[2,2],[3,3],[4,5]。這就意味著,對某個區間的查詢和修改可以在O(lgn)的時間完成。
線段樹的用途,可以是上面提到的求任意子區間和。但由於線段樹劃分區間的特性,只要所求的資訊能夠方便地用兩段子區間資訊加以組合得出,就可以用線段樹,只需要在每個結點處附加一些額外資訊。下面舉幾個例子。
1、區間染色問題 POJ 2528
題目大意是,有長度為L(1<=L<=10000000)的一條木板,操作n(1<=O<=10000)次,每次把它的一部分[a,b]貼上海報,問最後至少有一部分露在外面的海報有多少張。
最簡單的方法是FloodFill,開一個長度為L的數組,每次操作都把[a,b]之內的值改為海報的編號,最後統計不同的編號個數。但是L太大,數組開不了那麼大,所幸n很小,因此涉及的座標最多有20000個,可以只開20000的數組,把原本的座標映射到新座標上去(所謂“離散化”)。但這樣最壞情況複雜度還是O(n^2)比較大。用線段樹怎麼做呢?
容易想到,給每個結點加上額外的域,記錄這個區間被染成什麼顏色,-1表示未染色。還是以區間[1,5]為例,假設我們要[1,4]染成1號顏色,很簡單,找到[1,3],[4,4]標記為1就行了。如下
這就是懶操作。對[1,4]的染色可以不對1...4個每個值都進行修改,只需要在父區間記錄整個區間的資訊就可以了。但這樣有一個問題,假設要把[2,3]染成2號顏色該如何處理?
[2,3]分成[2,2]和[3,3],當然要把它們標記成2,但[1,3]已經不全是1號顏色了,應該標記成-1,這可以在從樹根向下搜尋的過程順便完成。但是區間[1,1]的確還是1號顏色,該把它標記成1,感覺有些繁.解決方案是標記下推。在對[1,3]修改之前把它的標記轉移到它的兩個子結點中,然後標記為-1。至於兩子結點最終是什麼顏色,在遞迴插入的過程是就處然處理好了,過程如下:
| 把[1,3]標記下推 |
插入[3,3] |
插入[2,2]時,把[1,2]標記下推 |
插入[2,2]完成 |
|
|
|
|
最後如何統計可以的編號?從根開始。如果當前結點標記不是-1,說明整個區間都是一種顏色,雖然它的子結點可能標記了其它色,但都無關緊要,因為都已經被覆蓋掉了。如果是-1,再遞迴地統計兩個子結點即可。
分析完成。這種染色常擴充到二維矩形,基本還是離散化+二維線段樹,以後再說。
參考代碼
2、區間和問題 POJ2750
大意是長度為n(小於100000)的一個環,可以任意更改某個值的大小,要求每次修改後都輸出當前環中最大的連續子序列和,要求這個子序列不能是整個環。共修改m次(也小於100000)。
先處理環的問題。把環切開,可以構成0,1,...,n-1的數組。如果子序列完全在[0,n-1]之間就容易了。問題是它可能跨越邊界,分成[0,i],[j,n-1]的兩段。這種情況下,[i+1,j-1]的和必定是最小連續序列和,這樣剩下的才能最大,用序列總和減掉就能得到最大和。可以發現,線段樹的結點[a,b]要維護3個主要資訊,[a,b]的總和,最小連續和,最大連續和。
接下來是如何從兩個子區間的資訊獲得父區間的資訊。總和容易,簡單加一下就好。最大連續和呢?取子區間的較大者?顯然不行。比如兩個子區間{4,3,-1000,5},{5,-1000,3 4},最大連續和都是7,但合起來最大連續和是10.關鍵在於拼接的地方可能產生更大的和。因此還需要記錄從區間左端和右端開始分別取得的最大連續和lmax,rmax是多少,比較left_child->rmax,right_child->lmax,left_child->max_sum,right_child->max_sum取較大者。最小連續和也是如此。這樣區間資訊的維護就簡單了。
對於修改操作,還是遞迴。從根結點開始。如果到達葉結點,修改後返回;否則讓子結點先修改,再根據修改後的子結點更新自己的資訊。
最後,如果根結點的最大連續和是整個環的總和,根據題目要求是不行了,這時減掉最小連續和就行了。
參考代碼
3、順序統計問題 POJ 2828
順序統計是很廣泛的一類問題。由於線段樹結點的有序性,在結點中附加子樹元素個數可以方便地實現順序統計。比如O(nlgn)的計算逆序對(其實這種方法用二叉搜尋樹的變形順序統計樹更好,演算法導論上有),另一種類似歸併排序的分治法大家都會。這裡要說的問題很有意思。
題目是說買火車票經常有人插隊。假設現在一個人都沒有,給出n(可達200000)個人的入隊情況,a b表示編號為b的人插到了當前第a個人後面,求出最後隊伍的排列。
n較小的話,用鏈表是很方便的。瓶頸在於要找到隊伍的第i個人不得不從鏈表開始搜尋,很慢。可以想到把鏈表變成二維形成二叉樹的結構,每個結點記錄以它為根的子樹有多少元素,這樣找第i個元素就是O(lgn)。舉例如下,結點左邊的值表示人的編號,右邊表示子樹大小。假如有人插隊到第5個人後面,可以發現,根結點的左子樹有3個人,加上根結點自己有4個人,新來的人必定要插入到右子樹中。基本思想就是這樣,不是本文重點,不再細說。
不幸的是,直接用這種方法寫順序統計樹逾時了。究其原因,二叉樹可能非常不平衡,很容易構造資料使它變成一條鏈,只有使用平衡二叉樹才行,無論哪一種寫起來都很麻煩。線段樹本身就是平衡的,但直接用線段樹不容易,因為線段樹要求已經插入的元素不能再改位置,顯然不合要求。
換個方向想,如果反著插的話,最後的插入的人位置肯定是固定的。這樣可以給之前存在的人留下空位,插到第i個人後面等價於插到第i個空位後面。這樣每個人插入後位置就固定了。只需要維護當前區間有多少個空位就行。
參考代碼
這三種應該是線段樹最常用的用途,當然它的變體非常多,不好完善總結,可以多到網上找一些題看看。
下一節講線段樹的簡化版,樹狀數組
NPBool原創,轉載請註明。
給定一段區間和若干查詢關於子區間性質(比如元素總和,最大最小值等等)的請求,要求高效返回結果是很常見的要求。接下來幾篇文章我詳細說一說應對這些需求所用的資料結構。
從求子區間元素和問題說起。給定序列a[1...n],要求從sum(i,j) = a[i]+a[i+1]+...+a[j]很簡單,一個迴圈就可以了。但如果很多個查詢過來,每次都迴圈一遍就太低效了。我們希望能對結果進行緩衝,如果有相同的查詢可以直接得到結果,至少能利用之前已經算出的值減少計算量(比如求sum(1,10),之前已經算出sum(1,5)和sum(6,10)就可以直接把它們加起來返回),這就要求把區間分割分成大小不一的子區間,並形成某種層次關係,每個子區間記錄自己的總和,查詢時把目標區間分成已知子區間的並;為了查詢的高效,分解的層數要儘可能少,達到lg(n)級,這實際就是動態規劃的一種應用。不同的分解方式就產生了不同的資料結構,這一節先說線段樹。
線段樹的分法是把區間[a,b]不斷二分,直到長度為1,[1,5]的分解結果如下。
易知,線段樹必是一棵完全二叉樹(即非葉結點必有兩孩子),葉結點個數為區間長度L,總結點為2L-1個,高度O(lgL),可以把它高效地存在數組裡面。再有,給定一個區間[a,b],可以把它分解為不超過2lg(b-a)個子區間的並(用歸納法不難證),比如[2,5]可分解為[2,2],[3,3],[4,5]。這就意味著,對某個區間的查詢和修改可以在O(lgn)的時間完成。
線段樹的用途,可以是上面提到的求任意子區間和。但由於線段樹劃分區間的特性,只要所求的資訊能夠方便地用兩段子區間資訊加以組合得出,就可以用線段樹,只需要在每個結點處附加一些額外資訊。下面舉幾個例子。
1、區間染色問題 POJ 2528
題目大意是,有長度為L(1<=L<=10000000)的一條木板,操作n(1<=O<=10000)次,每次把它的一部分[a,b]貼上海報,問最後至少有一部分露在外面的海報有多少張。
最簡單的方法是FloodFill,開一個長度為L的數組,每次操作都把[a,b]之內的值改為海報的編號,最後統計不同的編號個數。但是L太大,數組開不了那麼大,所幸n很小,因此涉及的座標最多有20000個,可以只開20000的數組,把原本的座標映射到新座標上去(所謂“離散化”)。但這樣最壞情況複雜度還是O(n^2)比較大。用線段樹怎麼做呢?
容易想到,給每個結點加上額外的域,記錄這個區間被染成什麼顏色,-1表示未染色。還是以區間[1,5]為例,假設我們要[1,4]染成1號顏色,很簡單,找到[1,3],[4,4]標記為1就行了。如下
這就是懶操作。對[1,4]的染色可以不對1...4個每個值都進行修改,只需要在父區間記錄整個區間的資訊就可以了。但這樣有一個問題,假設要把[2,3]染成2號顏色該如何處理?
[2,3]分成[2,2]和[3,3],當然要把它們標記成2,但[1,3]已經不全是1號顏色了,應該標記成-1,這可以在從樹根向下搜尋的過程順便完成。但是區間[1,1]的確還是1號顏色,該把它標記成1,感覺有些繁.解決方案是標記下推。在對[1,3]修改之前把它的標記轉移到它的兩個子結點中,然後標記為-1。至於兩子結點最終是什麼顏色,在遞迴插入的過程是就處然處理好了,過程如下:
| 把[1,3]標記下推 |
插入[3,3] |
插入[2,2]時,把[1,2]標記下推 |
插入[2,2]完成 |
|
|
|
|
最後如何統計可以的編號?從根開始。如果當前結點標記不是-1,說明整個區間都是一種顏色,雖然它的子結點可能標記了其它色,但都無關緊要,因為都已經被覆蓋掉了。如果是-1,再遞迴地統計兩個子結點即可。
分析完成。這種染色常擴充到二維矩形,基本還是離散化+二維線段樹,以後再說。
參考代碼
2、區間和問題 POJ2750
大意是長度為n(小於100000)的一個環,可以任意更改某個值的大小,要求每次修改後都輸出當前環中最大的連續子序列和,要求這個子序列不能是整個環。共修改m次(也小於100000)。
先處理環的問題。把環切開,可以構成0,1,...,n-1的數組。如果子序列完全在[0,n-1]之間就容易了。問題是它可能跨越邊界,分成[0,i],[j,n-1]的兩段。這種情況下,[i+1,j-1]的和必定是最小連續序列和,這樣剩下的才能最大,用序列總和減掉就能得到最大和。可以發現,線段樹的結點[a,b]要維護3個主要資訊,[a,b]的總和,最小連續和,最大連續和。
接下來是如何從兩個子區間的資訊獲得父區間的資訊。總和容易,簡單加一下就好。最大連續和呢?取子區間的較大者?顯然不行。比如兩個子區間{4,3,-1000,5},{5,-1000,3 4},最大連續和都是7,但合起來最大連續和是10.關鍵在於拼接的地方可能產生更大的和。因此還需要記錄從區間左端和右端開始分別取得的最大連續和lmax,rmax是多少,比較left_child->rmax,right_child->lmax,left_child->max_sum,right_child->max_sum取較大者。最小連續和也是如此。這樣區間資訊的維護就簡單了。
對於修改操作,還是遞迴。從根結點開始。如果到達葉結點,修改後返回;否則讓子結點先修改,再根據修改後的子結點更新自己的資訊。
最後,如果根結點的最大連續和是整個環的總和,根據題目要求是不行了,這時減掉最小連續和就行了。
參考代碼
3、順序統計問題 POJ 2828
順序統計是很廣泛的一類問題。由於線段樹結點的有序性,在結點中附加子樹元素個數可以方便地實現順序統計。比如O(nlgn)的計算逆序對(其實這種方法用二叉搜尋樹的變形順序統計樹更好,演算法導論上有),另一種類似歸併排序的分治法大家都會。這裡要說的問題很有意思。
題目是說買火車票經常有人插隊。假設現在一個人都沒有,給出n(可達200000)個人的入隊情況,a b表示編號為b的人插到了當前第a個人後面,求出最後隊伍的排列。
n較小的話,用鏈表是很方便的。瓶頸在於要找到隊伍的第i個人不得不從鏈表開始搜尋,很慢。可以想到把鏈表變成二維形成二叉樹的結構,每個結點記錄以它為根的子樹有多少元素,這樣找第i個元素就是O(lgn)。舉例如下,結點左邊的值表示人的編號,右邊表示子樹大小。假如有人插隊到第5個人後面,可以發現,根結點的左子樹有3個人,加上根結點自己有4個人,新來的人必定要插入到右子樹中。基本思想就是這樣,不是本文重點,不再細說。
不幸的是,直接用這種方法寫順序統計樹逾時了。究其原因,二叉樹可能非常不平衡,很容易構造資料使它變成一條鏈,只有使用平衡二叉樹才行,無論哪一種寫起來都很麻煩。線段樹本身就是平衡的,但直接用線段樹不容易,因為線段樹要求已經插入的元素不能再改位置,顯然不合要求。
換個方向想,如果反著插的話,最後的插入的人位置肯定是固定的。這樣可以給之前存在的人留下空位,插到第i個人後面等價於插到第i個空位後面。這樣每個人插入後位置就固定了。只需要維護當前區間有多少個空位就行。
參考代碼
這三種應該是線段樹最常用的用途,當然它的變體非常多,不好完善總結,可以多到網上找一些題看看。
下一節講線段樹的簡化版,樹狀數組
NPBool原創,轉載請註明。