總原理:
將[1,n]分解成若干特定的子區間(數量不超過4*n)
用線段樹對“編號連續”的一些點,進行修改或者統計操作,修改和統計的復雜度都是O(log2(n))
用線段樹統計的東西,必須符合區間加法,(也就是說,如果已知左右兩子樹的全部信息,比如要能夠推出父節點);否則,不可能通過分成的子區間來得到[L,R]的統計結果。
一個問題,只要能化成對一些“連續點”的修改和統計問題,基本就可以用線段樹來解決了
注意:區分3個概念:原數組下標,線段樹中的下標和存儲下標。
原數組下標,這里都默認下標從1開始(一般用a數組表示,a [1] , a[2] …… a [n] )
線段樹下標,是指該節點所管理的局部點集區,。一般都用區間 [ l , r ] 表示,即這一點存儲的是 a [l] ~ a [r] 區間內的局部解、局部信息。
存儲下標,是指該元素在總內存池 arr 中實際的存儲位置,一般用 arr [ root ]表示。存儲方式類似於“完全二叉樹”,詳細見下面的圖
注意,在元數個數N確定的時候,數字對 [ l , r ] 的值 和 root 的值,是對應的。
每一個arr[root],都有一個對應的、專屬於它自己管理的區間 [ l , r ]
因為對於確定的N,構建的線段樹是唯一確定的
具體落實到計算上,就是 l、r 和 root同步變化,例如:(l , mid , root*2) 、 (mid+1 , r , root*2+1)
線段樹的解題關鍵:
1:要推出父節點,我需要知道兩個子節點的那些信息?————這個決定了, 每個單位結點的數據結構(需要設置哪些成員變量),如何設置node
2:在1的基礎上,已知兩個子節點的完整信息,我該如何推出父節點?————定義push_up函數
3:如果是區間更新,還需要思考:如何設置lazytag?如何用設置的lazy_tag更新所需數據值?
Lazytag的設置,主要是要求:求sum的時候,根據從父區間傳下來的lazy_tag,能正確更新子區間所有內部數據值和他的lazy值,從而正確求出sum
線段樹的存儲結構:
假設有某個結點,它的信息存儲在 arr [x] 中,那么它的左子節點信息存儲在 arr [2x] 中,右結點信息存儲在 arr [2x+1]中
並且,arr [1] 為整個樹的根節點 ,所以,整體的統計信息、最終的答案,是存在節點1中的。
線段樹的代碼組成:
1:build函數:用於建立整個線段樹。
2:upgrade,修改(更新)函數。注意區分:單點更新、區間更新。
3:push_up: 父節點回溯更新函數
如果是區間更新,還需要:push_down,用於:把lazy_tag往下推一級,推到該節點的子節點,並更新該節點左右子節點的:數據值、lazy_tag值。
一、build函數:構建線段樹
原理:
開始時是區間[1,n] ,通過遞歸來逐步分解;
線段樹對於每個n的分解是唯一的,所以n相同的線段樹,結構相同。
先向下遞歸地尋找葉節點所在位置(尋找長度為1的區間),
一旦找到就把值放到這個位置,
並向上回溯地修改所有父節點的值。
遞歸遍歷的順序,十分類似於樹的后續遍歷。
例子:N == 10 ;原數組a:1,2,3,4,5,6,7,8,9,10;要求:區間和
過程如圖:

那么問題來了,究竟要遞歸到多深,才到可以存放原始數據的葉節點??
這取決於:[l,r]什么時候有l==r;其實質是,取決於總結點的個數N.
這也就是為什么我們說 “線段樹對於每個n的分解是唯一的,n相同的線段樹,結構相同。”
代碼:
void build(int l,int r,int root)//線段樹建樹 { if(l == r) { num[root]=1; return; } // [l,mid]:左子樹 [mid+1,r]:右子樹 int mid = (l+r)/2;
build(l,mid,root*2);
build(mid+1,r,root*2+1);
push_up(root); //對於這個根節點root,先把左右子樹都算出來了,再來更新它的值。 //沿路回溯。回溯到的點root,都是被 [la , rb] 或其子區間影響到的點,邊回溯邊更新 } //調用:build(1,N,1);
二、線段樹的點修改
首先由根節點1向下遞歸,找到對應的葉節點,
然后,修改葉節點的值,
再向上返回,在函數返回的過程中,更新路徑上的節點的統計信息。
void upgrade(int p,int val,int l,int r,int root) //單點更新的upgrade算法:把找點p,當成找區間[p,p] { if(l==r) { num[root]+=val; return ; } int mid = (l+r)/2; if(p>mid) //如果p>當前區間的中點,說明我想找的[p,p]區間,在右半邊 upgrade(p,val,mid+1,r,root*2+1); else upgrade(p,val,l,mid,root*2); //沿路回溯。回溯到的點root,都是被[p,p]區間影響到的點,邊回溯邊更新 num[root] = num[root*2] + num[root*2+1]; }
三、線段樹的區間修改 —— 引入 lazy_tag
和點修改一樣,也是將區間分成子區間,
首先由根節點1向下遞歸,找到對應的葉節點,
然后,修改本節點的值,
再向上返回,在函數返回的過程中,更新路徑上的節點的統計信息。
由此可見向上更新的部分,是一毛一樣的。
不同的是,點修改不用向下修改(因為對下面的沒有影響,並且也沒有下一級節點了,它自己就是最底層葉節點),
可是區間修改,理論上來說是需要向下修改的(因為父區間變動,子區間也會變動)
但是又不能,每次一區間修改,就連帶着修改下面的所有子結點,這也tm太慢了。
所以,我們引入了 lazy_tag , 這樣就可以不用把區間內的每個點都按照單點更新的方式更新一遍。
我們暫時不更新下面的子節點,而是打上一個標記,什么時候要用到子節點了,什么時候再看着這個標簽,更新子節點。
(一般都是sum的時候用push_down更新子節點,因為求和的時候需要先知道兩個子節點的值)
lazy_tag的含義:
本節點的統計信息已經根據標記更新過了,但是本節點的子節點仍需要進行更新。(注意:打上懶惰標記的節點,它自己是已經更新過了的)
即,如果要給一個區間的所有值都加上1,那么,實際上並沒有給這個區間的所有值都加上1,而是打個標記,記下來,這個節點所包含的區間需要加1.
打上標記后,要根據標記更新本節點的統計信息,比如,如果本節點維護的是區間和,而本節點包含5個數,那么,打上+1的標記之后,要給本節點維護的和+5。
這是向下延遲修改,但是向上顯示的信息是修改以后的信息,所以查詢的時候可以得到正確的結果。
有的標記之間會相互影響,所以比較簡單的做法是,每遞歸到一個區間,首先下推標記(若本節點有標記,就下推標記),然后再打上新的標記,這樣仍然每個區間操作的復雜度是O(log2(n))。
重申,請注意:區間修改才需要懶惰標記,單點修改不需要。
示例代碼:
void upgrade(int la,int rb,int l,int r ,int val,int root) { /*以后就永遠設定: la、rb為需更新區間的左、右端點(一直不變); l、r為當前區間的左、右端點,(隨遞歸更新) root為當前 [l , r ] 對應的根存儲位置(隨遞歸更新) */ //若本次所看區間,整個就包含在所要查詢的區間之內 if(la<=l && rb>=r) { num[root] = (r-l+1)*val; //把本區間num更新為正確值 lazy[root] = val; //增加lazy標記,表示:本區間的Sum正確,子區間的Sum仍需要根據lazy的值來調整 return ; } push_down(root,r-l+1); //在繼續遞歸之前,先把當前root 的標記往下推 //每次都是先下推,再更新,從而保證,計算root的時候,它的左右兩子樹都已經是正確值,左右兩子樹都不存在lazy更新延遲,都已經更新好了。 int mid = (l+r)/2; if(la<=mid) { upgrade(la,rb,l,mid,val,root*2); } if(rb>mid) { upgrade(la,rb,mid+1,r,val,root*2+1); } push_up(root); }
四、push_up函數
我的目標是,已知兩個子節點的某些信息,利用push_up函數推出父節點
思考:如果要用兩個子節點推出父節點,我需要知道子節點的那些信息?————這些信息都是線段樹需要維護的。
然后,已知了所有所需信息之后,具體該怎么推?————決定了:如何具體的寫出 push_up函數
注意:push_up函數內,兩個子節點的信息,是已經更新過的、正確的信息。因為在調用push_up之前,已經調用過push_down函數,也就是說,兩個子節點的信息,是已經被更新過了的,是正確的。
五、push_down函數
一、push_down函數總共需要干三件事:
1:更新左右子節點的lazy值,也就是:把父節點root上面的lazy標記,下推到兩個子節點上
2:依據lazy_tag的定義,更新左右子節點的num值,使左右子節點的值成為“被更新過的、正確的值”。
3:把父節點root自身的lazy清空。因為lazy_tag已經被下推,就向上查詢來看,父節點自身的lazy_tag已經不需要了
二、注意:
1:開始一切動作之前,要先特判:此父節點上究竟有沒有lazy_tag。如果本就沒有lazy_tag,千萬別更新,會wa。
2:lazy_tag的意義可以被任意定義;關於‘一’中的“更新”,具體的更新方法也是多種多樣的,但關鍵是:
lazy_tag的定義,要能跟 “更新num的操作”對應得上。
也就是說,必須要能夠根據lazy_tag,正確更新結點的num值才可以。
示例代碼:
(此處lazy_tag存儲的是:當前段中,每個結點被更新成的數值)
(num的意義是:此區間上的數值累加和)
(所以根據這里lazy_tag的定義,每個num 的更新方法就是:lazy的數值*區間長度)
void push_down(int root,int len) //傳入:父節點root 和 區間長度len { if(lazy[root] == 0) //如果此節點根本沒有lazy_tag,直接返回,不作處理 return; lazy[root*2] = lazy[root]; //把lazy下推到兩個子節點上 lazy[root*2+1] = lazy[root]; num[root*2] = lazy[root*2]*(len-(len)/2); //更新兩個子節點的num num[root*2+1] = lazy[root*2+1]*((len)/2);、 lazy[root]=0; //清空父節點自身的lazy_tag }
整體的示例代碼:
//線段樹——————區間修改,區間查詢 //題意:一個線性數組,不斷把某區間內的值修改“為”另一個輸入值,查詢區間內和。 #include <iostream> #include <cstdio> #include <cstring> #define maxn 100005 int num[maxn*4];//開四倍空間 int lazy[maxn*4]; using namespace std; void push_up(int root)//根節點狀態更新 { num[root] = num[root*2] + num[root*2+1]; } void build(int l,int r,int root)//線段樹建樹 { if(l == r) { num[root]=1; return; } int mid = (l+r)/2; build(l,mid,root*2); build(mid+1,r,root*2+1); push_up(root); } void push_down(int root,int len)//傳入:root結點下標、對應的當前[l,r]區間長度 { if(lazy[root] == 0)//假若這個節點根本沒有lazy_tag return; lazy[root*2] = lazy[root]; lazy[root*2+1] = lazy[root]; num[root*2] = lazy[root*2]*(len-(len)/2); num[root*2+1] = lazy[root*2+1]*((len)/2); lazy[root]=0; } void upgrade(int la,int rb,int l,int r ,int val,int root)//線段更新 //la、rb為需更新的區間左、右端點,l、r為當前區間左、右端點,root為當前l、r對應的根存儲位置 { if(la<=l && rb>=r) { num[root] = (r-l+1)*val; lazy[root] = val; return ; } push_down(root,r-l+1); int mid = (l+r)/2; if(la<=mid) { upgrade(la,rb,l,mid,val,root*2); } if(rb>mid) { upgrade(la,rb,mid+1,r,val,root*2+1); } push_up(root); } int query(int la,int rb,int l,int r,int root)//查詢區間[la,rb]的值 { if(l>=la && r<=rb) { return num[root]; } push_down(root,r-l+1); int mid = (l+r)/2; int ans=0; if(la<=mid) { ans += query(la,rb,l,mid,root*2); } if(rb>mid) { ans +=query(la,rb,mid+1,r,root*2+1); } return ans; } int main() { int t; scanf("%d",&t); for(int o=1;o<=t;o++) { int N;//原數組結點總數 scanf("%d",&N); memset(num,0,sizeof(num)); memset(lazy,0,sizeof(lazy)); build(1,N,1);//建樹 int x; scanf("%d",&x); while(x--) { int la,rb,val; scanf("%d%d%d",&la,&rb,&val); upgrade(la,rb,1,N,val,1);//更新[l,r]區間結點,每個結點都被更新“成”val query(la,rb,1,N,1);//查詢區間[la,rb]的值 } } return 0; }
