談一類神奇的數據結構——貓樹
貓樹是一個有趣的數據結構,之前一直覺得這玩意兒應該很玄學,但學了之後發現還是挺樸素也挺好打的數據結構
一、貓樹與普通線段樹的區別
要說兩者之間的區別,大概就是詢問(和修改)的復雜度了
詢問復雜度
首先線段樹的詢問復雜度肯定是 \(O(\log n)\) 的,這點沒什麽毛病吧?
然後貓樹的詢問復雜度呢?貓樹的詢問復雜度是 \(O(1)\)
這就是貓樹的優勢了,詢問 O 1 ,少一個 log 的話,大概是幾十倍的時間,但親測大概速度也就比普通線段樹快了一倍
究其原因的話還是普通線段樹的 log 基本是跑不滿的(所以說可能我數據造太爛了吧...)
但是不要小看這個“快一兩倍”,這已經不是常數的問題了啊!(何況這只是我不大靠譜的測試)
就好像莫隊的小優化能快個將近一倍之類的...
那麽修改?
修改復雜度
線段樹當然還是 \(O(\log n)\)
但是貓樹就要 \(O(n)\) 了,這個等到下面講算法思路的時候就會提到了
所以說基本上貓樹不用在待修改的區間詢問題中(瞬間感覺沒什麽用了)
二、貓樹的作用
聽你這麽說,貓樹不就是 ST 表麽?
那麽其實兩者並不一樣,因為他們能維護的信息範圍不一樣
線段樹能維護的信息貓樹基本都能維護,比如什麽區間和、區間 gcd 、最大子段和 等 滿足結合律且支持快速合並的信息
但是普通的 ST 表能夠處理最大子段和麽?(普通的 st 表當然不行,但用上貓樹的思想就不一定了,博主沒試過,有興趣的讀者可以考慮一下...)
說了這麽多,該談談算法實現了吧?
三、貓樹的算法實現
我們假設當前查詢的區間為 l , r
那麽我們是不是可以考慮把這個區間分成兩份,然後如果我們已經預處理除了這兩個區間的信息,是不是就可以合並得到答案了呢?
那麽現在的問題就是怎麽預處理這兩個區間的信息
其實關鍵就是要考慮可以使得任意一種區間都能被分成兩份處理過的區間
那麽我們先考慮用線段樹中類似分治的思想,預處理區間信息
我們考慮把 1~n 整個區間分成兩份 1~mid , mid+1~n
然後對於兩分區間,我們先從 mid 和 mid+1 出發,\(O(n)\) 地像兩邊去遍歷區間中的每個元素,同時維護要處理的信息
等兩個區間都處理完之後,我們再向下遞歸,將兩個區間繼續分下去,即叠代以上步驟直到區間表示的只有一個數
那麽這樣的復雜度是 \(O(n\log n)\) 的,也就是預處理的復雜度並不比線段樹差了
但是這裏又有了一個問題: 我們之前說過的要滿足每個區間都能被分成兩份預處理過的區間
那麽我們就要證明這樣的預處理能滿足以上條件了
proof:
我們看圖說話
我們可以發現,當選擇任意兩個點後,這兩個點之間的區間必然可以用他們在這顆線段樹上的 \(LCA\) 的中間點必然可以將這個區間分成兩段已處理過信息的區間
你可以隨意嘗試一下(當然我圖畫的有點...)
(為什麽會這麽神奇...)
證明一下,我們先將當前的兩個點表示在根節點上
我們發現這兩個點並不能被當前所在點的中間點分為兩份,於是我們將他們下移進入右節點
我們發現還是不能被中間點分成兩份,繼續下移
最後我們總能發現可以成功分割
但是呢,按照上面的找尋分割點的方法,我們發現好像復雜度還是 \(O(n)\)的?(難道復雜度是假的?)
別急,上面只是證明分割的可行性,並不是找尋分割點的方法
之前我們有提到分割點在 \(LCA\) 上,所以我們只需要處理出 \(LCA\) 的位置就好了,難道用樹剖或是倍增?復雜度還是沒變啊!頂多變成了 \(O(loglogn)\) 啊!
我們觀察一下就可以發現(或者說根據線段樹的性質來說),線段樹中兩個葉子結點的 \(LCA\) 其實就是他們位置編號的最長公共前綴(二進制下)
eg : \((10001)_2 (10110)_2\) 兩個節點的 \(LCA\) 就是 \((10)_2\)
那麽怎麽快速求出兩個數的最長公共前綴?
這裏要用到非常妙的一個辦法:
我們將兩個數異或之後可以發現他們的公共前綴不見了,即最高位的位置後移了 \(\log LCA.len\) , 其中 \(LCA.len\) 表示 \(LCA\) 節點在二進制下的長度
那麽我們就可以預處理一下 log 數組,然後在詢問的時候就可以快速求出兩個詢問節點的 \(LCA\) 所在的 層 了
等等,層?不用求出編號的麽?
這裏解釋一下,為了省空間,我們考慮將同一層處理出的信息放在一個數組裏,畢竟他們互相之間沒有相交
並且,這麽做的話,查詢的時候就只需要得到 \(LCA\) 所在層,然後將 l、 r 直接帶入就可以合並求解了
四、貓樹的代碼實現
以處理區間最大子段和為例:
//by Judge
#include<cstdio>
#include<iostream>
#define ll long long
using namespace std;
const int M=2e5+3;
#ifndef Judge
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
#endif
char buf[1<<21],*p1=buf,*p2=buf;
inline int read(){ int x=0,f=1; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f;
} char sr[1<<21],z[20];int C=-1,Z;
inline void Ot(){fwrite(sr,1,C+1,stdout),C=-1;}
inline void print(int x,char chr='\n'){
if(C>1<<20)Ot();if(x<0)sr[++C]=45,x=-x;
while(z[++Z]=x%10+48,x/=10);
while(sr[++C]=z[Z],--Z);sr[++C]=chr;
} int n,m,a[M],len,lg[M<<2],pos[M],p[21][M],s[21][M];
// p 數組為區間最大子段和, s 數組為包含端點的最大子段和
inline int Max(int a,int b){return a>b?a:b;}
#define ls k<<1
#define rs k<<1|1
#define mid (l+r>>1)
#define lson ls,l,mid
#define rson rs,mid+1,r
void build(int k,int l,int r,int d){
if(l==r) return pos[l]=k,void(); int prep,sm;
// 處理左半部分
p[d][mid]=s[d][mid]=prep=sm=a[mid],sm=Max(sm,0);
for(int i=mid-1;i>=l;--i)
prep+=a[i],sm+=a[i],s[d][i]=Max(s[d][i+1],prep),
p[d][i]=Max(p[d][i+1],sm),sm=Max(sm,0);
// 處理右半部分
p[d][mid+1]=s[d][mid+1]=prep=sm=a[mid+1],sm=Max(sm,0);
for(int i=mid+2;i<=r;++i)
prep+=a[i],sm+=a[i],s[d][i]=Max(s[d][i-1],prep),
p[d][i]=Max(p[d][i-1],sm),sm=Max(sm,0);
build(lson,d+1),build(rson,d+1); //向下遞歸
}
inline int query(int l,int r){ if(l==r) return a[l];
int d=lg[pos[l]]-lg[pos[l]^pos[r]]; //得到 lca 所在層
return Max(Max(p[d][l],p[d][r]),s[d][l]+s[d][r]);
}
int main(){ n=read(),len=2;
while(len<n) len<<=1;
for(int i=1;i<=n;++i) a[i]=read();
for(int i=2,l=len<<1;i<=l;++i)
lg[i]=lg[i>>1]+1;
build(1,1,len,1);
for(int m=read(),l,r;m;--m)
l=read(),r=read(),
print(query(l,r));
return Ot(),0;
}
碼量其實會少很多,可以看到最主要的碼量就在 \(build\) 裏面,但是 \(build\) 函數的思路還是很清晰的
五、貓樹的推薦例題
GSS1
就是上面的板子
其他的能拿來當純模板的基本找不到(可見限制還是蠻大的,畢竟帶修改的不行),不過一些要拿線段樹來優化的題目(比如線段樹優化 dp )還是可以用上的...吧?
參考資料:%%%zjp大佬
談一類神奇的數據結構——貓樹