【學習筆記】線段樹詳解(全)


【學習筆記】線段樹詳解(全)

和三個同學一起搞了接近兩個月的線段樹,頭都要炸了T_T,趁心態尚未涼之前趕快把東西記下來。。。


【目錄】


一:【基礎姿勢】

\(piao\) 一下隔壁大佬的文章QAQ: \(Silent\)_\(EAG\)

基礎題走起:

高檔題走起:


二:【懶標記】

見隔壁:\(Silent\)_\(EAG\)

基礎題走起:

高檔題走起:


三:【掃~描~線~】

見隔壁:\(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;
}

基礎題走起:

高檔題走起:


七:【可持續化線段樹—靜態主席樹】

來看一道經典的例題 【模板】可持久化線段樹 \(1\) (主席樹) \([3834]\) \(/\) \(K\)-\(th\) \(Number\) \([P3834]\) \([POJ2104]\) \([SP3946]\)

【題目大意】

給定一個長為 \(n\) 的數列以及 \(Q\) 個查詢,每次查詢輸入兩個整數 \(l\),\(r\),輸出數列中 \(l\) ~ \(r\)\(K\) 小的數。

【 分析】

此題有三個關鍵點:

  1. 求一棵數列中的第 \(K\) 大或第 \(K\) 小的數。
    解決方案:權值線段樹

  2. 如果僅僅是這樣,則非常好辦,給出的詢問是一段區間。一段長為 \(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]]\)

  3. 可內存還是遠遠不夠,即使是使用了【動態開點】 \(+\) 離散化,值域由 \(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】 
}

基礎題走起:


八:【可持續化線段樹—動態主席樹(可修)】

一樣的,先上例題: \(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);
//先讓這個位置上原來的數減少一個,再把新數加一個,就達到了替換的目的 
    }
}

高檔題走起:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM