教程 關於一種比較特別的線段樹寫法

2022-02-27 05:45:58 字數 3711 閱讀 3514

這篇noip水平的blog主要是為了防止我afo後寫法失傳而寫的(大霧)

博主平常寫線段樹的時候經常用一種結構體飛指標的寫法, 這種寫法具有若干優勢:

出錯會報re方便用gdb一類工具快速定位錯誤(平衡樹也可以用類似寫法, 一秒定位板子錯誤)

而且將線段樹函式中相對比較醜陋的部分引數隱式傳入, 所以(可能)看上去比較漂亮一些

在使用記憶體池而不是動態記憶體的情況下一般比普通陣列寫法效率要高

原生一體化, 在資料結構之間巢狀時可以直接套用而不必進行各種相容性修改

介面作為成員函式出現, 不會出現識別符號衝突(重名)的情況

下面就以線段樹最基礎的實現例子: 在 \(o(n+q\log n)\) 的時間複雜度內對長度為 \(n\) 的序列進行 \(q\) 次區間加法區間求和為例來介紹一下這種寫法.

(可能我當前的寫法並沒有做到用指標+結構體所能做到的最優美的程度而且沒有做嚴格封裝, 求dalao輕噴)

注意這篇文章的重點是寫法而不是線段樹這個知識點qwq...

前置技能是要知道對某個物件呼叫成員函式的時候有個this指標指向呼叫這個函式的**物件.

定義乙個結構體node作為線段樹的結點. 這個結構體的成員變數與函式定義如下:

struct node;
其中:

個人一般選擇在建構函式中建樹. 寫法如下(此處初值為 \(0\)):

node(int l,int r):l(l),r(r),add(0),sum(0)

}

這個實現方法利用了new node()會新建乙個結點並返回乙個指標的性質遞迴建立了一棵線段樹.

new node(l,r)實際上就是建立乙個包含區間 \([l,r]\) 的線段樹. 其中 \(l\) 和 \(r\) 在保證 \(l\le r\) 的情況下可以任意.

注意到我在 \(l=r\) 的時候並沒有對lchrch賦值, 也就是說是野指標. 為什麼保留這個野指標不會出現問題呢? 我們到查詢的時候再做解釋.

實際使用的時候可以這樣做:

int main()
然後就可以建立一棵包含區間 \([1,n]\) 的線段樹了.

在這個例子中要進行的修改是 \(o(\log n)\) 時間複雜度內的區間加法, 那麼需要先實現惰性求值, 當操作深入到子樹中的時候下傳標記進行計算.

首先實現乙個小的輔助函式void add(int):

void add(int d)
作用是給當前結點所代表的區間加上 \(d\). 含義很明顯就不解釋了.

有了這個小輔助函式之後可以這樣無腦地寫void pushdown():

void pushdown()

}

這兩個函式中所有this->因為沒有識別符號重複的情況其實是可以去掉的, 博主的個人習慣是保留.

子樹修改後顯然祖先結點的資訊是需要更新的, 於是這樣寫:

void maintain()
主要的操作函式可以寫成這樣:

void add(int l,int r,int d)

}

其中判交部分寫得非常無腦, 而且全程沒有各種 \(\pm1\) 的煩惱.

注意第一行的this->l/this->rl/r是有區別的.this->l/this->r指的是線段樹所代表的"這個"區間, 而l/r則代表要修改的區間.

之前留下了乙個野指標的問題. 顯然每次呼叫的時候都保持查詢區間和當前結點代表的區間有交集, 那麼遞迴到葉子的時候依然有交集的話必然會覆蓋整個結點(因為葉子結點只有乙個點啊喂). 於是就可以保證**不出問題.

在主函式內可以這樣使用:

int main()
按照線段樹的分治套路, 我們只需要判斷求和區間是否完全包含當前區間, 如果完全包含則直接返回, 否則下傳惰性求值標記並分治下去, 對和求和區間相交的子樹遞迴求和. 下面直接實現剛剛描述的分治過程.

int query(int l,int r)

}

其實在查詢的時候, 有時候會維護一些特殊運算, 比如矩陣乘法/最大子段和一類的東西. 這個時候可能需要過一下腦子才能知道ans的初值是啥. 然而實際上我們直接用下面這種寫法就可以避免臨時變數與單位元初值的問題:

int query(int l,int r)

}

其中加法可以被改為任何滿足結合律的運算.

主函式內可以這樣使用:

int main()
下面以進行單點修改區間求和並要求可持久化為例來說明.

先實現乙個建構函式用來把原結點的資訊複製過來:

node(node* ptr)
然後每次修改的時候先複製一遍結點就完事了. 簡單無腦. (下面實現的是將下標為 \(x\) 的值改成 \(d\))

void modify(int x,int d)

else

this->maintain();

}}

其實對於單點的情況還可以用問號表示式(或者三目運算子? 隨便怎麼叫了)搞一搞:

void modify(int x,int d)

}

動態開點的時候我們就不能隨便留著野指標了. 因為我們需要通過判空指標來判斷當前子樹有沒有被建立.

那麼建構函式我們改成這樣:

node(int l,int r):l(l),r(r),add(0),sum(0),lch(null),rch(null){}
然後就需要注意處處判空了, 因為這次不能假定只要當前點不是葉子就可以安全訪問子節點了.

遇到空結點如果要求和的話就忽略, 如果需要進入子樹進行操作的話就新建.

而且在判斷是否和子節點有交集的時候也不能直接引用子節點中的端點資訊了, 有可能需要計算int mid=(this->l+this->r)>>1. 一般查詢的時候沒有計算的必要, 因為發現結點為空之後不需要和它判交.

有時候動態分配記憶體可能會造成少許效能問題, 如果被輕微卡常可以嘗試使用記憶體池.

記憶體池的意思就是一開始分配一大坨最後再用.

方法就是先開一塊記憶體和乙個尾指標,pool_size為使用的最大結點數量:

node pool[pool_size]

node* ptop=pool;

然後將所有new替換為new(ptop++)就可以了.new(ptr)的意思是對假裝ptr指向的記憶體是新分配的, 然後呼叫建構函式並返回這個指標.

顯然這個寫法也是有一定缺陷的, 目前發現的有如下幾點:

最後希望有興趣的讀者可以嘗試實現一下這種寫法, 萬一發現這玩意確實挺好用呢?

(厚臉皮求推薦)

一種特別的樹形結構 並查集

並查集主要解決連線問題 並查集操作 find i 查詢父親結點 isconnected p,q 查詢是否相連,返回bool unionelements p,q 合併兩個結點 普通版本 無路徑壓縮,無優化 class unionfind 析構函式 unionfind 查詢過程,查詢元素p所對應的集合編...

很特別的乙個動態規劃入門教程

通過金礦模型介紹動態規劃 附 首先我們思考乙個問題,如何用最少的硬幣揍夠i元 i 11 為什麼要這麼問呢?兩個原因 1 當我們遇到乙個大問題時,總是習慣把問題的規模變小,這樣便於分析討論。2.這個規模變小後的問題和原來的問題是同質的,除了規模變小,其它的都是一樣的,本質上它還是同乙個問題規模變小後的...

一種達到微妙級別的計時器

ifndef celltimestamp hpp define celltimestamp hpp 為了避免同乙個標頭檔案被包含 include 多次,c c 中有兩種巨集實現方式 一種是 ifndef方式 另一種是 pragma once方式。pragma once 達到微秒的計時器 includ...