【學習筆記】線段樹詳解(全)
和三個同學一起搞了接近兩個月的線段樹,頭都要炸了T_T,趁心態尚未涼之前趕快把東西記下來。。。
【目錄】
- 【基礎】作者:\((Silent\)_\(EAG)\)
- 【懶標記】作者:\((Silent\)_\(EAG)\)
- 【掃描線】作者:\((IC\)_\(QQQ)\)
- 【權值線段樹】作者:\((Xing\)_\(Ling)\)
- 【動態開點】作者:\((Xing\)_\(Ling)\)
- 【線段樹合並】作者:\((Xing\)_\(Ling)\)
- 【可持續化線段樹—靜態主席樹】作者:\((Xing\)_\(Ling)\)
- 【可持續化線段樹—動態主席樹(
豈可修)】作者:\((Xing\)_\(Ling)\)
一:【基礎姿勢】
\(piao\) 一下隔壁大佬的文章QAQ: \(Silent\)_\(EAG\)
基礎題走起:
-
【模板】線段樹 \(1\) \([P3372]\)
【標簽】線段樹/樹狀數組
【題解】\(Silent\)_\(EAG\) -
\(Can\) \(you\) \(answer\) \(these\) \(queries\) \(I\) \([SP1043]\)
【標簽】線段樹 -
\(Interval\) \(GCD\) \([CH4302]\)
【標簽】線段樹/樹狀數組/\(GCD\)
【題解】\(Silent\)_\(EAG\)
高檔題走起:
-
\(Can\) \(you\) \(answer\) \(these\) \(queries\) \(III\) \([SP1716]\)
【標簽】線段樹/懶標記
【題解】\(Silent\)_\(EAG\) -
炸勃龍 \(I\) \([P4118]\) \([BZOJ5394]\) \(/\) 奈芙蓮·盧可·印薩尼亞 \(Nephren\) \(Ruq\) \(Insania\) \([P3934]\)
【標簽】線段樹/樹狀數組/暴力枚舉/數論/歐拉定理
(\(YNOI2016\) 毒瘤數論)
二:【懶標記】
基礎題走起:
- 【模板】線段樹 \(2\) \([P3373]\) \(/\) 維護序列 \([P2023]\) \([BZOJ1798]\)
【標簽】線段樹/懶標記
【題解】\(Silent\)_\(EAG\)
高檔題走起:
-
\(Can\) \(you\) \(answer\) \(these\) \(queries\) \(IIII\) \([SP2713]\)
【標簽】線段樹/懶標記
【題解】\(Silent\)_\(EAG\) -
市場 \([LOJ6029]\)
【標簽】線段樹/懶標記
【題解】\(Silent\)_\(EAG\) -
酒店 \(Hotel\) \([P2894]\) \([BZOJ1593]\)
【標簽】線段樹/懶標記 -
序列操作 \([P2572]\) \([BZOJ1858]\)
【標簽】線段樹/懶標記/珂朵莉樹
(如果嘗試用線段樹做的話,懶標記的運用達到了毀天滅地的地步。而珂學就爽了,那么它就是道大水題)
三:【掃~描~線~】
見隔壁:\(IC\)_\(QQQ\)
四:【權值線段樹】
QAQ:以前似乎在某個地方看到過一個叫做做權值樹狀數組的東西唉QWQ
DL:其實他們都是一樣的思想,只是不同的實現方式而已
QAQ:可是XX在寫過無數區間作業的題后,發現無論是哪一類,樹狀數組都比線段樹快不止三倍唉QWQ
DL:*****
權值線段樹是什么?
權值線段樹,顧名思義是一顆線段樹。
但它和普通線段樹略有不同: 普通線段樹維護的是一段區間的數值的總和或最大值等等...... 而權值線段樹維護的是一定范圍內某個數值出現的次數。實際上它和之前的權值樹狀數組是一樣的原理。(求逆序對也可用權值線段樹實現 )
它有什么用?
按照定義,我們可以用它對於權值進行計數,感覺有點像數位\(dp\)吧,求一定范圍內符合要求的數的個數。權值線段樹的\(“\)范圍\(”\)是不定的,而\(“\)要求\(”\)一般是:在給定的數值范圍內。
舉個栗子:
有一個數列: \(a(1,1,2,3,3,4,4,4,4,5)\) 對其維護一個計數的權值線段樹,樹的大小就是數列的值域 \([mins~maxs]\),即 \([1~5]\)。
如圖,在數列中:
數值 \(1\) 出現了 \(2\) 次
數值 \(2\) 出現了 \(1\) 次
數值 \(3\) 出現了 \(2\) 次
數值 \(4\) 出現了 \(4\) 次
數值 \(5\) 出現了 \(1\) 次
重要應用:
在此基礎上,它還有一個很重要的作用 :
查詢某區間內第 \(k\) 小或第 \(k\) 大的值。
引理:
如果在值域范圍內 \((\) 即 \([mins~maxs]\) \()\)中發現有某個位置 \(x\),數列中存在這個數 \(x\),且使得數值范圍在區間 \([mins~x]\) 內的數一共有 \(k\) 個,那么 \(x\) 就是第 \(k\) 小的數。
反之亦然:
如果在值域范圍內 \((\) 即 \([mins~maxs]\) \()\)中發現有某個位置 \(x\),數列中存在這個數 \(x\),且使得數值范圍在區間 \([x~maxs]\) 內的數一共有 \(k\) 個,那么 \(x\) 就是第 \(k\) 大的數。
【分析】
以查詢第 \(k\) 小為例
我們可以用一種二分的思想,當需要在某個值域范圍 \([l~r]\) 內查找第 \(k\) 小時,先計算出數值在 \([l~mid]\) 以內的數的個數 \(tmp\),再將其與 \(k\) 進行比較:
- 如果 \(tmp \geqslant k\),則說明第 \(k\) 小的數應存在於 \([l~mid]\) 這個范圍。
- 如果 \(tmp \leqslant k\),則說明第 \(k\) 小的數應存在於 \([mid+1~r]\) 這個范圍,而實際上就等價於在 \([mid+1~r]\) 中找到第 \(k-tmp\) 小的數。
【Code】
#define Re register int
#define pl tree[p].PL
#define pr tree[p].PR
inline int ask(Re p,Re L,Re R,Re k){//查詢第k小
if(L==R)return L;//邊界葉節點
Re tmp=tree[pl].g;//計算左子樹(數值范圍在L~mid的數)共有多少個數字
if(tmp>=k)return ask(pl,L,mid,k);
//左子樹已經超過k個,說明第k小在左子樹里面
else return ask(pr,mid+1,R,k-tmp);
//左子樹不足k個數字,應該在右子樹中找到第(k-tmp)小
}
五:【動態開點】
什么是動態開點?
動態開點用法較固定,目的也很明確:節省空間。
它的實質其實就是在空間不夠的情況下,把不需要的節點變成虛點。
有什么用?
求解逆序對時可以用權值樹狀數組,那么如果嘗試用權值線段樹做的話會出現什么后果呢?
肯定是可解的。但是,由於值域大多都是 \(inf\) 級別的數字,況且某些比較毒瘤的在線操作還沒法離散化,於是在使用權值線段樹時,一般都會伴隨着動態開點的使用。
如何使用?
這里引用一下一位大佬的比喻: 開局一個根,枝葉全靠給。
當要用到(一般只有修改)某個節點的信息時,就手動開一個新的節點,給它一個點的空間包括各種節點信息。而在查詢中如果發現進入的節點不存在(還沒開發過),那么直接返回,不需要在查詢時新建節點。
【空間復雜度】
\(Q*log(inf)\)。其中 \(Q\) 為修改次數。
【Code】
(基本框架)
int cnt;
inline void sakura(Re &p,Re L,Re R,Re ???){//【???修改】
if(!p)p=++cnt,tree[p].?=???;
//發現進入了一個空節點,新建一個節點,賦予它編號,記錄基本信息
if(L==R){tree[p].?=???;return;}
//達到葉子節點,記錄一些特殊的信息,並返回
Re tmp=???;//可能會在在遞歸之前進行一些計算來方便判斷
if(???)sakura(pl,L,mid,???);//遞歸進入左子樹
if(???)sakura(pr,mid+1,R,???);//遞歸進入右子樹
tree[p].?=???;//回溯后更新信息
}
六:【線段樹合並】
什么是線段樹合並?
簡單來說就是將兩棵線段樹合並起來,並累加它們的信息。
有什么用?
線段樹合並一般用於對樹上信息的統計,例如:對一棵樹的所有葉子節點都開一個線段樹,統計信息時,將所有的兒子節點的線段樹合並起來,得到父親節點的線段樹,再用其去合並統計祖先的信息。
【時間復雜度】
如果一棵線段樹的所有節點都不為空(動態開點會使得虛點的存在),離散化后值域為 \(n\),遞歸一棵線段樹樹的時間復雜度達到最大: \(O(logn)\)。如果總共 \(n\) 棵樹的所有節點都不為空,那么需要合並 \(n-1\) 次, 總時間復雜度達到最大: \(O(n*logn)\)。
【Code】
inline int merge(Re p,Re q){//【線段樹合並】
if(!p)return q;if(!q)return p;
//當需要合並的點的其中一個編號為0時 (即為空),返回另一個編號
tr[p].g+=tr[q].g,p;//把q合並到p上面去
pl=merge(pl,tr[q].lp);//合並左子樹,並記錄p點的左子樹編號
pr=merge(pr,tr[q].rp);//合並右子樹,並記錄p點的右子樹編號
return p;
}
基礎題走起:
-
晉升者計數 \(Promotion\) \(Counting\) \([P3605]\)
【標簽】DFS/離散化/樹狀數組/線段樹/線段樹合並/動態開點
【題解】\(Xing\)_\(Ling\) -
永無鄉 \([P3224]\) \([BZOJ2733]\)
【標簽】Splay/平衡樹/線段樹合並/DFS/權值線段樹/並查集/離散化
【題解】\(Xing\)_\(Ling\)
高檔題走起:
- \(ROT-Tree\) \(Rotations\) \([P3521]\) \([BZOJ2212]\)
【標簽】線段樹/權值線段樹/線段樹合並
【題解】\(IC\)_\(QQQ\)
七:【可持續化線段樹—靜態主席樹】
來看一道經典的例題 【模板】可持久化線段樹 \(1\) (主席樹) \([3834]\) \(/\) \(K\)-\(th\) \(Number\) \([P3834]\) \([POJ2104]\) \([SP3946]\)
【題目大意】
給定一個長為 \(n\) 的數列以及 \(Q\) 個查詢,每次查詢輸入兩個整數 \(l\),\(r\),輸出數列中 \(l\) ~ \(r\) 第 \(K\) 小的數。
【 分析】
此題有三個關鍵點:
-
求一棵數列中的第 \(K\) 大或第 \(K\) 小的數。
解決方案:權值線段樹 -
如果僅僅是這樣,則非常好辦,給出的詢問是一段區間。一段長為 \(n\) 的數列中共有 \(n(n-1)/2\) 個不同的區間,如果給每個區間都開一個權值線段樹,后果是:\(TLE\) \(+\) \(MLE\) \(+\) 初始化建樹無從入手。
解決方案:前綴和
(對於原數列的每個位置都建立一個權值線段樹,\(p[i]\) 表示第 \(i\) 個位置上的樹的根節點編號,用 \(tree[pt[i]]\) 表示從第 \(1\) 個到第 \(i\) 個數這個區間中共 \(i\) 個數所維護成的一棵權值線段樹。\(message[pt[i]]=\sum_{j=1}^i message[pt[j]]\)) -
可內存還是遠遠不夠,即使是使用了【動態開點】 \(+\) 離散化,值域由 \(inf\) 降為 \(N\),每一棵權值線段樹的節點數降為 \(N*2\),節點,但一共有 \(N\) 棵樹,\(N*N*2\) 動輒就是幾十萬兆內存。(做一個簡單的計算:
\(200000*200000*2*4*3/1024/1024 \thickapprox 305175.78125 Mb\))
解決方案:可持續化
由於第 \(i\) 棵樹 \(tree[pt[i]]\) 與第 \(i-1\) 棵樹 \(tree[pt[i-1]]\) 只有 \(logn\) 個節點不一樣,於是只需要對第 \(i-1\) 棵樹進行一次單點修改將 \(a[i]\) 加入,就變成了第 \(i\) 棵樹。因此我們可以由已經建好第 \(i-1\) 棵樹迅速建立起第 \(i\) 棵樹,這也就是主席樹思想的精髓所在。
說具體點,就是讓第 \(i\) 棵樹與第 \(i-1\) 棵樹公用一些節點(因為在這些沒有發生改變的部分,它們的信息是完全相同的),在遞歸過程建樹中,如果發現要進行【單點修改】操作的是左子樹,那就讓新樹的右子樹編號指向舊樹的右子樹編號(即 \(tree[pt[i]].pr=tree[pt[i-1]].pr\) )然后遞歸進入左子樹的建立,反之亦然。
但第 \(1\) 棵樹需要由第 \(0\) 棵樹變化而來,當有特殊需要時,要提起把第 \(0\) 棵樹建立完整,而道題不需要,因為第 \(0\) 棵樹本來就為空,所以不用管。
【時間復雜度】
每次建樹的過程都接近於【單點修改】, 為 \(O(logn)\) ,所以初始化建樹的過程為 \(O(nlogn)\) 。
單次詢問采用權值線段樹中的【查詢第 \(k\) 小】,為 \(O(logn)\)。
總共 \(Q\) 次詢問,為 \(O(Qlogn)\) 。
總時間復雜度: \(O((n+Q)logn)\)
【空間復雜度】
如果要建立完整的第 \(0\) 棵樹,會占用 \(n*2\) 個節點,每棵新樹的建立都要新建 \(logn\) 個節點,一共有 \(nlogn\)。
總空間復雜度:\((logn+2)*n\)
【Code】
#include<algorithm>
#include<cstdio>
#define mid (L+R>>1)
#define pl tr[p].lp
#define pr tr[p].rp
#define Re register int
#define F(a,b) for(i=a;i<=b;++i)
using namespace std;
const int N=1e5+3;
int x,y,z,i,n,m,k,t,fu,cnt,a[N],b[N],pt[N];//pt[i]表示離散化后i這個位置所對應的權值樹根的編號
struct QAQ{int g,lp,rp;}tr[N<<5];//權值樹,保守開一個32*N
inline void in(Re &x){//【快讀】自己動手,豐衣足食...
x=fu=0;char c=getchar();
while(c<'0'||c>'9')fu|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();x=fu?-x:x;
}
inline int creat(Re pp,Re x,Re L,Re R){
//把上一棵權值樹pp(即pt[a[i-1]])復制過來
//並在遞歸復制途中對x(即a[i]離散化后的位置)這個點同步進行單修操作
Re p=++cnt;pl=tr[pp].lp,pr=tr[pp].rp,tr[p].g=tr[pp].g+1;
//新開一個點,並把上一個的數據復制進來,並使tr[].g++
if(L==R)return p;//到達邊界: L==R(即x這個位置)
if(x<=mid)pl=creat(tr[pp].lp,x,L,mid);//遞歸進入條件:單修
else pr=creat(tr[pp].rp,x,mid+1,R);//注意tr[pp]要同時遞歸至左(右)子樹
return p;
}
inline int ask(Re p,Re pp,Re L,Re R,Re k){
//查詢。p為查詢區間左端點的權值樹根編號,pp為查詢區間右端點的權值樹根編號
if(L==R)return b[R];//邊界:L==R
Re tmp=tr[tr[pp].lp].g-tr[pl].g;//用前綴和思想計算出左子樹共有多少個數字
if(tmp>=k)return ask(pl,tr[pp].lp,L,mid,k);//左子樹已經超過k個,說明第k小在左子樹里面
else return ask(pr,tr[pp].rp,mid+1,R,k-tmp);//左子樹不足k個,應該在右子樹中找第(k-tmp)小
}
int main(){
in(n),in(k);
F(1,n)in(a[i]),b[i]=a[i];//復制進b[]並離散去重
sort(b+1,b+n+1);//【離散化】
m=unique(b+1,b+n+1)-b-1;//【去重】
F(1,n)pt[i]=creat(pt[i-1],lower_bound(b+1,b+m+1,a[i])-b,1,m);
//找出當前這個位置按權值排序后的位置x,進入建樹
while(k--)in(x),in(y),in(z),printf("%d\n",ask(pt[x-1],pt[y],1,m,z));//注意是【y】-【x-1】
}
基礎題走起:
-
【模板】可持久化數組(可持久化線段樹/平衡樹) \([P3919]\)
【標簽】平衡樹/線段樹/主席樹/可持續化 -
\(KUR-Couriers\) \([P3567]\)
【標簽】線段樹/主席樹/可持續化
八:【可持續化線段樹—動態主席樹(豈可修)】
一樣的,先上例題: \(Dynamic\) \(Rankings\) \([P2617]\) \([ZOJ2112]\) \([BZOJ1901]\)
【題目大意】
在【靜態主席樹】的基礎上加了一個【單點修改】的操作。
【分析】
回想一下,在剛剛靜態線段樹中用到了前綴和的思想:對於原數列的每個位置都建立一個權值線段樹,用 \(tree[pt[i]]\) 表示從第 \(1\) 個到第 \(i\) 個數這個區間中共 \(i\) 個數所維護成的一棵權值線段樹。
它維護的是每個位置前面的一整個大區間,而如果對某個位置 \(i\) 上的數進行了修改,那么從 \(i\) 這個位置開始以后所有的位置上存的信息都要改變(因為位置 \(i\) 后面的所有位置上所存的信息都包含了 \(i\) 的信息)。 那么一次【單點修改】的時間復雜度就是接近於 \(O(nlogn)\) 的,顯然不能嗎,滿足要求。
解決方案:樹(樹狀數組)套樹
【樹狀數組】每一個位置 \(i\) 上的數據 \(C[i]\) 維護的信息是: \(message[i]=\sum_{j=i-lowbit(i)+1}^i message[j]\)
而【靜態主席樹】每一個位置 \(tree[pt[i]]\) 上的數據 \(C[i]\) 維護的是: \(message[pt[i]]=\sum_{j=1}^i message[pt[j]]\)
【樹狀數組】:查詢 \(O(logn)\),修改 \(O(logn)\)
【靜態主席樹 \(+\) 暴力】:查詢 \(O(logn)\),修改 \(O(nlogn)\)
實際上靜態主席樹慢就慢在維護的前綴和范圍實在是太大了,尤其是最后一個位置,直接就是 \([1\) ~ \(N]\) 的范圍。既然如此,為什么不可以把范圍縮小一點呢?如果讓其維護和樹狀數組一樣的區間,雖然查詢第 \(k\) 小上面加了一個 \(O(logn)\),可【單點修改】卻降下了一個 \(O(logn)\) ,於是整個問題的時間復雜度就降了下來,可以完美解決了。
【時間復雜度】
每次查詢遞歸 \(logn\) 層,每層最多需要對 \(max(lowbit(i)) \thickapprox logn\) 個節點進行修改,為 \(O(log^2n)\)。
同理,【單點修改】也應該是 \(O(log^2n)\)。
一共 \(Q\) 次操作,為 \(O(Qlog^2n)\)
初始化建樹是 \(n\) 次【單點修改】,為 \(nlog^2n\)
總時間復雜度為:\(O((n+Q)log^2n)\)
【空間復雜度】
有 \(Q\) 次操作,如果每次操作都是【單點修改】且每次都是改的一個與之前都不同的數,那么離散化的值域為 \(Q+n\)。
如果要建立完整的第 \(0\) 棵樹,會占用 \(n*2\) 個節點,每棵新樹的建立都要新建 \(log^2(Q+n)\) 個節點,一共有 \(nlog^2(Q+n)\)。
\(Q\) 次操作,有 \(Qlog^2(Q+n)\)
總空間復雜度:\((Q+n)*log^2(Q+n)\)
數據規模:
\(1 \leqslant N,Q \leqslant 1e5\)
\(200000*21*21*4*3/1024/1024\)
算下來大概是 \(1009MB\),而跑下來實際是大約 \(154.14Mb\)
所以不用慌張,因為實際空間一定是遠遠小於理論空間的,出題人再怎么良心也不會故意弄一個卡極限的 \(1000Mb\) 數據出來,況且這一類毒瘤題空間限制都大得驚人。
這是洛谷的時空限制:
【Code】
#include<algorithm>
#include<cstring>
#include<cstdio>
#define mid (L+R>>1)
#define Re register int
#define F(i,a,b) for(Re i=a;i<=b;++i)
using namespace std;
const int N=2e5+3;char opt[N];
int x,y,z,n,m,T,t,fu,cnt,tl,tr,a[N],b[N],pt[N],C[N],ptl[20],ptr[20];
//ptl,ptr千萬不要開N,否則memset的時候會TLE到懷疑人生
struct QAQ{int g,lp,rp;}tree[N*400];//本應是441左右,開小一點也無所謂,因為根本用不到
struct O_O{int l,r,k;}Q[N];//儲存Q次查詢的內容,方便離散化
struct T_T{int i,x;}c[N];//離散化數組
inline void in(int &x){
x=fu=0;char c=getchar();
while(c<'0'||c>'9')fu|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=fu?-x:x;
}
inline int ask_(Re L,Re R,Re k){
if(L==R)return b[R];//注意:返回的值需要用到的是哪一個映射數組不能搞錯
Re tmp=0;
F(i,1,tl)tmp-=tree[tree[ptl[i]].lp].g;//計算左子樹信息
F(i,1,tr)tmp+=tree[tree[ptr[i]].lp].g;//計算左子樹信息
if(tmp>=k){
F(i,1,tl)ptl[i]=tree[ptl[i]].lp;//更新ptl,ptr所指向的節點編號
F(i,1,tr)ptr[i]=tree[ptr[i]].lp;
return ask_(L,mid,k);
}
else{
F(i,1,tl)ptl[i]=tree[ptl[i]].rp;
F(i,1,tr)ptr[i]=tree[ptr[i]].rp;
return ask_(mid+1,R,k-tmp);
}
}
inline int ask(Re L,Re R,Re k){//查詢第k小
memset(ptl,0,sizeof(ptl));//萬惡的memset
memset(ptr,0,sizeof(ptr));//數組開太大會瘋狂搶時間復雜度
tl=tr=0;
for(Re i=L-1;i;i-=i&-i)ptl[++tl]=pt[i];//先把所有要更新的位置的線段樹根節點記錄下來
for(Re i=R;i;i-=i&-i)ptr[++tr]=pt[i];//方便后面遞歸更新信息
return ask_(1,m,k);
}
inline void change(Re &p,Re L,Re R,Re w,Re v){
if(!p)p=++cnt;tree[p].g+=v;
if(L==R)return;
if(w<=mid)change(tree[p].lp,L,mid,w,v);
else change(tree[p].rp,mid+1,R,w,v);
}
inline void add(Re x,Re v){//【單點修改】
Re w=lower_bound(b+1,b+m+1,a[x])-b;//注意函數傳進來的參數和這里各種映射數組的調用不要搞錯
for(Re i=x;i<=n;i+=i&-i)change(pt[i],1,m,w,v);//樹狀數組思想更新信息
}
int main(){
// printf("%lf\n",(sizeof(tree))/1024.0/1024.0);
// printf("%lf\n",(sizeof(tree)+sizeof(Q)+sizeof(c)+sizeof(a)+sizeof(b)+sizeof(pt)+sizeof(C))/1024.0/1024.0);
in(n),in(T),m=n;
F(i,1,n)in(a[i]),b[i]=a[i];
F(i,1,T){
scanf(" %c",&opt[i]);
if(opt[i]=='Q')in(Q[i].l),in(Q[i].r),in(Q[i].k);
else in(c[i].i),in(c[i].x),b[++m]=c[i].x;
}
sort(b+1,b+m+1);
m=unique(b+1,b+m+1)-b-1;
F(i,1,n)add(i,1);//初始化建樹
F(i,1,T){
if(opt[i]=='Q')printf("%d\n",ask(Q[i].l,Q[i].r,Q[i].k));
else add(c[i].i,-1),a[c[i].i]=c[i].x,add(c[i].i,1);
//先讓這個位置上原來的數減少一個,再把新數加一個,就達到了替換的目的
}
}
高檔題走起:
- 【模板】二逼平衡樹(樹套樹)\([P3380]\) \([BZOJ3196]\) \([TYVJ1730]\)
【標簽】樹套樹/平衡樹/線段樹/樹狀數組
【題解】\(Xing\)_\(Ling\)
(略微有點變態)