1. 程式人生 > >【詳解】線段樹

【詳解】線段樹

線段樹詳解
By 巖之痕
目錄: 一:綜述 二:原理 三:遞迴實現 四:非遞迴原理 五:非遞迴實現 六:線段樹解題模型 七:掃描線 八:可持久化 (主席樹) 九:練習題


一:綜述

假設有編號從1到n的n個點,每個點都存了一些資訊,用[L,R]表示下標從L到R的這些點。 線段樹的用處就是,對編號連續的一些點進行修改或者統計操作,修改和統計的複雜度都是O(log2(n)).

線段樹的原理,就是,將[1,n]分解成若干特定的子區間(數量不超過4*n),然後,將每個區間[L,R]都分解為 少量特定的子區間,通過對這些少量子區間的修改或者統計,來實現快速對[L,R]的修改或者統計。
由此看出,用線段樹統計的東西,必須符合區間加法,否則,不可能通過分成的子區間來得到[L,R]的統計結果。
符合區間加法的例子: 數字之和——總數字之和 = 左區間數字之和 + 右區間數字之和 最大公因數(GCD)——總GCD = gcd( 左區間GCD , 右區間GCD ); 最大值——總最大值=max(左區間最大值,右區間最大值)
不符合區間加法的例子: 眾數——只知道左右區間的眾數,沒法求總區間的眾數 01序列的最長連續零——只知道左右區間的最長連續零,沒法知道總的最長連續零
一個問題,只要能化成對一些連續點的修改和統計問題,基本就可以用線段樹來解決了,具體怎麼轉化在第六節會講。 由於點的資訊可以千變萬化,所以線段樹是一種非常靈活的資料結構,可以做的題的型別特別多,只要會轉化。 線段樹當然是可以維護線段資訊的,因為線段資訊也是可以轉換成用點來表達的(每個點代表一條線段)。 所以在以下對結構的討論中,都是對點的討論,線段和點的對應關係在第七節掃描線中會講。

本文二到五節是講對線段樹操作的原理和實現。 六到八節介紹了線段樹解題模型,以及一些例題。
初學者可以先看這篇文章: 線段樹從零開始

二:原理

(注:由於線段樹的每個節點代表一個區間,以下敘述中不區分節點和區間,只是根據語境需要,選擇合適的詞)
線段樹本質上是維護下標為1,2,..,n的n個按順序排列的數的資訊,所以,其實是“點樹”,是維護n的點的資訊,至於每個點的資料的含義可以有很多, 在對線段操作的線段樹中,每個點代表一條線段,在用線段樹維護數列資訊的時候,每個點代表一個數,但本質上都是每個點代表一個數。以下,在討論線段樹的時候,區間[L,R]指的是下標從L到R的這(R-L+1)個數,而不是指一條連續的線段。只是有時候這些數代表實際上一條線段的統計結果而已。

線段樹是將每個區間[L,R]分解成[L,M]和[M+1,R] (其中M=(L+R)/2 這裡的除法是整數除法,即對結果下取整)直到 L==R 為止。  開始時是區間[1,n] ,通過遞迴來逐步分解,假設根的高度為1的話,樹的最大高度為(n>1)。 線段樹對於每個n的分解是唯一的,所以n相同的線段樹結構相同,這也是實現可持久化線段樹的基礎。 下圖展示了區間[1,13]的分解過程:

上圖中,每個區間都是一個節點,每個節點存自己對應的區間的統計資訊。

(1)線段樹的點修改:


假設要修改[5]的值,可以發現,每層只有一個節點包含[5],所以修改了[5]之後,只需要每層更新一個節點就可以線段樹每個節點的資訊都是正確的,所以修改次數的最大值為層數 複雜度O(log2(n))


(2)線段樹的區間查詢:


線段樹能快速進行區間查詢的基礎是下面的定理: 定理:n>=3時,一個[1,n]的線段樹可以將[1,n]的任意子區間[L,R]分解為不超過個子區間。 這樣,在查詢[L,R]的統計值的時候,只需要訪問不超過個節點,就可以獲得[L,R]的統計資訊,實現了O(log2(n))的區間查詢。
下面給出證明:
(2.1)先給出一個粗略的證明(結合下圖): 先考慮樹的最下層,將所有在區間[L,R]內的點選中,然後,若相鄰的點的直接父節點是同一個,那麼就用這個父節點代替這兩個節點(父節點在上一層)。這樣操作之後,本層最多剩下兩個節點。若最左側被選中的節點是它父節點的右子樹,那麼這個節點會被剩下。若最右側被選中的節點是它的父節點的左子樹,那麼這個節點會被剩下。中間的所有節點都被父節點取代。 對最下層處理完之後,考慮它的上一層,繼續進行同樣的處理,可以發現,每一層最多留下2個節點,其餘的節點升往上一層,這樣可以說明分割成的區間(節點)個數是大概是樹高的兩倍左右。
下圖為n=13的線段樹,區間[2,12],按照上面的敘述進行操作的過程圖:
由圖可以看出:在n=13的線段樹中,[2,12]=[2] + [3,4] + [5,7] + [8,10] + [11,12] 。

(2.2)然後給出正式一點的證明: 定理:n>=3時,一個[1,n]的線段樹可以將[1,n]的任意子區間[L,R]分解為不超過個子區間。

用數學歸納法,證明上面的定理: 首先,n=3,4,5時,用窮舉法不難證明定理成立。 假設對於n= 3,4,5,...,k-1上式都成立,下面來證明對於n=k ( k>=6 )成立: 分為4種情況來證明:
情況一:[L,R]包含根節點(L=1且R=n),此時,[L,R]被分解為了一個節點,定理成立。
情況二:[L,R]包含根節點的左子節點,此時[L,R]一定不包含根的右子節點(因為如果包含,就可以合併左右子節點, 用根節點替代,此時就是情況一)。這時,以右子節點為根的這個樹的元素個數為 [L,R]分成的子區間由兩部分組成: 一:根的左子結點,區間數為1  二:以根的右子節點為根的樹中,進行區間查詢,這個可以遞迴使用本定理。 由歸納假設可得,[L,R]一共被分成了個區間。 情況三:跟情況二對稱,不一樣的是,以根的左子節點為根的樹的元素個數為 [L,R]一共被分成了個區間。 從公式可以看出,情況二的區間數小於等於情況三的區間數,於是只需要證明情況三的區間數符合條件就行了。 於是,情況二和情況三定理成立。
情況四:[L,R]不包括根節點以及根節點的左右子節點。 於是,剩下的層,每層最多兩個節點(參考粗略證明中的內容)。 於是[L,R]最多被分解成了個區間,定理成立。  

上面只證明了是上界,但是,其實它是最小上界。 n=3,4時,有很多組區間的分解可以達到最小上界。 當n>4時,當且僅當n=2^t (t>=3),L=2,R=2^t -1 時,區間[L,R]的分解可以達到最小上界 就不證明了,有興趣可以自己去證明。 下圖是n=16 , L=2 , R=15 時的操作圖,此圖展示了達到最小上界的樹的結構。





(3)線段樹的區間修改:

線段樹的區間修改也是將區間分成子區間,但是要加一個標記,稱作懶惰標記。 標記的含義: 本節點的統計資訊已經根據標記更新過了,但是本節點的子節點仍需要進行更新。 即,如果要給一個區間的所有值都加上1,那麼,實際上並沒有給這個區間的所有值都加上1,而是打個標記,記下來,這個節點所包含的區間需要加1.打上標記後,要根據標記更新本節點的統計資訊,比如,如果本節點維護的是區間和,而本節點包含5個數,那麼,打上+1的標記之後,要給本節點維護的和+5。這是向下延遲修改,但是向上顯示的資訊是修改以後的資訊,所以查詢的時候可以得到正確的結果。有的標記之間會相互影響,所以比較簡單的做法是,每遞迴到一個區間,首先下推標記(若本節點有標記,就下推標記),然後再打上新的標記,這樣仍然每個區間操作的複雜度是O(log2(n))。
標記有相對標記絕對標記之分: 相對標記是將區間的所有數+a之類的操作,標記之間可以共存,跟打標記的順序無關(跟順序無關才是重點)。 所以,可以在區間修改的時候不下推標記,留到查詢的時候再下推。       注意:如果區間修改時不下推標記,那麼PushUp函式中,必須考慮本節點的標記。                  而如果所有操作都下推標記,那麼PushUp函式可以不考慮本節點的標記,因為本節點的標記一定已經被下推了(也就是對本節點無效了) 絕對標記是將區間的所有數變成a之類的操作,打標記的順序直接影響結果, 所以這種標記在區間修改的時候必須下推舊標記,不然會出錯。
注意,有多個標記的時候,標記下推的順序也很重要,錯誤的下推順序可能會導致錯誤。
之所以要區分兩種標記,是因為非遞迴線段樹只能維護相對標記。 因為非遞迴線段樹是自底向上直接修改分成的每個子區間,所以根本做不到在區間修改的時候下推標記。 非遞迴線段樹一般不下推標記,而是自下而上求答案的過程中,根據標記更新答案。

(4)線段樹的儲存結構:

線段樹是用陣列來模擬樹形結構,對於每一個節點R ,左子節點為 2*R (一般寫作R<<1)右子節點為 2*R+1(一般寫作R<<1|1) 然後以1為根節點,所以,整體的統計資訊是存在節點1中的。 這麼表示的原因看下圖就很明白了,左子樹的節點標號都是根節點的兩倍,右子樹的節點標號都是左子樹+1:
線段樹需要的陣列元素個數是:,一般都開4倍空間,比如: int A[n<<2];

三:遞迴實現

以下以維護數列區間和的線段樹為例,演示最基本的線段樹程式碼。

(0)定義:

[cpp]  view plain  copy
  1. #define maxn 100007  //元素總個數  
  2. #define ls l,m,rt<<1  
  3. #define rs m+1,r,rt<<1|1  
  4. int Sum[maxn<<2],Add[maxn<<2];//Sum求和,Add為懶惰標記   
  5. int A[maxn],n;//存原陣列資料下標[1,n]   

(1)建樹:

[cpp]  view plain  copy
  1. //PushUp函式更新節點資訊 ,這裡是求和  
  2. void PushUp(int rt){Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}  
  3. //Build函式建樹   
  4. void Build(int l,int r,int rt){ //l,r表示當前節點區間,rt表示當前節點編號  
  5.     if(l==r) {//若到達葉節點   
  6.         Sum[rt]=A[l];//儲存陣列值   
  7.         return;  
  8.     }  
  9.     int m=(l+r)>>1;  
  10.     //左右遞迴   
  11.     Build(l,m,rt<<1);  
  12.     Build(m+1,r,rt<<1|1);  
  13.     //更新資訊   
  14.     PushUp(rt);  
  15. }  


(2)點修改:

假設A[L]+=C:

相關推薦

線段

線段樹詳解 By 巖之痕 目錄: 一:綜述 二:原理 三:遞迴實現 四:非遞迴原理 五:非遞迴實現 六:線段樹解題模型 七:掃描線 八:可持久化

狀陣列

引入 如果給你n個數,然後進行q次詢問,每次詢問一個區間[x,y]的和,你會怎麼做? 第一種方法:最簡單的方法,用陣列存起來,每次列舉x-y,ans加起來就可以,時間複雜度O(qn),十分慢。 第二種方法:或許大多數人會使用字首和陣列:sum[i]=a[1]+a[2]+…+a[

(轉載)--SG函數和SG定理

nbsp 發現 方式 spa 賦值 problem eve 查詢 mex 在介紹SG函數和SG定理之前我們先介紹介紹必勝點與必敗點吧. 必勝點和必敗點的概念: P點:必敗點,換而言之,就是誰處於此位置,則在雙方操作正確的情況下必敗。 N

出錯記錄線段

線段樹 com img block width 然而 spa eight ron ① 2018-02-09 (20:05:31開始)    重寫了一遍還是這樣: 原因: 沒加 #include <iostream>. ② 2018-02-10

KMP算法

是不是 代碼 ++ 大牛 bilibili 開始 最長 [] 分別是 前言 KMP算法是學習數據結構 中的一大難點,不是說它有多難,而是它這個東西真的很難理解(反正我是這麽感覺的,這兩天我一直在研究KMP算法,總算感覺比較理解了這套算法, 在這裏我將自己的思路分享給大家

銀行信用評分卡中的WOE在幹什麼?WOE的意義?為什麼可以使用WOE值代替原來的特徵值來做LR的訓練輸入資料

其實我是帶著這個問題發現這篇帖子的 為什麼可以使用WOE值代替原來的特徵值來做LR的訓練輸入資料 以下為原文 https://zhuanlan.zhihu.com/p/30026040 WOE & IV woe全稱叫Weight of Evidence,常用在風險評估、授

快速冪 二進位制 取模

本來想昨天寫的   看到了cod:ww2  我:我就玩一把,真的,就一把 然後就到了12點 真香~ 程式碼如下 不想理解可以直接拿來用 時間複雜度 logn typedef long long ll; ll quickmod(ll n) {

啟發式合併線段,平衡

【啟發式合併】線段樹,平衡樹 啟發式合併就是一種複雜度可以證明的貪心合併 平衡樹啟發式合併: 對於平衡樹的啟發式合併,我們將一個 $size$ 較小平衡樹一個一個結點暴力加入 $size$ 較大的平衡樹中 最壞時間複雜度是玄學的 $O(N log^{2} N)$ 空間複雜度 $O(N)$ 模板題:

C++ MFC程序間通訊之剪貼簿

Windows剪貼簿是一種比較簡單的程序間通訊機制,同時它的開銷相對較小。它的實現原理很簡單,其實就是由由作業系統維護的一塊記憶體區域, 這塊記憶體區域不屬於任何單獨的程序,但是每一個程序又都可以訪問這塊記憶體區域,當一個程序將資料放到該記憶體區域中,而另一個

HTTP協議——經典面試題(轉載)

http請求由三部分組成,分別是:請求行、訊息報頭、請求正文 HTTP(超文字傳輸協議)是一個基於請求與響應模式的、無狀態的、應用層的協議,常基於TCP的連線方式,HTTP1.1版本中給出一種持續連線的機制,絕大多數的Web開發,都是構建在HTTP協議之上的Web應用。

js網站國際化,多國語言切換

JS網站國際化,多國語言切換【詳解】 作者:Anmbition 在web開發過程中通常會碰到需要多國語言支援的需求,我也看過一些文件,但寫的都不盡人意,最終我整理了一套通過js程式碼完成解決方案,並對程式碼進行了很大程度的優化,在使用過程中只需極少的程式碼即可完成。 第一步:核心JS

快速冪&龜速乘&快速乘

我相信進來看的人都會快速冪,對吧(和善的眼神) 如果不會。。。。那我們現在開始講吧(要不然為什麼叫詳解2333 ) 如果已經知道,就跳到下面去看吧~ 1. 快速冪 1.0 快速冪的誕生——最初的思路 我們通常需要求解形如 ab mod c 的式子,當b比較小的時

CS231n assignment1KNN中不使用迴圈計算距離:從原理到程式

本文主要講述不使用迴圈結構來計算兩個矩陣的歐氏距離, 設訓練集矩陣為train,size為num_train * num_features,設驗證集矩陣為validate,size為num_test,num_features。 因此我們計算每一個驗證集樣本到訓練集樣本的距離,就是將訓練集

JNI(Java Native Interface)

前言: 一提到JNI,多數程式設計者會下意識地感受到一種無法言喻的恐懼。它給人的第一感覺就是"難",因為它不是單純地在JVM環境內操作Java程式碼,而是跳出虛擬機器與其他程式語言進行互動。   你可能至今還沒聽說過這個技術,但是如果你是一個原始碼愛好者,或者有翻閱過JDK的一些原始碼,那你一定有接觸過nat

CopyOnWriteArrayList

前言   之前看《Java併發程式設計》這本書的時候,有看到這個,只記得"讀多寫少"、"寫入時複製"。書中沒有過多講述,只是一筆帶過(不過現在回頭看,發現講的都是精髓。老外的書大多重理論,喜歡花大篇幅講概念,這點我非常喜歡)記得當時是覺得可能有點難,先跳過了,結果就忘記回頭看了。今天突然想起來,就看了一下,整

以銀行零售業務為例,一個案例說清楚視覺化微服務架構_Kubernetes中文社群

Part 1: API設計和策略 軟體系統的複雜性是一個很痛苦的問題,而且無法避免。Fred Brooks將複雜性描述為,軟體系統解決業務問題所固有的本質複雜性,以及實施該解決方案所帶來的偶發複雜性。 隨著與採用“API優先”工程實踐和微服務架構的組織進行更密切的合作,我發現這種描述越來

洛谷線段 狀陣列區間修改區間查詢

在做一道整體二分的題目的時候遇到了這種區間修改區間查詢的樹狀陣列,感覺用起來手感不錯就拿來了。證明的話,那其實不重要,會用就好了 #include<cstdio> #include<

必須知道的八大種排序演算法java實現(二) 選擇排序,插入排序,希爾演算法

一、選擇排序   1、基本思想:在要排序的一組數中,選出最小的一個數與第一個位置的數交換;然後在剩下的數當中再找最小的與第二個位置的數交換,如此迴圈到倒數第二個數和最後一個數比較為止。   2、例項   3、演算法實現    /** * 選擇排序演算法 * 在未

HTTP協議——經典面試題

http請求由三部分組成,分別是:請求行、訊息報頭、請求正文 HTTP(超文字傳輸協議)是一個基於請求與響應模式的、無狀態的、應用層的協議,常基於TCP的連線方式,HTTP1.1版本中給出一種持續連線的機制,絕大多數的Web開發,都是構建在HTTP協議之上的Web應用。 1、常用的HTTP方法有哪些?GET:

WebSocket相關知識整理

前言   記得大概半年前就產生了疑惑,即後臺如何主動向前端推送資料。問了下專業老師,知道了原來有一個叫WebSocket的技術可以用於推送資料。於是,當時我就找了個教程,用的是Spring WebSocket。照著敲了一遍,也就搭起來了,依葫蘆畫瓢而已。當時有其他東西要學,也沒有相關的需求,就沒再接觸過。前