\(ps\): 此處以詢問區間和為例
線段樹之所以稱為「樹」,是因為其具有樹的結構特性。線段樹由於本身是專門用來處理區間問題的(包括\(rmq\)、\(rsq\)問題等),所以其結構可以近似的看做一棵二叉查詢樹:
\(emmmmm\)圖是從網上偷的
對於每乙個子節點而言,都表示整個序列中的一段子區間;對於每個葉子節點而言,都表示序列中的單個元素資訊;子節點不斷向自己的父親節點傳遞資訊,而父節點儲存的資訊則是他的每乙個子節點資訊的整合。
有沒有覺得很熟悉?對,線段樹就是分塊思想的樹化,或者說是對於資訊處理的二進位製化——用於達到\(o(logn)\)級別的處理速度,\(log\)以\(2\)為底。(其實以幾為底都只不過是個常數,可忽略)。而分塊的思想,則是可以用一句話總結為:通過將整個序列分為有窮個小塊,對於要查詢的一段區間,總是可以整合成\(k\)個所分塊與\(m\)個單個元素的資訊的並\((0<=k,m<=\sqrt)\)。但普通的分塊不能高效率地解決很多問題,所以作為\(log\)級別的資料結構,線段樹應運而生。
由於二叉樹的自身特性,對於每個父親節點的編號\(i\),他的兩個兒子的編號分別是\(2i\)和\(2i+1\),所以我們考慮寫兩個\(o(1)\)的取兒子函式:
int n;
int ans[maxn*4];
inline int ls(int p)//左兒子
inline int rs(int p)//右兒子
\(ps:\)此處的\(inline\)可以有效防止無需入棧的資訊入棧,節省時間和空間
那麼根據線段樹的服務物件,可以得到線段樹的維護:
void push_up_sum(int p)// 向上不斷維護區間操作
void push_up_min(int p)//max
此處一定要注意,\(push up\)操作的目的是為了維護父子節點之間的邏輯關係。當我們遞迴建樹時,對於每乙個節點我們都需要遍歷一遍,並且電腦中的遞迴實際意義是先向底層遞迴,然後從底層向上回溯,所以開始遞迴之後必然是先去整合子節點的資訊,再向它們的祖先回溯整合之後的資訊。(這其實是正確性的證明啦)
那麼對於建樹,由於二叉樹自身的父子節點之間的可傳遞關係,所以可以考慮遞迴建樹(\(emmmm\)之前好像不小心劇透了\(qwq\)),並且在建樹的同時,我們應該維護父子節點的關係:
void build(ll p,ll l,ll r)
//如果左右區間相同,那麼必然是葉子節點啦,只有葉子節點是被真實賦值的
ll mid=(l+r)>>1;
build(ls(p),l,mid);
build(rs(p),mid+1,r);
//此處由於我們採用的是二叉樹,所以對於整個結構來說,可以用二分來降低複雜度,否則樹形結構則沒有什麼明顯的優化
push_up(p);
//此處由於我們是要通過子節點來維護父親節點,所以pushup的位置應當是在回溯時。
}
為什麼不討論單點修改呢\(qwq\)?因為其實很顯然,單點修改就是區間修改的乙個子問題而已,即區間長度為\(1\)時進行的區間修改操作罷了\(qwq\)
那麼對於區間操作,我們考慮引入乙個名叫「\(lazy\)
\(tag\)」(懶標記)的東西——之所以稱其「\(lazy\)」,是因為原本區間修改需要通過先改變葉子節點的值,然後不斷地向上遞迴修改祖先節點直至到達根節點,時間複雜度最高可以到達\(o(nlogn)\)的級別。但當我們引入了懶標記之後,區間更新的期望複雜度就降到了\(o(logn)\)的級別且甚至會更低.
不扯淡了,聊正事:
分塊的思想是通過將整個序列分為有窮個小塊,對於要查詢的一段區間,總是可以整合成\(k\)個所分塊與\(m\)個單個元素的資訊的並\((0<=k,m<=logn)\)(小小修改了一下的上面的前言\(qwq\))
那麼我們可以反過來思考這個問題:對於乙個要修改的、長度為\(l\)的區間來說,總是可以看做由乙個長度為\(2\)^\(log\)(\(\lfloor\rfloor{}\))和剩下的元素(或者小區間組成)。那麼我們就可以先將其拆分成線段樹上節點所示的區間,之後分開處理:
如果單個元素被包含就只改變自己,如果整個區間被包含就修改整個區間
首先,懶標記的作用是記錄每次、每個節點要更新的值,也就是\(delta\),但線段樹的優點不在於全記錄(全記錄依然很慢qwq),而在於傳遞式記錄:
** 整個區間都被操作,記錄在公共祖先節點上;只修改了一部分,那麼就記錄在這部分的公共祖先上;如果四環以內只修改了自己的話,那就只改變自己。**
\(\rm\)
\(\rmt\),如果我們採用上述的優化方式的話,我們就需要在每次區間的查詢修改時\(pushdown\)一次,以免重複或者衝突或者**\(qwq\)
那麼對於\(pushdown\)而言,其實就是純粹的\(pushup\)的逆向思維(但不是逆向操作):
因為修改資訊存在父節點上,所以要由父節點向下傳導\(lazy\)
\(tag\)
那麼問題來了:怎麼傳導\(pushdown\)呢?這裡很有意思,開始回溯時執行\(pushup\),因為是向上傳導資訊;那我們如果要讓它向下更新,就調整順序,在向下遞迴的時候\(pushdown\)不就好惹~\(qwq\):
inline void f(ll p,ll l,ll r,ll k)
//我們可以認識到,f函式的唯一目的,就是記錄當前節點所代表的區間
inline void push_down(ll p,ll l,ll r)
inline void update(ll nl,ll nr,ll l,ll r,ll p,ll k)
push_down(p,l,r);
//回溯之前(也可以說是下一次遞迴之前,因為沒有遞迴就沒有回溯)
//由於是在回溯之前不斷向下傳遞,所以自然每個節點都可以更新到
ll mid=(l+r)>>1;
if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
push_up(p);
//回溯之後
}
沒什麼好說的,由於是資訊的整合,所以還是要用到分塊思想,我實在是不想再碼一遍了\(qwq\)
ll query(ll q_x,ll q_y,ll l,ll r,ll p)
最後貼高畫質無碼的標程:
(還有,輸入大資料一定不要用不加優化的cin/cout啊)
#include#include#define maxn 1000001
#define ll long long
using namespace std;
unsigned ll n,m,a[maxn],ans[maxn<<2],tag[maxn<<2];
inline ll ls(ll x)
inline ll rs(ll x)
void scan()
inline void push_up(ll p)
void build(ll p,ll l,ll r)
ll mid=(l+r)>>1;
build(ls(p),l,mid);
build(rs(p),mid+1,r);
push_up(p);
} inline void f(ll p,ll l,ll r,ll k)
inline void push_down(ll p,ll l,ll r)
inline void update(ll nl,ll nr,ll l,ll r,ll p,ll k)
push_down(p,l,r);
ll mid=(l+r)>>1;
if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
push_up(p);
}ll query(ll q_x,ll q_y,ll l,ll r,ll p)
int main()
case 2:}}
return 0;
}
TypeScript版線段樹
脫離acm隊伍已經一年多了,現在還能手寫的演算法不多,線段樹是其中一種。謹以此文紀念逝去的acm生涯 線段樹是一種二叉搜尋樹,常用於區間求和 區間求極值,其查詢和更新時間複雜度是o logn 線段樹的主要操作包括初始化 更新和查詢 1.初始化 初始化過程是乙個遞迴演算法,從根節點遞迴全樹。作用是設定...
指標版的線段樹
includeusing namespace std const int n 1000005 struct node tree n int x,t,outit n long long ans vectoraqueue n struct point data n void buildtree node...
非遞迴版線段樹模板
摘自這裡 0 定義 define maxn 100007 int a maxn n,n 原陣列,n為原陣列元素個數 n為擴充元素個數 int sum maxn 2 區間和 int add maxn 2 懶標記 1 建樹 void build int n 2 點修改 a l c void update...