線段樹入門(一)轉自 WDVXDR
這篇文章依然是左神(就是那個複賽寫錯YESNO,狂丟60分的帥小夥)所寫,在此轉載,供學弟學妹們膜拜,原文出處:https://www.cnblogs.com/wdvxdr/p/7238253.html。 正文如下: 接觸線段樹前我們先看一道比較經典的題目。 HDU 1166 敵兵佈陣 這道題要求我們對一個區間內的單個值進行修改,並查詢一段區間的和。 對於修改,我們很容易暴力的修改,複雜度為O(1),但查詢的複雜度就變成了O(n),總複雜度為O(n*m),由於n和m都很大,所以這樣肯定會超時的。 對於區間和我們常見的操作有字首和,用字首和可以將查詢操作的複雜度降為O(1),但修改操作的複雜度則會升到O(n)。總複雜度也沒有改變。 當這兩種方法都無法滿足題目的需求時,我們可以考慮是否可以將修改和查詢的複雜度均攤一下,使總複雜度降低。 線段樹解決這個問題比較好的工具。 什麼是線段樹
struct segtree { int l,r;//左右端點 long long sum;//要維護的資訊 }tree[maxn<<2];//一般開4*n防止爆空間
建立線段樹
由於線段樹是一個二叉樹,而且是一個平衡二叉樹,如果當前結點的編號是i,左端點為L ,右端點為 R , 那麼左兒子的 編號為 i*2 ,左端點為 L ,右端點為 (L + R)/2 ; 同理右兒子的 編號為 i*2+1,左端點為(L+R)/2 ,右端點為 R)。如果當前結點的左端點等於右端點,那麼該節點就是葉子節點,直接在該節點賦值即可。顯然線段樹是遞迴定義的。 複製程式碼
void build(int root,int l,int r)
{
tree[root].l = l;tree[root].r = r;//初始化左右端點
if(l==r)//為葉子節點,直接賦值
tree[root].sum = a[l];
else
{
int mid = (l+r)>>1;
build(root<<1,l,mid);//建立左子樹
build(root<<1|1,mid+1,r);//右子樹
push_up(root);//從下往上維護線段樹
}
}
從下往上更新線段樹
void push_up(int x)
{
tree[x].sum = tree[x<<1].sum + tree[x<<1|1].sum;//因題目不同而不同
}
單點更新 單點更新的方法很好理解,如果目標更新節點在左兒子裡,去左兒子中查詢;反之,在右兒子中。不斷遞迴,知道找到需要維護的節點,更新它,然後從下往上維護線段樹回來。這就是維護的過程,程式碼如下:
void update(int x,int q,long long val)
{
int L=tree[x].l,R=tree[x].r;
if(L==q&&R==q)//葉子節點,直接更新
tree[x].num = val;
int mid = (L+R)>>1;
if(mid>=q) update(x<<1,l,r,val);//在左子樹中
if(q>mid) update(x<<1|1,l,r,val);//在右子樹中
push_up(x);//從下往上維護線段樹
}
區間查詢 題目中讓我們查詢區間求和,不難想到如果當前結點的區間完全被目標區間包含,直接返回當前結點的sum值,否則判斷是否在左子樹,右子樹中,然後分別對左子樹和右子樹進行查詢。具體過程通過以下程式碼理解:
long long query(int x,int l,int r)
{
int L=tree[x].l,R=tree[x].r;
if(l<=L&&R<=r)//完全在查詢區間內
return tree[x].sum;//直接返回值
else
{
long long ans = 0;
int mid = (L+R)>>1;
if(mid>=l) ans += query(x<<1,l,r);//在左子樹中有一部分
if(r>mid) ans += query(x<<1|1,l,r);//在右子樹中有一部分
return ans;//返回答案
}
}
這樣我們就解決了這道題了。